@camstack/system 1.2.62 → 1.2.64
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/addon-utils.js +1 -1
- package/dist/addon-utils.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 +3 -3
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- 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-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 +123 -884
- package/dist/builtins/device-manager/device-manager.addon.mjs +123 -884
- 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 +2 -2
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
- package/dist/builtins/platform-probe/index.js +2 -2
- package/dist/builtins/platform-probe/index.mjs +2 -2
- 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 +73 -7
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +73 -7
- package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +19 -0
- 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-DnKpFaP1.mjs → dist-CMb7aKS0.mjs} +1227 -1330
- package/dist/{dist-DgPUarcs.js → dist-DP3nHlNM.js} +1251 -1366
- package/dist/http/asset-compression.d.ts +93 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -9
- package/dist/index.mjs +65 -11
- package/dist/kernel/heap-watch.d.ts +55 -0
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
- package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-BE6l8Y4W.js} +21 -15
- package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-Dz1ryHIN.mjs} +21 -15
- package/dist/{model-download-service-D8B-4ktF.mjs → model-download-service-CTBHzORJ.mjs} +220 -2
- package/dist/{model-download-service-Cpc6SWzV.js → model-download-service-Caj1kaSi.js} +220 -2
- 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
|
@@ -5967,12 +5967,34 @@ var streamBrokerCapability = {
|
|
|
5967
5967
|
kinds: zod.z.array(EventMediaKindSchema).min(1).default(["mp4"]),
|
|
5968
5968
|
/** GIF geometry. The video keeps the source's own. */
|
|
5969
5969
|
gifMaxWidth: zod.z.number().int().min(120).max(1280).default(640),
|
|
5970
|
-
gifFps: zod.z.number().int().min(1).max(15).default(8),
|
|
5971
5970
|
/**
|
|
5972
|
-
*
|
|
5973
|
-
*
|
|
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.
|
|
5974
5975
|
*/
|
|
5975
|
-
|
|
5976
|
+
gifFps: zod.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: zod.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: zod.z.number().min(1).max(8).default(4)
|
|
5976
5998
|
}), EventMediaProductionSchema, {
|
|
5977
5999
|
kind: "mutation",
|
|
5978
6000
|
auth: "admin"
|
|
@@ -6565,6 +6587,28 @@ function kebabToCamel(s) {
|
|
|
6565
6587
|
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
6566
6588
|
* must stay so.
|
|
6567
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
|
+
}
|
|
6568
6612
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
6569
6613
|
* agent is the reason placement is not fixed to the hub. */
|
|
6570
6614
|
var CoreBlockPlacementSchema = zod.z.union([zod.z.literal("hub"), zod.z.string().min(1)]);
|
|
@@ -6653,6 +6697,23 @@ var coreBlocksCapability = {
|
|
|
6653
6697
|
auth: "admin"
|
|
6654
6698
|
}),
|
|
6655
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(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema }), {
|
|
6713
|
+
kind: "mutation",
|
|
6714
|
+
auth: "admin"
|
|
6715
|
+
}),
|
|
6716
|
+
/**
|
|
6656
6717
|
* Type-check without saving — what the editor calls as the author types, so
|
|
6657
6718
|
* the compiler's verdict is the same one the server will reach.
|
|
6658
6719
|
*/
|
|
@@ -7693,909 +7754,173 @@ var deviceExportCapability = {
|
|
|
7693
7754
|
unexposeDevice: method(UnexposeInputSchema, zod.z.void(), { kind: "mutation" })
|
|
7694
7755
|
}
|
|
7695
7756
|
};
|
|
7757
|
+
var ProviderStatusSchema = zod.z.object({
|
|
7758
|
+
connected: zod.z.boolean(),
|
|
7759
|
+
deviceCount: zod.z.number(),
|
|
7760
|
+
error: zod.z.string().optional()
|
|
7761
|
+
});
|
|
7762
|
+
zod.z.object({
|
|
7763
|
+
externalId: zod.z.string(),
|
|
7764
|
+
name: zod.z.string(),
|
|
7765
|
+
type: zod.z.string(),
|
|
7766
|
+
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
7767
|
+
});
|
|
7696
7768
|
/**
|
|
7697
|
-
*
|
|
7698
|
-
*
|
|
7699
|
-
*
|
|
7700
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
7701
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
7702
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
7703
|
-
* 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`.
|
|
7704
7772
|
*/
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
/**
|
|
7711
|
-
*
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7773
|
+
var DiscoveryCandidateSchema = zod.z.object({
|
|
7774
|
+
stableId: zod.z.string(),
|
|
7775
|
+
type: zod.z.enum(DeviceType),
|
|
7776
|
+
suggestedName: zod.z.string(),
|
|
7777
|
+
prefilledConfig: zod.z.record(zod.z.string(), zod.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
|
+
});
|
|
7718
7786
|
/**
|
|
7719
|
-
*
|
|
7720
|
-
*
|
|
7721
|
-
*
|
|
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.
|
|
7722
7790
|
*/
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
/**
|
|
7734
|
-
*
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7791
|
+
var DeviceSummarySchema = zod.z.object({
|
|
7792
|
+
id: zod.z.number(),
|
|
7793
|
+
stableId: zod.z.string(),
|
|
7794
|
+
addonId: zod.z.string(),
|
|
7795
|
+
type: zod.z.string(),
|
|
7796
|
+
name: zod.z.string(),
|
|
7797
|
+
parentDeviceId: zod.z.number().nullable(),
|
|
7798
|
+
online: zod.z.boolean(),
|
|
7799
|
+
features: zod.z.array(zod.z.string()),
|
|
7800
|
+
config: zod.z.record(zod.z.string(), zod.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 = zod.z.object({
|
|
7814
|
+
status: zod.z.enum(["ok", "error"]),
|
|
7815
|
+
labels: zod.z.array(zod.z.string()).optional(),
|
|
7816
|
+
error: zod.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 = zod.z.unknown();
|
|
7827
|
+
var deviceProviderCapability = {
|
|
7828
|
+
name: "device-provider",
|
|
7829
|
+
scope: "system",
|
|
7830
|
+
mode: "collection",
|
|
7831
|
+
methods: {
|
|
7832
|
+
start: method(zod.z.void(), zod.z.void(), { kind: "mutation" }),
|
|
7833
|
+
stop: method(zod.z.void(), zod.z.void(), { kind: "mutation" }),
|
|
7834
|
+
getStatus: method(zod.z.void(), ProviderStatusSchema),
|
|
7835
|
+
getDevices: method(zod.z.void(), zod.z.array(zod.z.object({
|
|
7836
|
+
id: zod.z.string(),
|
|
7837
|
+
name: zod.z.string(),
|
|
7838
|
+
type: zod.z.string()
|
|
7839
|
+
}))),
|
|
7840
|
+
supportsDiscovery: method(zod.z.object({}), zod.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(zod.z.object({ params: zod.z.record(zod.z.string(), zod.z.unknown()).optional() }), zod.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(zod.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(zod.z.object({}), zod.z.object({ deviceType: zod.z.enum(DeviceType).nullable() })),
|
|
7865
|
+
adoptDiscoveredDevice: method(zod.z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
7866
|
+
kind: "mutation",
|
|
7867
|
+
auth: "admin"
|
|
7868
|
+
}),
|
|
7869
|
+
supportsManualCreation: method(zod.z.object({}), zod.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(zod.z.object({ type: zod.z.enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
7877
|
+
createDevice: method(zod.z.object({
|
|
7878
|
+
type: zod.z.enum(DeviceType),
|
|
7879
|
+
config: zod.z.record(zod.z.string(), zod.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(zod.z.object({
|
|
7897
|
+
type: zod.z.enum(DeviceType),
|
|
7898
|
+
key: zod.z.string(),
|
|
7899
|
+
value: zod.z.unknown(),
|
|
7900
|
+
formValues: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
7901
|
+
}), FieldProbeResultSchema, {
|
|
7902
|
+
kind: "mutation",
|
|
7903
|
+
auth: "admin"
|
|
7904
|
+
})
|
|
7739
7905
|
}
|
|
7740
7906
|
};
|
|
7741
7907
|
/**
|
|
7742
|
-
*
|
|
7743
|
-
*
|
|
7744
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
7745
|
-
* own-property check against it.
|
|
7908
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
7909
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
7746
7910
|
*
|
|
7747
|
-
*
|
|
7748
|
-
*
|
|
7749
|
-
*
|
|
7750
|
-
*
|
|
7751
|
-
* callable — they are simply "unknown function" at parse time.
|
|
7911
|
+
* Replaces:
|
|
7912
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
7913
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
7914
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
7752
7915
|
*
|
|
7753
|
-
*
|
|
7754
|
-
*
|
|
7755
|
-
*
|
|
7756
|
-
*
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
}
|
|
7762
|
-
function asString$1(value, name, index) {
|
|
7763
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
7764
|
-
return value;
|
|
7765
|
-
}
|
|
7766
|
-
function finiteResult(value, name) {
|
|
7767
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
7768
|
-
return value;
|
|
7769
|
-
}
|
|
7770
|
-
function allFiniteNumbers(args, name) {
|
|
7771
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
7772
|
-
}
|
|
7773
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
7774
|
-
var table = {
|
|
7775
|
-
min: {
|
|
7776
|
-
minArgs: 1,
|
|
7777
|
-
maxArgs: INF,
|
|
7778
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
7779
|
-
},
|
|
7780
|
-
max: {
|
|
7781
|
-
minArgs: 1,
|
|
7782
|
-
maxArgs: INF,
|
|
7783
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
7784
|
-
},
|
|
7785
|
-
abs: {
|
|
7786
|
-
minArgs: 1,
|
|
7787
|
-
maxArgs: 1,
|
|
7788
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
7789
|
-
},
|
|
7790
|
-
floor: {
|
|
7791
|
-
minArgs: 1,
|
|
7792
|
-
maxArgs: 1,
|
|
7793
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
7794
|
-
},
|
|
7795
|
-
ceil: {
|
|
7796
|
-
minArgs: 1,
|
|
7797
|
-
maxArgs: 1,
|
|
7798
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
7799
|
-
},
|
|
7800
|
-
sqrt: {
|
|
7801
|
-
minArgs: 1,
|
|
7802
|
-
maxArgs: 1,
|
|
7803
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
7804
|
-
},
|
|
7805
|
-
round: {
|
|
7806
|
-
minArgs: 1,
|
|
7807
|
-
maxArgs: 2,
|
|
7808
|
-
apply: (args) => {
|
|
7809
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
7810
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
7811
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
7812
|
-
const factor = 10 ** digits;
|
|
7813
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
7814
|
-
}
|
|
7815
|
-
},
|
|
7816
|
-
pow: {
|
|
7817
|
-
minArgs: 2,
|
|
7818
|
-
maxArgs: 2,
|
|
7819
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
7820
|
-
},
|
|
7821
|
-
clamp: {
|
|
7822
|
-
minArgs: 3,
|
|
7823
|
-
maxArgs: 3,
|
|
7824
|
-
apply: (args) => {
|
|
7825
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
7826
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
7827
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
7828
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
7829
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
7830
|
-
}
|
|
7831
|
-
},
|
|
7832
|
-
avg: {
|
|
7833
|
-
minArgs: 1,
|
|
7834
|
-
maxArgs: INF,
|
|
7835
|
-
apply: (args) => {
|
|
7836
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
7837
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
7838
|
-
}
|
|
7839
|
-
},
|
|
7840
|
-
sum: {
|
|
7841
|
-
minArgs: 1,
|
|
7842
|
-
maxArgs: INF,
|
|
7843
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
7844
|
-
},
|
|
7845
|
-
coalesce: {
|
|
7846
|
-
minArgs: 1,
|
|
7847
|
-
maxArgs: INF,
|
|
7848
|
-
apply: (args) => {
|
|
7849
|
-
for (const a of args) if (a !== null) return a;
|
|
7850
|
-
return null;
|
|
7851
|
-
}
|
|
7852
|
-
},
|
|
7853
|
-
age: {
|
|
7854
|
-
minArgs: 2,
|
|
7855
|
-
maxArgs: 2,
|
|
7856
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
7857
|
-
},
|
|
7858
|
-
convert: {
|
|
7859
|
-
minArgs: 3,
|
|
7860
|
-
maxArgs: 3,
|
|
7861
|
-
apply: (args, hooks) => {
|
|
7862
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
7863
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
7864
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
7865
|
-
if (hooks.convert) {
|
|
7866
|
-
const out = hooks.convert(x, from, to);
|
|
7867
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
7868
|
-
return finiteResult(out, "convert");
|
|
7869
|
-
}
|
|
7870
|
-
if (from === to) return x;
|
|
7871
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
7872
|
-
}
|
|
7873
|
-
}
|
|
7874
|
-
};
|
|
7875
|
-
/** Frozen, null-prototype builtin table. */
|
|
7876
|
-
var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
|
|
7877
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
7878
|
-
* callees at parse time (immediate author feedback). */
|
|
7879
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
7880
|
-
/**
|
|
7881
|
-
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
7882
|
-
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
7883
|
-
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
7884
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
7885
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
7886
|
-
* is a parse error with a source position, so member access / assignment /
|
|
7887
|
-
* template literals are lexically impossible.
|
|
7888
|
-
*/
|
|
7889
|
-
var KEYWORDS = new Set([
|
|
7890
|
-
"true",
|
|
7891
|
-
"false",
|
|
7892
|
-
"null"
|
|
7893
|
-
]);
|
|
7894
|
-
function isDigit(ch) {
|
|
7895
|
-
return ch >= "0" && ch <= "9";
|
|
7896
|
-
}
|
|
7897
|
-
function isIdentStart(ch) {
|
|
7898
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
7899
|
-
}
|
|
7900
|
-
function isIdentPart(ch) {
|
|
7901
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
7902
|
-
}
|
|
7903
|
-
function isWhitespace(ch) {
|
|
7904
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
7905
|
-
}
|
|
7906
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
7907
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
7908
|
-
* string. */
|
|
7909
|
-
function tokenize(source) {
|
|
7910
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
7911
|
-
const tokens = [];
|
|
7912
|
-
let i = 0;
|
|
7913
|
-
const n = source.length;
|
|
7914
|
-
while (i < n) {
|
|
7915
|
-
const ch = source[i];
|
|
7916
|
-
if (isWhitespace(ch)) {
|
|
7917
|
-
i += 1;
|
|
7918
|
-
continue;
|
|
7919
|
-
}
|
|
7920
|
-
if (isDigit(ch)) {
|
|
7921
|
-
const start = i;
|
|
7922
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
7923
|
-
if (i < n && source[i] === ".") {
|
|
7924
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
7925
|
-
i += 1;
|
|
7926
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
7927
|
-
}
|
|
7928
|
-
const text = source.slice(start, i);
|
|
7929
|
-
const value = Number(text);
|
|
7930
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
7931
|
-
tokens.push({
|
|
7932
|
-
type: "number",
|
|
7933
|
-
value,
|
|
7934
|
-
pos: start
|
|
7935
|
-
});
|
|
7936
|
-
continue;
|
|
7937
|
-
}
|
|
7938
|
-
if (ch === "'" || ch === "\"") {
|
|
7939
|
-
const quote = ch;
|
|
7940
|
-
const start = i;
|
|
7941
|
-
i += 1;
|
|
7942
|
-
let out = "";
|
|
7943
|
-
let closed = false;
|
|
7944
|
-
while (i < n) {
|
|
7945
|
-
const c = source[i];
|
|
7946
|
-
if (c === "\\") {
|
|
7947
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
7948
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
7949
|
-
out += next;
|
|
7950
|
-
i += 2;
|
|
7951
|
-
continue;
|
|
7952
|
-
}
|
|
7953
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
7954
|
-
}
|
|
7955
|
-
if (c === quote) {
|
|
7956
|
-
closed = true;
|
|
7957
|
-
i += 1;
|
|
7958
|
-
break;
|
|
7959
|
-
}
|
|
7960
|
-
out += c;
|
|
7961
|
-
i += 1;
|
|
7962
|
-
}
|
|
7963
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
7964
|
-
tokens.push({
|
|
7965
|
-
type: "string",
|
|
7966
|
-
value: out,
|
|
7967
|
-
pos: start
|
|
7968
|
-
});
|
|
7969
|
-
continue;
|
|
7970
|
-
}
|
|
7971
|
-
if (isIdentStart(ch)) {
|
|
7972
|
-
const start = i;
|
|
7973
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
7974
|
-
const text = source.slice(start, i);
|
|
7975
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
7976
|
-
type: "keyword",
|
|
7977
|
-
keyword: keywordOf(text),
|
|
7978
|
-
pos: start
|
|
7979
|
-
});
|
|
7980
|
-
else tokens.push({
|
|
7981
|
-
type: "identifier",
|
|
7982
|
-
name: text,
|
|
7983
|
-
pos: start
|
|
7984
|
-
});
|
|
7985
|
-
continue;
|
|
7986
|
-
}
|
|
7987
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
7988
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
7989
|
-
tokens.push({
|
|
7990
|
-
type: "punct",
|
|
7991
|
-
punct: two,
|
|
7992
|
-
pos: i
|
|
7993
|
-
});
|
|
7994
|
-
i += 2;
|
|
7995
|
-
continue;
|
|
7996
|
-
}
|
|
7997
|
-
if (isSinglePunct(ch)) {
|
|
7998
|
-
tokens.push({
|
|
7999
|
-
type: "punct",
|
|
8000
|
-
punct: ch,
|
|
8001
|
-
pos: i
|
|
8002
|
-
});
|
|
8003
|
-
i += 1;
|
|
8004
|
-
continue;
|
|
8005
|
-
}
|
|
8006
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
8007
|
-
}
|
|
8008
|
-
tokens.push({
|
|
8009
|
-
type: "eof",
|
|
8010
|
-
pos: n
|
|
8011
|
-
});
|
|
8012
|
-
return tokens;
|
|
8013
|
-
}
|
|
8014
|
-
function keywordOf(text) {
|
|
8015
|
-
if (text === "true") return "true";
|
|
8016
|
-
if (text === "false") return "false";
|
|
8017
|
-
return "null";
|
|
8018
|
-
}
|
|
8019
|
-
function isSinglePunct(ch) {
|
|
8020
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
8021
|
-
}
|
|
8022
|
-
/**
|
|
8023
|
-
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
8024
|
-
*
|
|
8025
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
8026
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
8027
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
8028
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
8029
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
8030
|
-
* that references a since-removed builtin degrades at read.
|
|
8031
|
-
*
|
|
8032
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
8033
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
8034
|
-
*/
|
|
8035
|
-
/** Binary/logical operator precedence (higher binds tighter). */
|
|
8036
|
-
var BINARY_PRECEDENCE = {
|
|
8037
|
-
"||": 1,
|
|
8038
|
-
"&&": 2,
|
|
8039
|
-
"==": 3,
|
|
8040
|
-
"!=": 3,
|
|
8041
|
-
"<": 4,
|
|
8042
|
-
"<=": 4,
|
|
8043
|
-
">": 4,
|
|
8044
|
-
">=": 4,
|
|
8045
|
-
"+": 5,
|
|
8046
|
-
"-": 5,
|
|
8047
|
-
"*": 6,
|
|
8048
|
-
"/": 6,
|
|
8049
|
-
"%": 6
|
|
8050
|
-
};
|
|
8051
|
-
function isLogicalOp(op) {
|
|
8052
|
-
return op === "&&" || op === "||";
|
|
8053
|
-
}
|
|
8054
|
-
function isBinaryOp(op) {
|
|
8055
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
8056
|
-
}
|
|
8057
|
-
var Parser = class {
|
|
8058
|
-
tokens;
|
|
8059
|
-
pos = 0;
|
|
8060
|
-
nodeCount = 0;
|
|
8061
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
8062
|
-
callees = /* @__PURE__ */ new Set();
|
|
8063
|
-
constructor(tokens) {
|
|
8064
|
-
this.tokens = tokens;
|
|
8065
|
-
}
|
|
8066
|
-
parse() {
|
|
8067
|
-
const ast = this.parseTernary();
|
|
8068
|
-
const tok = this.peek();
|
|
8069
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
8070
|
-
return {
|
|
8071
|
-
ast,
|
|
8072
|
-
identifiers: this.identifiers,
|
|
8073
|
-
callees: this.callees,
|
|
8074
|
-
nodeCount: this.nodeCount
|
|
8075
|
-
};
|
|
8076
|
-
}
|
|
8077
|
-
peek() {
|
|
8078
|
-
return this.tokens[this.pos];
|
|
8079
|
-
}
|
|
8080
|
-
next() {
|
|
8081
|
-
return this.tokens[this.pos++];
|
|
8082
|
-
}
|
|
8083
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
8084
|
-
expectPunct(punct) {
|
|
8085
|
-
const tok = this.peek();
|
|
8086
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
8087
|
-
this.pos += 1;
|
|
8088
|
-
}
|
|
8089
|
-
matchPunct(punct) {
|
|
8090
|
-
const tok = this.peek();
|
|
8091
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
8092
|
-
this.pos += 1;
|
|
8093
|
-
return true;
|
|
8094
|
-
}
|
|
8095
|
-
return false;
|
|
8096
|
-
}
|
|
8097
|
-
countNode() {
|
|
8098
|
-
this.nodeCount += 1;
|
|
8099
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
8100
|
-
}
|
|
8101
|
-
parseTernary() {
|
|
8102
|
-
const test = this.parseBinary(1);
|
|
8103
|
-
if (this.matchPunct("?")) {
|
|
8104
|
-
const consequent = this.parseTernary();
|
|
8105
|
-
this.expectPunct(":");
|
|
8106
|
-
const alternate = this.parseTernary();
|
|
8107
|
-
this.countNode();
|
|
8108
|
-
return {
|
|
8109
|
-
kind: "conditional",
|
|
8110
|
-
test,
|
|
8111
|
-
consequent,
|
|
8112
|
-
alternate
|
|
8113
|
-
};
|
|
8114
|
-
}
|
|
8115
|
-
return test;
|
|
8116
|
-
}
|
|
8117
|
-
parseBinary(minPrec) {
|
|
8118
|
-
let left = this.parseUnary();
|
|
8119
|
-
for (;;) {
|
|
8120
|
-
const tok = this.peek();
|
|
8121
|
-
if (tok.type !== "punct") break;
|
|
8122
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
8123
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
8124
|
-
const op = tok.punct;
|
|
8125
|
-
this.pos += 1;
|
|
8126
|
-
const right = this.parseBinary(prec + 1);
|
|
8127
|
-
this.countNode();
|
|
8128
|
-
if (isLogicalOp(op)) left = {
|
|
8129
|
-
kind: "logical",
|
|
8130
|
-
op,
|
|
8131
|
-
left,
|
|
8132
|
-
right
|
|
8133
|
-
};
|
|
8134
|
-
else if (isBinaryOp(op)) left = {
|
|
8135
|
-
kind: "binary",
|
|
8136
|
-
op,
|
|
8137
|
-
left,
|
|
8138
|
-
right
|
|
8139
|
-
};
|
|
8140
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
8141
|
-
}
|
|
8142
|
-
return left;
|
|
8143
|
-
}
|
|
8144
|
-
parseUnary() {
|
|
8145
|
-
const tok = this.peek();
|
|
8146
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
8147
|
-
const op = tok.punct;
|
|
8148
|
-
this.pos += 1;
|
|
8149
|
-
const operand = this.parseUnary();
|
|
8150
|
-
this.countNode();
|
|
8151
|
-
return {
|
|
8152
|
-
kind: "unary",
|
|
8153
|
-
op,
|
|
8154
|
-
operand
|
|
8155
|
-
};
|
|
8156
|
-
}
|
|
8157
|
-
return this.parsePrimary();
|
|
8158
|
-
}
|
|
8159
|
-
parsePrimary() {
|
|
8160
|
-
const tok = this.next();
|
|
8161
|
-
switch (tok.type) {
|
|
8162
|
-
case "number":
|
|
8163
|
-
this.countNode();
|
|
8164
|
-
return {
|
|
8165
|
-
kind: "literal",
|
|
8166
|
-
value: tok.value
|
|
8167
|
-
};
|
|
8168
|
-
case "string":
|
|
8169
|
-
this.countNode();
|
|
8170
|
-
return {
|
|
8171
|
-
kind: "literal",
|
|
8172
|
-
value: tok.value
|
|
8173
|
-
};
|
|
8174
|
-
case "keyword":
|
|
8175
|
-
this.countNode();
|
|
8176
|
-
return {
|
|
8177
|
-
kind: "literal",
|
|
8178
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
8179
|
-
};
|
|
8180
|
-
case "identifier": {
|
|
8181
|
-
const nextTok = this.peek();
|
|
8182
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
8183
|
-
this.identifiers.add(tok.name);
|
|
8184
|
-
this.countNode();
|
|
8185
|
-
return {
|
|
8186
|
-
kind: "identifier",
|
|
8187
|
-
name: tok.name
|
|
8188
|
-
};
|
|
8189
|
-
}
|
|
8190
|
-
case "punct":
|
|
8191
|
-
if (tok.punct === "(") {
|
|
8192
|
-
const inner = this.parseTernary();
|
|
8193
|
-
this.expectPunct(")");
|
|
8194
|
-
return inner;
|
|
8195
|
-
}
|
|
8196
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
8197
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
8198
|
-
}
|
|
8199
|
-
}
|
|
8200
|
-
parseCall(callee, pos) {
|
|
8201
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
8202
|
-
this.expectPunct("(");
|
|
8203
|
-
const args = [];
|
|
8204
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
8205
|
-
args.push(this.parseTernary());
|
|
8206
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
8207
|
-
if (this.matchPunct(",")) continue;
|
|
8208
|
-
this.expectPunct(")");
|
|
8209
|
-
break;
|
|
8210
|
-
}
|
|
8211
|
-
this.callees.add(callee);
|
|
8212
|
-
this.countNode();
|
|
8213
|
-
return {
|
|
8214
|
-
kind: "call",
|
|
8215
|
-
callee,
|
|
8216
|
-
args
|
|
8217
|
-
};
|
|
8218
|
-
}
|
|
8219
|
-
};
|
|
8220
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
8221
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
8222
|
-
function parseExpression(source) {
|
|
8223
|
-
return new Parser(tokenize(source)).parse();
|
|
8224
|
-
}
|
|
8225
|
-
/**
|
|
8226
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
8227
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
8228
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
8229
|
-
* one per read on a hot resolve path.
|
|
8230
|
-
*
|
|
8231
|
-
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
8232
|
-
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
8233
|
-
* callers is safe and maximises hit rate.
|
|
8234
|
-
*/
|
|
8235
|
-
var cache = /* @__PURE__ */ new Map();
|
|
8236
|
-
function getCached(source) {
|
|
8237
|
-
const hit = cache.get(source);
|
|
8238
|
-
if (hit !== void 0) {
|
|
8239
|
-
cache.delete(source);
|
|
8240
|
-
cache.set(source, hit);
|
|
8241
|
-
return hit;
|
|
8242
|
-
}
|
|
8243
|
-
let result;
|
|
8244
|
-
try {
|
|
8245
|
-
result = {
|
|
8246
|
-
ok: true,
|
|
8247
|
-
parsed: parseExpression(source)
|
|
8248
|
-
};
|
|
8249
|
-
} catch (err) {
|
|
8250
|
-
result = {
|
|
8251
|
-
ok: false,
|
|
8252
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
8253
|
-
};
|
|
8254
|
-
}
|
|
8255
|
-
cache.set(source, result);
|
|
8256
|
-
if (cache.size > 256) {
|
|
8257
|
-
const oldest = cache.keys().next().value;
|
|
8258
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
8259
|
-
}
|
|
8260
|
-
return result;
|
|
8261
|
-
}
|
|
8262
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
8263
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
8264
|
-
function compileExpressionSafe(source) {
|
|
8265
|
-
return getCached(source);
|
|
8266
|
-
}
|
|
8267
|
-
/**
|
|
8268
|
-
* Tree-walking evaluator for the safe expression mini-language.
|
|
8269
|
-
*
|
|
8270
|
-
* SECURITY (spec §4 rule 2/5):
|
|
8271
|
-
* - The scope is an `Object.create(null)` copy of ONLY the caller's own
|
|
8272
|
-
* enumerable binding entries, so `name in scope` is a pure own-key check and
|
|
8273
|
-
* `constructor` / `__proto__` / `toString` are plain unknown identifiers.
|
|
8274
|
-
* - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
|
|
8275
|
-
* directly — the only external calls are into the frozen builtin table.
|
|
8276
|
-
* - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
|
|
8277
|
-
* construction; the step counter is defense-in-depth for a crafted max-size
|
|
8278
|
-
* AST. Nothing blocks: there are no timers, awaits or unbounded loops.
|
|
8279
|
-
*/
|
|
8280
|
-
var EMPTY_HOOKS = Object.freeze({});
|
|
8281
|
-
/** Build a null-prototype scope from own-enumerable binding entries. Inherited
|
|
8282
|
-
* keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
|
|
8283
|
-
* so nothing smuggles in via the prototype chain. */
|
|
8284
|
-
function createExpressionScope(bindings) {
|
|
8285
|
-
const scope = Object.create(null);
|
|
8286
|
-
for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
|
|
8287
|
-
return scope;
|
|
8288
|
-
}
|
|
8289
|
-
function isFiniteNumber(value) {
|
|
8290
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
8291
|
-
}
|
|
8292
|
-
/** JS truthiness of a primitive value. */
|
|
8293
|
-
function truthy(value) {
|
|
8294
|
-
return Boolean(value);
|
|
8295
|
-
}
|
|
8296
|
-
function requireFinite(value, context) {
|
|
8297
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
|
|
8298
|
-
return value;
|
|
8299
|
-
}
|
|
8300
|
-
function step(ctx) {
|
|
8301
|
-
ctx.steps += 1;
|
|
8302
|
-
if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
|
|
8303
|
-
}
|
|
8304
|
-
function evalNode(node, ctx) {
|
|
8305
|
-
step(ctx);
|
|
8306
|
-
switch (node.kind) {
|
|
8307
|
-
case "literal": return node.value;
|
|
8308
|
-
case "identifier":
|
|
8309
|
-
if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
|
|
8310
|
-
return ctx.scope[node.name];
|
|
8311
|
-
case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
|
|
8312
|
-
case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
|
|
8313
|
-
case "logical": {
|
|
8314
|
-
const left = evalNode(node.left, ctx);
|
|
8315
|
-
if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
|
|
8316
|
-
return truthy(left) ? left : evalNode(node.right, ctx);
|
|
8317
|
-
}
|
|
8318
|
-
case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
|
|
8319
|
-
case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
|
|
8320
|
-
}
|
|
8321
|
-
}
|
|
8322
|
-
function evalUnary(op, operand) {
|
|
8323
|
-
if (op === "!") return !truthy(operand);
|
|
8324
|
-
if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
|
|
8325
|
-
return requireFinite(-operand, "unary \"-\"");
|
|
8326
|
-
}
|
|
8327
|
-
function evalBinary(op, left, right) {
|
|
8328
|
-
switch (op) {
|
|
8329
|
-
case "==": return left === right;
|
|
8330
|
-
case "!=": return left !== right;
|
|
8331
|
-
case "+":
|
|
8332
|
-
if (typeof left === "string" && typeof right === "string") return left + right;
|
|
8333
|
-
if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
|
|
8334
|
-
throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
|
|
8335
|
-
case "-":
|
|
8336
|
-
case "*":
|
|
8337
|
-
case "/":
|
|
8338
|
-
case "%":
|
|
8339
|
-
if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
|
|
8340
|
-
return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
|
|
8341
|
-
case "<":
|
|
8342
|
-
case "<=":
|
|
8343
|
-
case ">":
|
|
8344
|
-
case ">=":
|
|
8345
|
-
if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
8346
|
-
if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
8347
|
-
throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
|
|
8348
|
-
}
|
|
8349
|
-
}
|
|
8350
|
-
function evalCall(callee, args, hooks) {
|
|
8351
|
-
if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
|
|
8352
|
-
const builtin = EXPRESSION_BUILTINS[callee];
|
|
8353
|
-
if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
|
|
8354
|
-
return builtin.apply(args, hooks);
|
|
8355
|
-
}
|
|
8356
|
-
/** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
|
|
8357
|
-
* runtime failure (unknown identifier, type mismatch, non-finite result,
|
|
8358
|
-
* step-budget exhaustion). */
|
|
8359
|
-
function evaluateAst(node, scope, opts) {
|
|
8360
|
-
return evalNode(node, {
|
|
8361
|
-
scope,
|
|
8362
|
-
hooks: opts?.hooks ?? EMPTY_HOOKS,
|
|
8363
|
-
maxSteps: opts?.maxSteps ?? 4096,
|
|
8364
|
-
steps: 0
|
|
8365
|
-
});
|
|
8366
|
-
}
|
|
8367
|
-
/**
|
|
8368
|
-
* Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
|
|
8369
|
-
* Non-primitive values (objects, arrays, `undefined`, functions, bigint,
|
|
8370
|
-
* symbol) and non-finite numbers become `undefined` so the caller can apply
|
|
8371
|
-
* its binding-miss policy (→ `null`). `null` itself is a valid value.
|
|
8372
|
-
*/
|
|
8373
|
-
function toExpressionValue(raw) {
|
|
8374
|
-
if (raw === null) return null;
|
|
8375
|
-
if (typeof raw === "string") return raw;
|
|
8376
|
-
if (typeof raw === "boolean") return raw;
|
|
8377
|
-
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
8378
|
-
}
|
|
8379
|
-
/**
|
|
8380
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
8381
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
8382
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
8383
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
8384
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
8385
|
-
*/
|
|
8386
|
-
function validateExpressionSource(src) {
|
|
8387
|
-
const names = Object.keys(src.bindings);
|
|
8388
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
8389
|
-
for (const name of names) {
|
|
8390
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
8391
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
8392
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
8393
|
-
}
|
|
8394
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
8395
|
-
if (!compiled.ok) return compiled.error;
|
|
8396
|
-
const bound = new Set(names);
|
|
8397
|
-
for (const id of compiled.parsed.identifiers) {
|
|
8398
|
-
if (id === "now") continue;
|
|
8399
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
8400
|
-
}
|
|
8401
|
-
return null;
|
|
8402
|
-
}
|
|
8403
|
-
/**
|
|
8404
|
-
* Shared read-path evaluation for BOTH resolver channels. Builds a null-proto
|
|
8405
|
-
* scope from `bindingValues` plus the injected `now` (supplied by the caller
|
|
8406
|
-
* for determinism/testability), compiles via the LRU, and evaluates. Any
|
|
8407
|
-
* failure (parse or eval) returns `{ ok: false }` — the caller treats that as
|
|
8408
|
-
* "skip this link".
|
|
8409
|
-
*/
|
|
8410
|
-
function evaluateLinkExpression(expr, bindingValues, now, opts) {
|
|
8411
|
-
const compiled = compileExpressionSafe(expr);
|
|
8412
|
-
if (!compiled.ok) return {
|
|
8413
|
-
ok: false,
|
|
8414
|
-
error: compiled.error
|
|
8415
|
-
};
|
|
8416
|
-
const scope = createExpressionScope({
|
|
8417
|
-
...bindingValues,
|
|
8418
|
-
["now"]: now
|
|
8419
|
-
});
|
|
8420
|
-
try {
|
|
8421
|
-
return {
|
|
8422
|
-
ok: true,
|
|
8423
|
-
value: evaluateAst(compiled.parsed.ast, scope, opts)
|
|
8424
|
-
};
|
|
8425
|
-
} catch (err) {
|
|
8426
|
-
return {
|
|
8427
|
-
ok: false,
|
|
8428
|
-
error: err instanceof ExpressionEvalError ? err.message : String(err)
|
|
8429
|
-
};
|
|
8430
|
-
}
|
|
8431
|
-
}
|
|
8432
|
-
var ProviderStatusSchema = zod.z.object({
|
|
8433
|
-
connected: zod.z.boolean(),
|
|
8434
|
-
deviceCount: zod.z.number(),
|
|
8435
|
-
error: zod.z.string().optional()
|
|
8436
|
-
});
|
|
8437
|
-
zod.z.object({
|
|
8438
|
-
externalId: zod.z.string(),
|
|
8439
|
-
name: zod.z.string(),
|
|
8440
|
-
type: zod.z.string(),
|
|
8441
|
-
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
8442
|
-
});
|
|
8443
|
-
/**
|
|
8444
|
-
* Candidate handed back from discovery and accepted by
|
|
8445
|
-
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
8446
|
-
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
8447
|
-
*/
|
|
8448
|
-
var DiscoveryCandidateSchema = zod.z.object({
|
|
8449
|
-
stableId: zod.z.string(),
|
|
8450
|
-
type: zod.z.enum(DeviceType),
|
|
8451
|
-
suggestedName: zod.z.string(),
|
|
8452
|
-
prefilledConfig: zod.z.record(zod.z.string(), zod.z.unknown()),
|
|
8453
|
-
/**
|
|
8454
|
-
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
8455
|
-
* Discovery pre-populates this for systems that know the upstream
|
|
8456
|
-
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
8457
|
-
* flows live through the cap STATUS SLICE after adoption.
|
|
8458
|
-
*/
|
|
8459
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
8460
|
-
});
|
|
8461
|
-
/**
|
|
8462
|
-
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
8463
|
-
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
8464
|
-
* tRPC layer can pass it through without reshaping.
|
|
8465
|
-
*/
|
|
8466
|
-
var DeviceSummarySchema = zod.z.object({
|
|
8467
|
-
id: zod.z.number(),
|
|
8468
|
-
stableId: zod.z.string(),
|
|
8469
|
-
addonId: zod.z.string(),
|
|
8470
|
-
type: zod.z.string(),
|
|
8471
|
-
name: zod.z.string(),
|
|
8472
|
-
parentDeviceId: zod.z.number().nullable(),
|
|
8473
|
-
online: zod.z.boolean(),
|
|
8474
|
-
features: zod.z.array(zod.z.string()),
|
|
8475
|
-
config: zod.z.record(zod.z.string(), zod.z.unknown()),
|
|
8476
|
-
/** Optional upstream-system identity (dispatch key + system tag).
|
|
8477
|
-
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
8478
|
-
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
8479
|
-
* synthetic backfill is in effect. */
|
|
8480
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
8481
|
-
});
|
|
8482
|
-
/**
|
|
8483
|
-
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
8484
|
-
* creation). Matches the UI-side `FieldProbeResult` in
|
|
8485
|
-
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
8486
|
-
* returned `labels` as chips next to the input.
|
|
8487
|
-
*/
|
|
8488
|
-
var FieldProbeResultSchema = zod.z.object({
|
|
8489
|
-
status: zod.z.enum(["ok", "error"]),
|
|
8490
|
-
labels: zod.z.array(zod.z.string()).optional(),
|
|
8491
|
-
error: zod.z.string().optional()
|
|
8492
|
-
});
|
|
8493
|
-
/**
|
|
8494
|
-
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
8495
|
-
* it as `unknown` at the capability layer — the router just passes it
|
|
8496
|
-
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
8497
|
-
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
8498
|
-
* but we deliberately avoid a Zod mirror because the union is large and
|
|
8499
|
-
* not meant for runtime validation at this seam.
|
|
8500
|
-
*/
|
|
8501
|
-
var CreationSchemaOutputSchema = zod.z.unknown();
|
|
8502
|
-
var deviceProviderCapability = {
|
|
8503
|
-
name: "device-provider",
|
|
8504
|
-
scope: "system",
|
|
8505
|
-
mode: "collection",
|
|
8506
|
-
methods: {
|
|
8507
|
-
start: method(zod.z.void(), zod.z.void(), { kind: "mutation" }),
|
|
8508
|
-
stop: method(zod.z.void(), zod.z.void(), { kind: "mutation" }),
|
|
8509
|
-
getStatus: method(zod.z.void(), ProviderStatusSchema),
|
|
8510
|
-
getDevices: method(zod.z.void(), zod.z.array(zod.z.object({
|
|
8511
|
-
id: zod.z.string(),
|
|
8512
|
-
name: zod.z.string(),
|
|
8513
|
-
type: zod.z.string()
|
|
8514
|
-
}))),
|
|
8515
|
-
supportsDiscovery: method(zod.z.object({}), zod.z.boolean()),
|
|
8516
|
-
/**
|
|
8517
|
-
* Run a network scan. `params` carries optional provider-specific scan
|
|
8518
|
-
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
8519
|
-
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
8520
|
-
* (provider uses its local-network default).
|
|
8521
|
-
*/
|
|
8522
|
-
discoverDevices: method(zod.z.object({ params: zod.z.record(zod.z.string(), zod.z.unknown()).optional() }), zod.z.array(DiscoveryCandidateSchema), {
|
|
8523
|
-
kind: "mutation",
|
|
8524
|
-
auth: "admin"
|
|
8525
|
-
}),
|
|
8526
|
-
/**
|
|
8527
|
-
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
8528
|
-
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
8529
|
-
* `null` when the provider takes no extra scan params — the generic
|
|
8530
|
-
* aggregated scan never renders this; the per-integration scan does.
|
|
8531
|
-
*/
|
|
8532
|
-
getDiscoveryParamsSchema: method(zod.z.object({}), CreationSchemaOutputSchema),
|
|
8533
|
-
/**
|
|
8534
|
-
* The DeviceType this provider creates via manual add (Camera for
|
|
8535
|
-
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
8536
|
-
* provider does not support manual creation. Lets the Add-Device dialog
|
|
8537
|
-
* pick the right type instead of assuming Camera.
|
|
8538
|
-
*/
|
|
8539
|
-
getManualCreationType: method(zod.z.object({}), zod.z.object({ deviceType: zod.z.enum(DeviceType).nullable() })),
|
|
8540
|
-
adoptDiscoveredDevice: method(zod.z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
8541
|
-
kind: "mutation",
|
|
8542
|
-
auth: "admin"
|
|
8543
|
-
}),
|
|
8544
|
-
supportsManualCreation: method(zod.z.object({}), zod.z.boolean()),
|
|
8545
|
-
/**
|
|
8546
|
-
* Fetch the creation form schema for a given DeviceType. Returns
|
|
8547
|
-
* `null` when the provider does not support manually creating
|
|
8548
|
-
* devices of that type. The output is a `ConfigUISchema` — the
|
|
8549
|
-
* router type-asserts it at the boundary.
|
|
8550
|
-
*/
|
|
8551
|
-
getChildCreationSchema: method(zod.z.object({ type: zod.z.enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
8552
|
-
createDevice: method(zod.z.object({
|
|
8553
|
-
type: zod.z.enum(DeviceType),
|
|
8554
|
-
config: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
8555
|
-
}), DeviceSummarySchema, {
|
|
8556
|
-
kind: "mutation",
|
|
8557
|
-
auth: "admin"
|
|
8558
|
-
}),
|
|
8559
|
-
/**
|
|
8560
|
-
* Test a single field in the creation form before the device has
|
|
8561
|
-
* been persisted. Typical use: probing an RTSP URL entered by the
|
|
8562
|
-
* user. Providers that don't support field probing return
|
|
8563
|
-
* `{ success: true, message: 'Field test not supported' }`.
|
|
8564
|
-
*
|
|
8565
|
-
* `formValues` is the live snapshot of every field in the form at
|
|
8566
|
-
* the moment the user clicked Test — useful for probes that depend
|
|
8567
|
-
* on multiple fields together (e.g. Reolink autodetect needs host
|
|
8568
|
-
* + credentials + UID + transport mode in a single call). Optional
|
|
8569
|
-
* for backwards compatibility; providers free to ignore it.
|
|
8570
|
-
*/
|
|
8571
|
-
testCreationField: method(zod.z.object({
|
|
8572
|
-
type: zod.z.enum(DeviceType),
|
|
8573
|
-
key: zod.z.string(),
|
|
8574
|
-
value: zod.z.unknown(),
|
|
8575
|
-
formValues: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
8576
|
-
}), FieldProbeResultSchema, {
|
|
8577
|
-
kind: "mutation",
|
|
8578
|
-
auth: "admin"
|
|
8579
|
-
})
|
|
8580
|
-
}
|
|
8581
|
-
};
|
|
8582
|
-
/**
|
|
8583
|
-
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
8584
|
-
* live registry access, and all management operations into a single tRPC surface.
|
|
8585
|
-
*
|
|
8586
|
-
* Replaces:
|
|
8587
|
-
* - `device-persistence` capability (persistence methods absorbed here)
|
|
8588
|
-
* - `device-management.router.ts` (deleted in Phase 2)
|
|
8589
|
-
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
8590
|
-
*
|
|
8591
|
-
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
8592
|
-
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
8593
|
-
* - No nodeId routing needed — this is a pure hub singleton.
|
|
8594
|
-
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
8595
|
-
* - No shadow registry or cross-node aggregation required.
|
|
8596
|
-
*
|
|
8597
|
-
* Forked workers register devices back to the hub via `ctx.devices`
|
|
8598
|
-
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
7916
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
7917
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
7918
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
7919
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
7920
|
+
* - No shadow registry or cross-node aggregation required.
|
|
7921
|
+
*
|
|
7922
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
7923
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
8599
7924
|
*/
|
|
8600
7925
|
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
8601
7926
|
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
@@ -8608,92 +7933,6 @@ var ChildLayoutEntrySchema = zod.z.object({
|
|
|
8608
7933
|
order: zod.z.number().optional(),
|
|
8609
7934
|
collapsed: zod.z.boolean().optional()
|
|
8610
7935
|
});
|
|
8611
|
-
/** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
|
|
8612
|
-
* `device-management.ts`. Source is a union: a FIELD source copies a sibling
|
|
8613
|
-
* accessory's status field (`kind` optional/absent for wire compat); a
|
|
8614
|
-
* LITERAL source carries a per-device constant (no sibling is read); a
|
|
8615
|
-
* GLOBAL source (P2e) copies ANY device's status field, addressed by the
|
|
8616
|
-
* source device's full re-sync-stable `stableId`. */
|
|
8617
|
-
var DeviceLinkFieldSourceSchema = zod.z.object({
|
|
8618
|
-
kind: zod.z.literal("field").optional(),
|
|
8619
|
-
sourceKey: zod.z.string(),
|
|
8620
|
-
cap: zod.z.string(),
|
|
8621
|
-
fieldPath: zod.z.string()
|
|
8622
|
-
});
|
|
8623
|
-
var DeviceLinkLiteralSourceSchema = zod.z.object({
|
|
8624
|
-
kind: zod.z.literal("literal"),
|
|
8625
|
-
value: zod.z.union([
|
|
8626
|
-
zod.z.string(),
|
|
8627
|
-
zod.z.number(),
|
|
8628
|
-
zod.z.boolean(),
|
|
8629
|
-
zod.z.null()
|
|
8630
|
-
])
|
|
8631
|
-
});
|
|
8632
|
-
var DeviceLinkGlobalSourceSchema = zod.z.object({
|
|
8633
|
-
kind: zod.z.literal("global"),
|
|
8634
|
-
sourceStableId: zod.z.string(),
|
|
8635
|
-
cap: zod.z.string(),
|
|
8636
|
-
fieldPath: zod.z.string()
|
|
8637
|
-
});
|
|
8638
|
-
/** Expression source (Stage X): compute the target field from N named bindings
|
|
8639
|
-
* via the safe expression engine. Bindings are field | literal | global — never
|
|
8640
|
-
* another expression (no nesting). The `superRefine` runs the SAME author-time
|
|
8641
|
-
* validation as `validateExpressionSource` (compiles the expr, checks binding
|
|
8642
|
-
* names + identifier coverage) so every wire boundary that parses a DeviceLink
|
|
8643
|
-
* (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
|
|
8644
|
-
* Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
|
|
8645
|
-
var DeviceLinkExpressionSourceSchema = zod.z.object({
|
|
8646
|
-
kind: zod.z.literal("expression"),
|
|
8647
|
-
expr: zod.z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
8648
|
-
bindings: zod.z.record(zod.z.string().regex(EXPRESSION_IDENTIFIER_RE), zod.z.union([
|
|
8649
|
-
DeviceLinkFieldSourceSchema,
|
|
8650
|
-
DeviceLinkLiteralSourceSchema,
|
|
8651
|
-
DeviceLinkGlobalSourceSchema
|
|
8652
|
-
]))
|
|
8653
|
-
}).superRefine((src, ctx) => {
|
|
8654
|
-
const err = validateExpressionSource(src);
|
|
8655
|
-
if (err !== null) ctx.addIssue({
|
|
8656
|
-
code: "custom",
|
|
8657
|
-
message: err,
|
|
8658
|
-
path: ["expr"]
|
|
8659
|
-
});
|
|
8660
|
-
});
|
|
8661
|
-
var DeviceLinkSchema = zod.z.object({
|
|
8662
|
-
id: zod.z.string(),
|
|
8663
|
-
source: zod.z.union([
|
|
8664
|
-
DeviceLinkFieldSourceSchema,
|
|
8665
|
-
DeviceLinkLiteralSourceSchema,
|
|
8666
|
-
DeviceLinkGlobalSourceSchema,
|
|
8667
|
-
DeviceLinkExpressionSourceSchema
|
|
8668
|
-
]),
|
|
8669
|
-
target: zod.z.object({
|
|
8670
|
-
cap: zod.z.string(),
|
|
8671
|
-
fieldPath: zod.z.string(),
|
|
8672
|
-
itemKey: zod.z.string().optional()
|
|
8673
|
-
}),
|
|
8674
|
-
transform: zod.z.discriminatedUnion("kind", [
|
|
8675
|
-
zod.z.object({ kind: zod.z.literal("identity") }),
|
|
8676
|
-
zod.z.object({
|
|
8677
|
-
kind: zod.z.literal("enum-map"),
|
|
8678
|
-
mapping: zod.z.record(zod.z.string(), zod.z.union([
|
|
8679
|
-
zod.z.string(),
|
|
8680
|
-
zod.z.number(),
|
|
8681
|
-
zod.z.boolean()
|
|
8682
|
-
])),
|
|
8683
|
-
fallback: zod.z.union([
|
|
8684
|
-
zod.z.string(),
|
|
8685
|
-
zod.z.number(),
|
|
8686
|
-
zod.z.boolean()
|
|
8687
|
-
]).optional()
|
|
8688
|
-
}),
|
|
8689
|
-
zod.z.object({
|
|
8690
|
-
kind: zod.z.literal("linear"),
|
|
8691
|
-
scale: zod.z.number(),
|
|
8692
|
-
offset: zod.z.number(),
|
|
8693
|
-
clamp: zod.z.tuple([zod.z.number(), zod.z.number()]).readonly().optional()
|
|
8694
|
-
})
|
|
8695
|
-
]).optional()
|
|
8696
|
-
});
|
|
8697
7936
|
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
8698
7937
|
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
8699
7938
|
var DeviceCapDisplayOverrideSchema = zod.z.object({
|
|
@@ -8773,8 +8012,6 @@ var DeviceInfoSchema = zod.z.object({
|
|
|
8773
8012
|
* named accordion sections (with optional intra-section order). See
|
|
8774
8013
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
8775
8014
|
childLayout: zod.z.array(ChildLayoutEntrySchema).readonly().optional(),
|
|
8776
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
8777
|
-
deviceLinks: zod.z.array(DeviceLinkSchema).readonly().optional(),
|
|
8778
8015
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
8779
8016
|
display: DeviceDisplayOverrideSchema.optional()
|
|
8780
8017
|
});
|
|
@@ -8783,7 +8020,7 @@ var ConfigEntrySchema = zod.z.object({
|
|
|
8783
8020
|
value: zod.z.unknown(),
|
|
8784
8021
|
description: zod.z.string().optional()
|
|
8785
8022
|
});
|
|
8786
|
-
var
|
|
8023
|
+
var LinkedDevicesModeSchema = zod.z.enum(["auto", "manual"]);
|
|
8787
8024
|
/** One resolved linked device — the compact projection consumers need. */
|
|
8788
8025
|
var LinkedDeviceSchema = zod.z.object({
|
|
8789
8026
|
deviceId: zod.z.number(),
|
|
@@ -8846,8 +8083,6 @@ var DeviceMetaSchema = zod.z.object({
|
|
|
8846
8083
|
* accordion sections (with optional intra-section order). See
|
|
8847
8084
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
8848
8085
|
childLayout: zod.z.array(ChildLayoutEntrySchema).readonly().optional(),
|
|
8849
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
8850
|
-
deviceLinks: zod.z.array(DeviceLinkSchema).readonly().optional(),
|
|
8851
8086
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
8852
8087
|
* Optional: only present for accessory children that carry a known role. */
|
|
8853
8088
|
role: zod.z.string().nullable().optional(),
|
|
@@ -8999,16 +8234,6 @@ var deviceManagerCapability = {
|
|
|
8999
8234
|
kind: "mutation",
|
|
9000
8235
|
auth: "admin"
|
|
9001
8236
|
}),
|
|
9002
|
-
/** Set (or replace) the cross-device field wirings on a device's meta row.
|
|
9003
|
-
* Mirrors `setChildLayout`; persisted, projected, preserved across
|
|
9004
|
-
* re-register/restore. Idempotent. */
|
|
9005
|
-
setDeviceLinks: method(zod.z.object({
|
|
9006
|
-
deviceId: zod.z.number(),
|
|
9007
|
-
deviceLinks: zod.z.array(DeviceLinkSchema).readonly()
|
|
9008
|
-
}), zod.z.void(), {
|
|
9009
|
-
kind: "mutation",
|
|
9010
|
-
auth: "admin"
|
|
9011
|
-
}),
|
|
9012
8237
|
/** Set (or clear) the per-device display override on the meta row. Mirrors
|
|
9013
8238
|
* `setChildLayout` persistence; `null` clears the override entirely. The
|
|
9014
8239
|
* override unit(s) are normalized (`normalizeUnit`) at write so the render
|
|
@@ -9032,15 +8257,15 @@ var deviceManagerCapability = {
|
|
|
9032
8257
|
kind: "mutation",
|
|
9033
8258
|
auth: "admin"
|
|
9034
8259
|
}),
|
|
9035
|
-
/**
|
|
9036
|
-
*
|
|
9037
|
-
*
|
|
9038
|
-
*
|
|
9039
|
-
*
|
|
9040
|
-
*
|
|
9041
|
-
*
|
|
9042
|
-
*
|
|
9043
|
-
*
|
|
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. */
|
|
9044
8269
|
getWireableFields: method(zod.z.object({
|
|
9045
8270
|
deviceId: zod.z.number(),
|
|
9046
8271
|
includeSynthesizable: zod.z.boolean().optional()
|
|
@@ -9182,7 +8407,7 @@ var deviceManagerCapability = {
|
|
|
9182
8407
|
* (see the module docblock) — there are no bespoke link mutations.
|
|
9183
8408
|
*/
|
|
9184
8409
|
getLinkedDevices: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.object({
|
|
9185
|
-
mode:
|
|
8410
|
+
mode: LinkedDevicesModeSchema,
|
|
9186
8411
|
devices: zod.z.array(LinkedDeviceSchema)
|
|
9187
8412
|
})),
|
|
9188
8413
|
/** Get stream sources for a camera device. */
|
|
@@ -9246,11 +8471,7 @@ var deviceManagerCapability = {
|
|
|
9246
8471
|
deviceId: zod.z.number(),
|
|
9247
8472
|
entries: zod.z.array(zod.z.object({
|
|
9248
8473
|
capName: zod.z.string(),
|
|
9249
|
-
kind: zod.z.enum([
|
|
9250
|
-
"native",
|
|
9251
|
-
"wrapped",
|
|
9252
|
-
"linked"
|
|
9253
|
-
]),
|
|
8474
|
+
kind: zod.z.enum(["native", "wrapped"]),
|
|
9254
8475
|
providerAddonId: zod.z.string(),
|
|
9255
8476
|
providerNodeId: zod.z.string(),
|
|
9256
8477
|
nativeAddonId: zod.z.string()
|
|
@@ -9268,11 +8489,7 @@ var deviceManagerCapability = {
|
|
|
9268
8489
|
deviceId: zod.z.number(),
|
|
9269
8490
|
entries: zod.z.array(zod.z.object({
|
|
9270
8491
|
capName: zod.z.string(),
|
|
9271
|
-
kind: zod.z.enum([
|
|
9272
|
-
"native",
|
|
9273
|
-
"wrapped",
|
|
9274
|
-
"linked"
|
|
9275
|
-
]),
|
|
8492
|
+
kind: zod.z.enum(["native", "wrapped"]),
|
|
9276
8493
|
providerAddonId: zod.z.string(),
|
|
9277
8494
|
providerNodeId: zod.z.string(),
|
|
9278
8495
|
nativeAddonId: zod.z.string()
|
|
@@ -10480,6 +9697,10 @@ var motionDetectionCapability = {
|
|
|
10480
9697
|
mode: "singleton",
|
|
10481
9698
|
kind: "wrapper",
|
|
10482
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],
|
|
10483
9704
|
exposesDeviceSettings: true,
|
|
10484
9705
|
methods: {
|
|
10485
9706
|
analyze: method(zod.z.object({
|
|
@@ -12873,6 +12094,18 @@ var OauthIntegrationDescriptorSchema = zod.z.object({
|
|
|
12873
12094
|
* redirect_uri that does not start with one of these. Required —
|
|
12874
12095
|
* an empty list means the integration can never complete linking. */
|
|
12875
12096
|
allowedRedirectPrefixes: zod.z.array(zod.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: zod.z.array(zod.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: zod.z.boolean().optional(),
|
|
12876
12109
|
/** Optional public origin (no trailing slash) that this integration's
|
|
12877
12110
|
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
12878
12111
|
* operator-selected external-access endpoint resolved by the addon. When
|
|
@@ -13029,7 +12262,7 @@ var TrackEnvelopeSchema = zod.z.object({
|
|
|
13029
12262
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
13030
12263
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
13031
12264
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
13032
|
-
* zonesVisited, bestEventId, envelope) and returns `positions` /
|
|
12265
|
+
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
13033
12266
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
13034
12267
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
13035
12268
|
* (`getObjectEvents` et al.).
|
|
@@ -13266,6 +12499,24 @@ var TrackSchema = zod.z.object({
|
|
|
13266
12499
|
* Populated from the persisted envelope columns on historical reads;
|
|
13267
12500
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
13268
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: zod.z.boolean().optional(),
|
|
13269
12520
|
...TrackFlagFields,
|
|
13270
12521
|
...TrackRetrainFields
|
|
13271
12522
|
});
|
|
@@ -17015,6 +16266,10 @@ var SsoBridgeClaimsSchema = zod.z.object({
|
|
|
17015
16266
|
integrationId: zod.z.string().optional(),
|
|
17016
16267
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
17017
16268
|
jti: zod.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: zod.z.string().optional(),
|
|
17018
16273
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
17019
16274
|
* tokens so the verify path can check the session is not revoked. */
|
|
17020
16275
|
sessionId: zod.z.string().optional()
|
|
@@ -17771,6 +17026,10 @@ var videoclipsCapability = {
|
|
|
17771
17026
|
mode: "singleton",
|
|
17772
17027
|
kind: "wrapper",
|
|
17773
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],
|
|
17774
17033
|
methods: {
|
|
17775
17034
|
listClips: method(zod.z.object({
|
|
17776
17035
|
deviceId: zod.z.number(),
|
|
@@ -20266,7 +19525,29 @@ var FaceInfoSchema = zod.z.object({
|
|
|
20266
19525
|
recognizedIdentityId: zod.z.string().optional(),
|
|
20267
19526
|
identityName: zod.z.string().optional(),
|
|
20268
19527
|
assigned: zod.z.boolean(),
|
|
19528
|
+
/**
|
|
19529
|
+
* The crop, inline, base64.
|
|
19530
|
+
*
|
|
19531
|
+
* **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
|
|
19532
|
+
* field alone is ~2.87 MiB, re-sent in full on every operator assign and
|
|
19533
|
+
* every 30 s poll, base64-inflated over the msgpack socket and held in the
|
|
19534
|
+
* query heap. It stays for callers that have not migrated; `includeCrops:
|
|
19535
|
+
* false` turns it off once they have.
|
|
19536
|
+
*/
|
|
20269
19537
|
base64: zod.z.string().optional(),
|
|
19538
|
+
/**
|
|
19539
|
+
* Same crop, as a data-plane URL for `<img src>` — the move the admin
|
|
19540
|
+
* snapshot surfaces made on 2026-08-08.
|
|
19541
|
+
*
|
|
19542
|
+
* Served by the `event-media` plane, which resolves a raw MediaStore key and
|
|
19543
|
+
* is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
|
|
19544
|
+
* cookie, so no header plumbing is needed. The bytes then ride the browser's
|
|
19545
|
+
* HTTP cache with an ETag and `immutable`, instead of the WebSocket.
|
|
19546
|
+
*
|
|
19547
|
+
* Absent when the face has no stored crop, or when the addon has no data
|
|
19548
|
+
* plane — callers fall back to {@link base64}.
|
|
19549
|
+
*/
|
|
19550
|
+
cropUrl: zod.z.string().optional(),
|
|
20270
19551
|
/** Design B: the face bbox (pixel space) on the key frame — lets a detail
|
|
20271
19552
|
* view draw the box over the native `keyFrameMediaKey` frame. Absent on
|
|
20272
19553
|
* legacy rows written before design B. */
|
|
@@ -20342,7 +19623,23 @@ var faceGalleryCapability = {
|
|
|
20342
19623
|
}),
|
|
20343
19624
|
listRecentFaces: method(zod.z.object({
|
|
20344
19625
|
limit: zod.z.number().int().positive().optional(),
|
|
20345
|
-
filter: FaceFilterEnum.optional()
|
|
19626
|
+
filter: FaceFilterEnum.optional(),
|
|
19627
|
+
/**
|
|
19628
|
+
* Inline the base64 crop on every row. Default `true` — the existing
|
|
19629
|
+
* behaviour, kept so no caller breaks.
|
|
19630
|
+
*
|
|
19631
|
+
* Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
|
|
19632
|
+
* drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
|
|
19633
|
+
* the browser cache the images.
|
|
19634
|
+
*
|
|
19635
|
+
* **This is an INPUT field, so it does not reach the addon until the
|
|
19636
|
+
* next train.** The hub router validates cap inputs against its own
|
|
19637
|
+
* compiled Zod, which strips a key it does not know — verified today
|
|
19638
|
+
* on the OUTPUT side, where an additive field DOES arrive immediately
|
|
19639
|
+
* (`Track.hasFace`). Until the train ships, sending `false` is
|
|
19640
|
+
* harmless and simply keeps the crops inline.
|
|
19641
|
+
*/
|
|
19642
|
+
includeCrops: zod.z.boolean().optional()
|
|
20346
19643
|
}).optional(), zod.z.array(FaceInfoSchema).readonly()),
|
|
20347
19644
|
getFaceByTrack: method(zod.z.object({
|
|
20348
19645
|
deviceId: zod.z.number().int(),
|
|
@@ -25697,14 +24994,19 @@ var userManagementCapability = {
|
|
|
25697
24994
|
username: zod.z.string(),
|
|
25698
24995
|
scopes: zod.z.array(TokenScopeSchema),
|
|
25699
24996
|
redirectUri: zod.z.string(),
|
|
25700
|
-
hubUrl: zod.z.string()
|
|
24997
|
+
hubUrl: zod.z.string(),
|
|
24998
|
+
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
24999
|
+
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
25000
|
+
codeChallenge: zod.z.string().optional()
|
|
25701
25001
|
}), zod.z.object({ code: zod.z.string() }), {
|
|
25702
25002
|
kind: "mutation",
|
|
25703
25003
|
access: "create"
|
|
25704
25004
|
}),
|
|
25705
25005
|
oauthExchangeCode: method(zod.z.object({
|
|
25706
25006
|
code: zod.z.string(),
|
|
25707
|
-
redirectUri: zod.z.string()
|
|
25007
|
+
redirectUri: zod.z.string(),
|
|
25008
|
+
/** PKCE verifier. REQUIRED when the code carries a challenge. */
|
|
25009
|
+
codeVerifier: zod.z.string().optional()
|
|
25708
25010
|
}), zod.z.object({
|
|
25709
25011
|
accessToken: zod.z.string(),
|
|
25710
25012
|
refreshToken: zod.z.string(),
|
|
@@ -26246,277 +25548,872 @@ var zoneAnalyticsCapability = {
|
|
|
26246
25548
|
runtimeState: CameraOccupancySnapshotSchema
|
|
26247
25549
|
};
|
|
26248
25550
|
/**
|
|
26249
|
-
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
26250
|
-
* cap so a single CRUD surface backs every consumer; each stage has
|
|
26251
|
-
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
26252
|
-
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
26253
|
-
*
|
|
26254
|
-
* Extend the enum here when a new gating consumer comes online (audio
|
|
26255
|
-
* gating, alert filtering, …) — no other surface needs to change.
|
|
25551
|
+
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
25552
|
+
* cap so a single CRUD surface backs every consumer; each stage has
|
|
25553
|
+
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
25554
|
+
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
25555
|
+
*
|
|
25556
|
+
* Extend the enum here when a new gating consumer comes online (audio
|
|
25557
|
+
* gating, alert filtering, …) — no other surface needs to change.
|
|
25558
|
+
*/
|
|
25559
|
+
var ZoneRuleStageEnum = zod.z.enum([
|
|
25560
|
+
"motion",
|
|
25561
|
+
"detection",
|
|
25562
|
+
"package"
|
|
25563
|
+
]);
|
|
25564
|
+
/**
|
|
25565
|
+
* Zone rules capability — per-camera CRUD over the {@link ZoneRule}
|
|
25566
|
+
* arrays that decide how each pipeline stage uses the polygon zones.
|
|
25567
|
+
*
|
|
25568
|
+
* Hosted by `addon-pipeline-orchestrator` alongside the zones provider
|
|
25569
|
+
* so the operator has a single hub-side source of truth for both
|
|
25570
|
+
* geometry and behaviour. Per-stage rules are stored under the
|
|
25571
|
+
* `zoneRules.<stage>` key in the orchestrator's per-device store and
|
|
25572
|
+
* mirrored to the device-state slice `<stage>-zone-rules` on every
|
|
25573
|
+
* mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
|
|
25574
|
+
* subscribe to that slice and refresh their gating without
|
|
25575
|
+
* round-tripping the cap.
|
|
25576
|
+
*
|
|
25577
|
+
* Sets are bulk-replace — the operator UI sends the new rule list
|
|
25578
|
+
* wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
|
|
25579
|
+
* naturally in the rule editor without per-rule mutation chatter.
|
|
25580
|
+
*/
|
|
25581
|
+
var zoneRulesCapability = {
|
|
25582
|
+
name: "zone-rules",
|
|
25583
|
+
scope: "device",
|
|
25584
|
+
mode: "singleton",
|
|
25585
|
+
deviceTypes: [DeviceType.Camera],
|
|
25586
|
+
methods: {
|
|
25587
|
+
/** Read the full rule list for a given stage (empty when no rules
|
|
25588
|
+
* are defined yet). */
|
|
25589
|
+
listRules: method(zod.z.object({
|
|
25590
|
+
deviceId: zod.z.number(),
|
|
25591
|
+
stage: ZoneRuleStageEnum
|
|
25592
|
+
}), zod.z.array(ZoneRuleSchema).readonly()),
|
|
25593
|
+
/** Bulk-replace the rule list for one stage. The provider validates
|
|
25594
|
+
* each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
|
|
25595
|
+
* thresholds in range) and rejects the whole patch if any entry
|
|
25596
|
+
* is invalid — partial writes are a configuration footgun. */
|
|
25597
|
+
setRules: method(zod.z.object({
|
|
25598
|
+
deviceId: zod.z.number(),
|
|
25599
|
+
stage: ZoneRuleStageEnum,
|
|
25600
|
+
rules: zod.z.array(ZoneRuleSchema).readonly()
|
|
25601
|
+
}), zod.z.void(), {
|
|
25602
|
+
kind: "mutation",
|
|
25603
|
+
auth: "admin"
|
|
25604
|
+
})
|
|
25605
|
+
},
|
|
25606
|
+
/**
|
|
25607
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
25608
|
+
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
25609
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
25610
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
25611
|
+
* complete current set. Consumers that only care about one stage
|
|
25612
|
+
* just read the matching property.
|
|
25613
|
+
*
|
|
25614
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
25615
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
25616
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
25617
|
+
* The orchestrator provider writes this stage as a first-class slice
|
|
25618
|
+
* (Phase 4): every mutation mirrors the full `{motion, detection,
|
|
25619
|
+
* package}` shape, so consumers read the current package rules directly
|
|
25620
|
+
* off `device.state.zoneRules.value.package`.
|
|
25621
|
+
*/
|
|
25622
|
+
runtimeState: zod.z.object({
|
|
25623
|
+
motion: zod.z.array(ZoneRuleSchema).readonly(),
|
|
25624
|
+
detection: zod.z.array(ZoneRuleSchema).readonly(),
|
|
25625
|
+
package: zod.z.array(ZoneRuleSchema).readonly()
|
|
25626
|
+
})
|
|
25627
|
+
};
|
|
25628
|
+
/**
|
|
25629
|
+
* Runtime defaults -- used by ConfigManager.get() for backward compatibility
|
|
25630
|
+
* until Plan B wires all runtime settings to the system_settings SQL table.
|
|
25631
|
+
*
|
|
25632
|
+
* Moved from @camstack/system/config-schema to @camstack/types so that
|
|
25633
|
+
* @camstack/system can reference it without a cross-package kernel import.
|
|
25634
|
+
*/
|
|
25635
|
+
var RUNTIME_DEFAULTS = {
|
|
25636
|
+
"features.streaming": true,
|
|
25637
|
+
"features.notifications": true,
|
|
25638
|
+
"features.objectDetection": false,
|
|
25639
|
+
"features.remoteAccess": true,
|
|
25640
|
+
"features.agentCluster": false,
|
|
25641
|
+
"features.smartHome": true,
|
|
25642
|
+
"features.recordings": true,
|
|
25643
|
+
"features.backup": true,
|
|
25644
|
+
"features.repl": true,
|
|
25645
|
+
"retention.detectionEventsDays": 30,
|
|
25646
|
+
"retention.audioLevelsDays": 7,
|
|
25647
|
+
"logging.level": "info",
|
|
25648
|
+
"logging.retentionDays": 30,
|
|
25649
|
+
"eventBus.ringBufferSize": 1e4,
|
|
25650
|
+
"storage.provider": "sqlite-storage",
|
|
25651
|
+
"storage.locations": {
|
|
25652
|
+
data: "camstack-data/data",
|
|
25653
|
+
media: "camstack-data/media",
|
|
25654
|
+
recordings: "camstack-data/recordings",
|
|
25655
|
+
cache: "/tmp/camstack-cache",
|
|
25656
|
+
logs: "camstack-data/logs",
|
|
25657
|
+
models: "camstack-data/models"
|
|
25658
|
+
},
|
|
25659
|
+
providers: [],
|
|
25660
|
+
"ffmpeg.binaryPath": "ffmpeg",
|
|
25661
|
+
"ffmpeg.hwAccel": "auto",
|
|
25662
|
+
"ffmpeg.threadCount": 0,
|
|
25663
|
+
"auth.tokenExpiry": "7d"
|
|
25664
|
+
};
|
|
25665
|
+
/**
|
|
25666
|
+
* Accessory device helpers — shared across drivers.
|
|
25667
|
+
*
|
|
25668
|
+
* Many vendor-specific drivers register accessory child devices on
|
|
25669
|
+
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
25670
|
+
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
25671
|
+
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
25672
|
+
* spawning, builds a name derived from the parent, and produces a
|
|
25673
|
+
* stableId tied to the parent so boot-restore can reconstruct the
|
|
25674
|
+
* relationship.
|
|
25675
|
+
*
|
|
25676
|
+
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
25677
|
+
* drivers may reasonably disagree on the right type for an accessory
|
|
25678
|
+
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
25679
|
+
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
25680
|
+
* one canonical mapping was over-prescriptive and added a layer of
|
|
25681
|
+
* indirection without saving meaningful code at call sites — the
|
|
25682
|
+
* driver knows its own hardware best.
|
|
25683
|
+
*/
|
|
25684
|
+
/**
|
|
25685
|
+
* Subset of `DeviceRole` values that drivers register as child
|
|
25686
|
+
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
25687
|
+
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
25688
|
+
* children, so the call site reads as
|
|
25689
|
+
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
25690
|
+
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
25691
|
+
* any role works, including non-accessory ones like Doorbell).
|
|
25692
|
+
*/
|
|
25693
|
+
var AccessoryKind = {
|
|
25694
|
+
Siren: DeviceRole.Siren,
|
|
25695
|
+
Floodlight: DeviceRole.Floodlight,
|
|
25696
|
+
Spotlight: DeviceRole.Spotlight,
|
|
25697
|
+
PirSensor: DeviceRole.PirSensor,
|
|
25698
|
+
Chime: DeviceRole.Chime,
|
|
25699
|
+
Autotrack: DeviceRole.Autotrack,
|
|
25700
|
+
Nightvision: DeviceRole.Nightvision,
|
|
25701
|
+
PrivacyMask: DeviceRole.PrivacyMask
|
|
25702
|
+
};
|
|
25703
|
+
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
25704
|
+
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;
|
|
25705
|
+
new Set(Object.values(DeviceType));
|
|
25706
|
+
DeviceFeature.BatteryOperated;
|
|
25707
|
+
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
|
|
25708
|
+
* Zod v4 exposes `.unwrap()` on all three wrapper classes. */
|
|
25709
|
+
function unwrap(schema) {
|
|
25710
|
+
let s = schema;
|
|
25711
|
+
while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
|
|
25712
|
+
return s;
|
|
25713
|
+
}
|
|
25714
|
+
function leafKind(schema) {
|
|
25715
|
+
const s = unwrap(schema);
|
|
25716
|
+
if (s instanceof zod.z.ZodEnum) return {
|
|
25717
|
+
kind: "enum",
|
|
25718
|
+
enumValues: s.options
|
|
25719
|
+
};
|
|
25720
|
+
if (s instanceof zod.z.ZodNumber) return { kind: "number" };
|
|
25721
|
+
if (s instanceof zod.z.ZodString) return { kind: "string" };
|
|
25722
|
+
if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
|
|
25723
|
+
return null;
|
|
25724
|
+
}
|
|
25725
|
+
/** Walk a cap status schema into flat wireable leaf fields (dotted paths).
|
|
25726
|
+
* Recurses into nested ZodObject (unwrapping nullable/optional/default first),
|
|
25727
|
+
* so fields with null live values are still offered. Arrays / unknown shapes
|
|
25728
|
+
* are skipped — never throws. */
|
|
25729
|
+
function enumerateSchemaFields(schema, prefix = "") {
|
|
25730
|
+
const root = unwrap(schema);
|
|
25731
|
+
if (!(root instanceof zod.z.ZodObject)) return [];
|
|
25732
|
+
const out = [];
|
|
25733
|
+
const shape = root.shape;
|
|
25734
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
25735
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
25736
|
+
const inner = unwrap(field);
|
|
25737
|
+
if (inner instanceof zod.z.ZodObject) {
|
|
25738
|
+
out.push(...enumerateSchemaFields(inner, path));
|
|
25739
|
+
continue;
|
|
25740
|
+
}
|
|
25741
|
+
const leaf = leafKind(field);
|
|
25742
|
+
if (leaf) out.push({
|
|
25743
|
+
path,
|
|
25744
|
+
kind: leaf.kind,
|
|
25745
|
+
...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
|
|
25746
|
+
});
|
|
25747
|
+
}
|
|
25748
|
+
return out;
|
|
25749
|
+
}
|
|
25750
|
+
/** Enumerate the per-item wireable fields of an item-array cap (see
|
|
25751
|
+
* `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
|
|
25752
|
+
* `keyField` (the key comes from the link's `itemKey`, never from a wired
|
|
25753
|
+
* source), each tagged `item: true` so the authoring UI collects an
|
|
25754
|
+
* `itemKey` alongside the field. Never throws. */
|
|
25755
|
+
function enumerateItemArrayFields(itemArray) {
|
|
25756
|
+
return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
|
|
25757
|
+
...f,
|
|
25758
|
+
item: true
|
|
25759
|
+
}));
|
|
25760
|
+
}
|
|
25761
|
+
/**
|
|
25762
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
25763
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
25764
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
26256
25765
|
*/
|
|
26257
|
-
|
|
26258
|
-
|
|
26259
|
-
|
|
26260
|
-
|
|
26261
|
-
|
|
25766
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
25767
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
25768
|
+
var ExpressionParseError = class extends Error {
|
|
25769
|
+
position;
|
|
25770
|
+
constructor(message, position) {
|
|
25771
|
+
super(message);
|
|
25772
|
+
this.name = "ExpressionParseError";
|
|
25773
|
+
this.position = position;
|
|
25774
|
+
}
|
|
25775
|
+
};
|
|
25776
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
25777
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
25778
|
+
var ExpressionEvalError = class extends Error {
|
|
25779
|
+
constructor(message) {
|
|
25780
|
+
super(message);
|
|
25781
|
+
this.name = "ExpressionEvalError";
|
|
25782
|
+
}
|
|
25783
|
+
};
|
|
26262
25784
|
/**
|
|
26263
|
-
*
|
|
26264
|
-
*
|
|
25785
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
25786
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
25787
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
25788
|
+
* own-property check against it.
|
|
26265
25789
|
*
|
|
26266
|
-
*
|
|
26267
|
-
*
|
|
26268
|
-
*
|
|
26269
|
-
* `
|
|
26270
|
-
*
|
|
26271
|
-
* mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
|
|
26272
|
-
* subscribe to that slice and refresh their gating without
|
|
26273
|
-
* round-tripping the cap.
|
|
25790
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
25791
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
25792
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
25793
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
25794
|
+
* callable — they are simply "unknown function" at parse time.
|
|
26274
25795
|
*
|
|
26275
|
-
*
|
|
26276
|
-
*
|
|
26277
|
-
*
|
|
25796
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
25797
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
25798
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
25799
|
+
* closed rather than emitting a garbage value.
|
|
26278
25800
|
*/
|
|
26279
|
-
|
|
26280
|
-
name:
|
|
26281
|
-
|
|
26282
|
-
|
|
26283
|
-
|
|
26284
|
-
|
|
26285
|
-
|
|
26286
|
-
|
|
26287
|
-
|
|
26288
|
-
|
|
26289
|
-
|
|
26290
|
-
|
|
26291
|
-
|
|
26292
|
-
|
|
26293
|
-
|
|
26294
|
-
|
|
26295
|
-
|
|
26296
|
-
|
|
26297
|
-
|
|
26298
|
-
|
|
26299
|
-
|
|
26300
|
-
kind: "mutation",
|
|
26301
|
-
auth: "admin"
|
|
26302
|
-
})
|
|
25801
|
+
function asFiniteNumber(value, name, index) {
|
|
25802
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
25803
|
+
return value;
|
|
25804
|
+
}
|
|
25805
|
+
function asString$1(value, name, index) {
|
|
25806
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
25807
|
+
return value;
|
|
25808
|
+
}
|
|
25809
|
+
function finiteResult(value, name) {
|
|
25810
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
25811
|
+
return value;
|
|
25812
|
+
}
|
|
25813
|
+
function allFiniteNumbers(args, name) {
|
|
25814
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
25815
|
+
}
|
|
25816
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
25817
|
+
var table = {
|
|
25818
|
+
min: {
|
|
25819
|
+
minArgs: 1,
|
|
25820
|
+
maxArgs: INF,
|
|
25821
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
26303
25822
|
},
|
|
26304
|
-
|
|
26305
|
-
|
|
26306
|
-
|
|
26307
|
-
|
|
26308
|
-
|
|
26309
|
-
|
|
26310
|
-
|
|
26311
|
-
|
|
26312
|
-
|
|
26313
|
-
|
|
26314
|
-
|
|
26315
|
-
|
|
26316
|
-
|
|
26317
|
-
|
|
26318
|
-
|
|
26319
|
-
|
|
26320
|
-
|
|
26321
|
-
|
|
26322
|
-
|
|
26323
|
-
|
|
26324
|
-
|
|
25823
|
+
max: {
|
|
25824
|
+
minArgs: 1,
|
|
25825
|
+
maxArgs: INF,
|
|
25826
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
25827
|
+
},
|
|
25828
|
+
abs: {
|
|
25829
|
+
minArgs: 1,
|
|
25830
|
+
maxArgs: 1,
|
|
25831
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
25832
|
+
},
|
|
25833
|
+
floor: {
|
|
25834
|
+
minArgs: 1,
|
|
25835
|
+
maxArgs: 1,
|
|
25836
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
25837
|
+
},
|
|
25838
|
+
ceil: {
|
|
25839
|
+
minArgs: 1,
|
|
25840
|
+
maxArgs: 1,
|
|
25841
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
25842
|
+
},
|
|
25843
|
+
sqrt: {
|
|
25844
|
+
minArgs: 1,
|
|
25845
|
+
maxArgs: 1,
|
|
25846
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
25847
|
+
},
|
|
25848
|
+
round: {
|
|
25849
|
+
minArgs: 1,
|
|
25850
|
+
maxArgs: 2,
|
|
25851
|
+
apply: (args) => {
|
|
25852
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
25853
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
25854
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
25855
|
+
const factor = 10 ** digits;
|
|
25856
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
25857
|
+
}
|
|
25858
|
+
},
|
|
25859
|
+
pow: {
|
|
25860
|
+
minArgs: 2,
|
|
25861
|
+
maxArgs: 2,
|
|
25862
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
25863
|
+
},
|
|
25864
|
+
clamp: {
|
|
25865
|
+
minArgs: 3,
|
|
25866
|
+
maxArgs: 3,
|
|
25867
|
+
apply: (args) => {
|
|
25868
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
25869
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
25870
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
25871
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
25872
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
25873
|
+
}
|
|
25874
|
+
},
|
|
25875
|
+
avg: {
|
|
25876
|
+
minArgs: 1,
|
|
25877
|
+
maxArgs: INF,
|
|
25878
|
+
apply: (args) => {
|
|
25879
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
25880
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
25881
|
+
}
|
|
25882
|
+
},
|
|
25883
|
+
sum: {
|
|
25884
|
+
minArgs: 1,
|
|
25885
|
+
maxArgs: INF,
|
|
25886
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
25887
|
+
},
|
|
25888
|
+
coalesce: {
|
|
25889
|
+
minArgs: 1,
|
|
25890
|
+
maxArgs: INF,
|
|
25891
|
+
apply: (args) => {
|
|
25892
|
+
for (const a of args) if (a !== null) return a;
|
|
25893
|
+
return null;
|
|
25894
|
+
}
|
|
25895
|
+
},
|
|
25896
|
+
age: {
|
|
25897
|
+
minArgs: 2,
|
|
25898
|
+
maxArgs: 2,
|
|
25899
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
25900
|
+
},
|
|
25901
|
+
convert: {
|
|
25902
|
+
minArgs: 3,
|
|
25903
|
+
maxArgs: 3,
|
|
25904
|
+
apply: (args, hooks) => {
|
|
25905
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
25906
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
25907
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
25908
|
+
if (hooks.convert) {
|
|
25909
|
+
const out = hooks.convert(x, from, to);
|
|
25910
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
25911
|
+
return finiteResult(out, "convert");
|
|
25912
|
+
}
|
|
25913
|
+
if (from === to) return x;
|
|
25914
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
25915
|
+
}
|
|
25916
|
+
}
|
|
26325
25917
|
};
|
|
25918
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
25919
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
25920
|
+
* callees at parse time (immediate author feedback). */
|
|
25921
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
26326
25922
|
/**
|
|
26327
|
-
*
|
|
26328
|
-
* until Plan B wires all runtime settings to the system_settings SQL table.
|
|
25923
|
+
* Resource-bound constants for the safe expression engine.
|
|
26329
25924
|
*
|
|
26330
|
-
*
|
|
26331
|
-
*
|
|
25925
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
25926
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
25927
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
25928
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
25929
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
26332
25930
|
*/
|
|
26333
|
-
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
|
|
26341
|
-
"
|
|
26342
|
-
"
|
|
26343
|
-
"
|
|
26344
|
-
"
|
|
26345
|
-
|
|
26346
|
-
"logging.retentionDays": 30,
|
|
26347
|
-
"eventBus.ringBufferSize": 1e4,
|
|
26348
|
-
"storage.provider": "sqlite-storage",
|
|
26349
|
-
"storage.locations": {
|
|
26350
|
-
data: "camstack-data/data",
|
|
26351
|
-
media: "camstack-data/media",
|
|
26352
|
-
recordings: "camstack-data/recordings",
|
|
26353
|
-
cache: "/tmp/camstack-cache",
|
|
26354
|
-
logs: "camstack-data/logs",
|
|
26355
|
-
models: "camstack-data/models"
|
|
26356
|
-
},
|
|
26357
|
-
providers: [],
|
|
26358
|
-
"ffmpeg.binaryPath": "ffmpeg",
|
|
26359
|
-
"ffmpeg.hwAccel": "auto",
|
|
26360
|
-
"ffmpeg.threadCount": 0,
|
|
26361
|
-
"auth.tokenExpiry": "7d"
|
|
26362
|
-
};
|
|
25931
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
25932
|
+
* rejected without allocation. */
|
|
25933
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
25934
|
+
/** A legal binding / identifier name. */
|
|
25935
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25936
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
25937
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
25938
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
25939
|
+
"now",
|
|
25940
|
+
"true",
|
|
25941
|
+
"false",
|
|
25942
|
+
"null"
|
|
25943
|
+
]);
|
|
26363
25944
|
/**
|
|
26364
|
-
*
|
|
26365
|
-
*
|
|
26366
|
-
*
|
|
26367
|
-
*
|
|
26368
|
-
*
|
|
26369
|
-
*
|
|
26370
|
-
*
|
|
26371
|
-
* stableId tied to the parent so boot-restore can reconstruct the
|
|
26372
|
-
* relationship.
|
|
26373
|
-
*
|
|
26374
|
-
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
26375
|
-
* drivers may reasonably disagree on the right type for an accessory
|
|
26376
|
-
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
26377
|
-
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
26378
|
-
* one canonical mapping was over-prescriptive and added a layer of
|
|
26379
|
-
* indirection without saving meaningful code at call sites — the
|
|
26380
|
-
* driver knows its own hardware best.
|
|
25945
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
25946
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
25947
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
25948
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
25949
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
25950
|
+
* is a parse error with a source position, so member access / assignment /
|
|
25951
|
+
* template literals are lexically impossible.
|
|
26381
25952
|
*/
|
|
25953
|
+
var KEYWORDS = new Set([
|
|
25954
|
+
"true",
|
|
25955
|
+
"false",
|
|
25956
|
+
"null"
|
|
25957
|
+
]);
|
|
25958
|
+
function isDigit(ch) {
|
|
25959
|
+
return ch >= "0" && ch <= "9";
|
|
25960
|
+
}
|
|
25961
|
+
function isIdentStart(ch) {
|
|
25962
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
25963
|
+
}
|
|
25964
|
+
function isIdentPart(ch) {
|
|
25965
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
25966
|
+
}
|
|
25967
|
+
function isWhitespace(ch) {
|
|
25968
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
25969
|
+
}
|
|
25970
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
25971
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
25972
|
+
* string. */
|
|
25973
|
+
function tokenize(source) {
|
|
25974
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
25975
|
+
const tokens = [];
|
|
25976
|
+
let i = 0;
|
|
25977
|
+
const n = source.length;
|
|
25978
|
+
while (i < n) {
|
|
25979
|
+
const ch = source[i];
|
|
25980
|
+
if (isWhitespace(ch)) {
|
|
25981
|
+
i += 1;
|
|
25982
|
+
continue;
|
|
25983
|
+
}
|
|
25984
|
+
if (isDigit(ch)) {
|
|
25985
|
+
const start = i;
|
|
25986
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
25987
|
+
if (i < n && source[i] === ".") {
|
|
25988
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
25989
|
+
i += 1;
|
|
25990
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
25991
|
+
}
|
|
25992
|
+
const text = source.slice(start, i);
|
|
25993
|
+
const value = Number(text);
|
|
25994
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
25995
|
+
tokens.push({
|
|
25996
|
+
type: "number",
|
|
25997
|
+
value,
|
|
25998
|
+
pos: start
|
|
25999
|
+
});
|
|
26000
|
+
continue;
|
|
26001
|
+
}
|
|
26002
|
+
if (ch === "'" || ch === "\"") {
|
|
26003
|
+
const quote = ch;
|
|
26004
|
+
const start = i;
|
|
26005
|
+
i += 1;
|
|
26006
|
+
let out = "";
|
|
26007
|
+
let closed = false;
|
|
26008
|
+
while (i < n) {
|
|
26009
|
+
const c = source[i];
|
|
26010
|
+
if (c === "\\") {
|
|
26011
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
26012
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
26013
|
+
out += next;
|
|
26014
|
+
i += 2;
|
|
26015
|
+
continue;
|
|
26016
|
+
}
|
|
26017
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
26018
|
+
}
|
|
26019
|
+
if (c === quote) {
|
|
26020
|
+
closed = true;
|
|
26021
|
+
i += 1;
|
|
26022
|
+
break;
|
|
26023
|
+
}
|
|
26024
|
+
out += c;
|
|
26025
|
+
i += 1;
|
|
26026
|
+
}
|
|
26027
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
26028
|
+
tokens.push({
|
|
26029
|
+
type: "string",
|
|
26030
|
+
value: out,
|
|
26031
|
+
pos: start
|
|
26032
|
+
});
|
|
26033
|
+
continue;
|
|
26034
|
+
}
|
|
26035
|
+
if (isIdentStart(ch)) {
|
|
26036
|
+
const start = i;
|
|
26037
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
26038
|
+
const text = source.slice(start, i);
|
|
26039
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
26040
|
+
type: "keyword",
|
|
26041
|
+
keyword: keywordOf(text),
|
|
26042
|
+
pos: start
|
|
26043
|
+
});
|
|
26044
|
+
else tokens.push({
|
|
26045
|
+
type: "identifier",
|
|
26046
|
+
name: text,
|
|
26047
|
+
pos: start
|
|
26048
|
+
});
|
|
26049
|
+
continue;
|
|
26050
|
+
}
|
|
26051
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
26052
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
26053
|
+
tokens.push({
|
|
26054
|
+
type: "punct",
|
|
26055
|
+
punct: two,
|
|
26056
|
+
pos: i
|
|
26057
|
+
});
|
|
26058
|
+
i += 2;
|
|
26059
|
+
continue;
|
|
26060
|
+
}
|
|
26061
|
+
if (isSinglePunct(ch)) {
|
|
26062
|
+
tokens.push({
|
|
26063
|
+
type: "punct",
|
|
26064
|
+
punct: ch,
|
|
26065
|
+
pos: i
|
|
26066
|
+
});
|
|
26067
|
+
i += 1;
|
|
26068
|
+
continue;
|
|
26069
|
+
}
|
|
26070
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
26071
|
+
}
|
|
26072
|
+
tokens.push({
|
|
26073
|
+
type: "eof",
|
|
26074
|
+
pos: n
|
|
26075
|
+
});
|
|
26076
|
+
return tokens;
|
|
26077
|
+
}
|
|
26078
|
+
function keywordOf(text) {
|
|
26079
|
+
if (text === "true") return "true";
|
|
26080
|
+
if (text === "false") return "false";
|
|
26081
|
+
return "null";
|
|
26082
|
+
}
|
|
26083
|
+
function isSinglePunct(ch) {
|
|
26084
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
26085
|
+
}
|
|
26382
26086
|
/**
|
|
26383
|
-
*
|
|
26384
|
-
*
|
|
26385
|
-
*
|
|
26386
|
-
*
|
|
26387
|
-
* `
|
|
26388
|
-
*
|
|
26389
|
-
*
|
|
26087
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
26088
|
+
*
|
|
26089
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
26090
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
26091
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
26092
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
26093
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
26094
|
+
* that references a since-removed builtin degrades at read.
|
|
26095
|
+
*
|
|
26096
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
26097
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
26390
26098
|
*/
|
|
26391
|
-
|
|
26392
|
-
|
|
26393
|
-
|
|
26394
|
-
|
|
26395
|
-
|
|
26396
|
-
|
|
26397
|
-
|
|
26398
|
-
|
|
26399
|
-
|
|
26099
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
26100
|
+
var BINARY_PRECEDENCE = {
|
|
26101
|
+
"||": 1,
|
|
26102
|
+
"&&": 2,
|
|
26103
|
+
"==": 3,
|
|
26104
|
+
"!=": 3,
|
|
26105
|
+
"<": 4,
|
|
26106
|
+
"<=": 4,
|
|
26107
|
+
">": 4,
|
|
26108
|
+
">=": 4,
|
|
26109
|
+
"+": 5,
|
|
26110
|
+
"-": 5,
|
|
26111
|
+
"*": 6,
|
|
26112
|
+
"/": 6,
|
|
26113
|
+
"%": 6
|
|
26400
26114
|
};
|
|
26401
|
-
|
|
26402
|
-
|
|
26403
|
-
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
|
|
26408
|
-
|
|
26409
|
-
|
|
26410
|
-
|
|
26411
|
-
|
|
26412
|
-
|
|
26115
|
+
function isLogicalOp(op) {
|
|
26116
|
+
return op === "&&" || op === "||";
|
|
26117
|
+
}
|
|
26118
|
+
function isBinaryOp(op) {
|
|
26119
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
26120
|
+
}
|
|
26121
|
+
var Parser = class {
|
|
26122
|
+
tokens;
|
|
26123
|
+
pos = 0;
|
|
26124
|
+
nodeCount = 0;
|
|
26125
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
26126
|
+
callees = /* @__PURE__ */ new Set();
|
|
26127
|
+
constructor(tokens) {
|
|
26128
|
+
this.tokens = tokens;
|
|
26129
|
+
}
|
|
26130
|
+
parse() {
|
|
26131
|
+
const ast = this.parseTernary();
|
|
26132
|
+
const tok = this.peek();
|
|
26133
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
26134
|
+
return {
|
|
26135
|
+
ast,
|
|
26136
|
+
identifiers: this.identifiers,
|
|
26137
|
+
callees: this.callees,
|
|
26138
|
+
nodeCount: this.nodeCount
|
|
26139
|
+
};
|
|
26140
|
+
}
|
|
26141
|
+
peek() {
|
|
26142
|
+
return this.tokens[this.pos];
|
|
26143
|
+
}
|
|
26144
|
+
next() {
|
|
26145
|
+
return this.tokens[this.pos++];
|
|
26146
|
+
}
|
|
26147
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
26148
|
+
expectPunct(punct) {
|
|
26149
|
+
const tok = this.peek();
|
|
26150
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
26151
|
+
this.pos += 1;
|
|
26413
26152
|
}
|
|
26414
|
-
|
|
26415
|
-
|
|
26416
|
-
|
|
26417
|
-
|
|
26418
|
-
|
|
26419
|
-
const max = transform.clamp[1];
|
|
26420
|
-
out = Math.min(max, Math.max(min, out));
|
|
26153
|
+
matchPunct(punct) {
|
|
26154
|
+
const tok = this.peek();
|
|
26155
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
26156
|
+
this.pos += 1;
|
|
26157
|
+
return true;
|
|
26421
26158
|
}
|
|
26422
|
-
return
|
|
26159
|
+
return false;
|
|
26423
26160
|
}
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
|
|
26427
|
-
/** Returns true when `x` is a non-null, non-array plain object. */
|
|
26428
|
-
function isRecord(x) {
|
|
26429
|
-
return x !== null && typeof x === "object" && !Array.isArray(x);
|
|
26430
|
-
}
|
|
26431
|
-
/** Read a dot-path (`a.b.c`) out of an object. Returns `undefined` for any
|
|
26432
|
-
* missing/non-object segment — never throws. */
|
|
26433
|
-
function getByPath(obj, path) {
|
|
26434
|
-
const parts = path.split(".");
|
|
26435
|
-
let cur = obj;
|
|
26436
|
-
for (const part of parts) {
|
|
26437
|
-
if (!isRecord(cur)) return void 0;
|
|
26438
|
-
cur = cur[part];
|
|
26161
|
+
countNode() {
|
|
26162
|
+
this.nodeCount += 1;
|
|
26163
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
26439
26164
|
}
|
|
26440
|
-
|
|
26441
|
-
|
|
26442
|
-
|
|
26443
|
-
|
|
26444
|
-
|
|
26445
|
-
|
|
26446
|
-
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26450
|
-
|
|
26451
|
-
|
|
26452
|
-
|
|
26453
|
-
...obj,
|
|
26454
|
-
[path]: value
|
|
26455
|
-
};
|
|
26456
|
-
const head = path.slice(0, dotIndex);
|
|
26457
|
-
const tail = path.slice(dotIndex + 1);
|
|
26458
|
-
const child = obj[head];
|
|
26459
|
-
const childObj = isRecord(child) ? child : {};
|
|
26460
|
-
return {
|
|
26461
|
-
...obj,
|
|
26462
|
-
[head]: setByPath(childObj, tail, value)
|
|
26463
|
-
};
|
|
26464
|
-
}
|
|
26465
|
-
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
|
|
26466
|
-
* Zod v4 exposes `.unwrap()` on all three wrapper classes. */
|
|
26467
|
-
function unwrap(schema) {
|
|
26468
|
-
let s = schema;
|
|
26469
|
-
while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
|
|
26470
|
-
return s;
|
|
26471
|
-
}
|
|
26472
|
-
function leafKind(schema) {
|
|
26473
|
-
const s = unwrap(schema);
|
|
26474
|
-
if (s instanceof zod.z.ZodEnum) return {
|
|
26475
|
-
kind: "enum",
|
|
26476
|
-
enumValues: s.options
|
|
26477
|
-
};
|
|
26478
|
-
if (s instanceof zod.z.ZodNumber) return { kind: "number" };
|
|
26479
|
-
if (s instanceof zod.z.ZodString) return { kind: "string" };
|
|
26480
|
-
if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
|
|
26481
|
-
return null;
|
|
26482
|
-
}
|
|
26483
|
-
/** Walk a cap status schema into flat wireable leaf fields (dotted paths).
|
|
26484
|
-
* Recurses into nested ZodObject (unwrapping nullable/optional/default first),
|
|
26485
|
-
* so fields with null live values are still offered. Arrays / unknown shapes
|
|
26486
|
-
* are skipped — never throws. */
|
|
26487
|
-
function enumerateSchemaFields(schema, prefix = "") {
|
|
26488
|
-
const root = unwrap(schema);
|
|
26489
|
-
if (!(root instanceof zod.z.ZodObject)) return [];
|
|
26490
|
-
const out = [];
|
|
26491
|
-
const shape = root.shape;
|
|
26492
|
-
for (const [key, field] of Object.entries(shape)) {
|
|
26493
|
-
const path = prefix ? `${prefix}.${key}` : key;
|
|
26494
|
-
const inner = unwrap(field);
|
|
26495
|
-
if (inner instanceof zod.z.ZodObject) {
|
|
26496
|
-
out.push(...enumerateSchemaFields(inner, path));
|
|
26497
|
-
continue;
|
|
26165
|
+
parseTernary() {
|
|
26166
|
+
const test = this.parseBinary(1);
|
|
26167
|
+
if (this.matchPunct("?")) {
|
|
26168
|
+
const consequent = this.parseTernary();
|
|
26169
|
+
this.expectPunct(":");
|
|
26170
|
+
const alternate = this.parseTernary();
|
|
26171
|
+
this.countNode();
|
|
26172
|
+
return {
|
|
26173
|
+
kind: "conditional",
|
|
26174
|
+
test,
|
|
26175
|
+
consequent,
|
|
26176
|
+
alternate
|
|
26177
|
+
};
|
|
26498
26178
|
}
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26502
|
-
|
|
26503
|
-
|
|
26504
|
-
|
|
26179
|
+
return test;
|
|
26180
|
+
}
|
|
26181
|
+
parseBinary(minPrec) {
|
|
26182
|
+
let left = this.parseUnary();
|
|
26183
|
+
for (;;) {
|
|
26184
|
+
const tok = this.peek();
|
|
26185
|
+
if (tok.type !== "punct") break;
|
|
26186
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
26187
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
26188
|
+
const op = tok.punct;
|
|
26189
|
+
this.pos += 1;
|
|
26190
|
+
const right = this.parseBinary(prec + 1);
|
|
26191
|
+
this.countNode();
|
|
26192
|
+
if (isLogicalOp(op)) left = {
|
|
26193
|
+
kind: "logical",
|
|
26194
|
+
op,
|
|
26195
|
+
left,
|
|
26196
|
+
right
|
|
26197
|
+
};
|
|
26198
|
+
else if (isBinaryOp(op)) left = {
|
|
26199
|
+
kind: "binary",
|
|
26200
|
+
op,
|
|
26201
|
+
left,
|
|
26202
|
+
right
|
|
26203
|
+
};
|
|
26204
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
26205
|
+
}
|
|
26206
|
+
return left;
|
|
26207
|
+
}
|
|
26208
|
+
parseUnary() {
|
|
26209
|
+
const tok = this.peek();
|
|
26210
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
26211
|
+
const op = tok.punct;
|
|
26212
|
+
this.pos += 1;
|
|
26213
|
+
const operand = this.parseUnary();
|
|
26214
|
+
this.countNode();
|
|
26215
|
+
return {
|
|
26216
|
+
kind: "unary",
|
|
26217
|
+
op,
|
|
26218
|
+
operand
|
|
26219
|
+
};
|
|
26220
|
+
}
|
|
26221
|
+
return this.parsePrimary();
|
|
26222
|
+
}
|
|
26223
|
+
parsePrimary() {
|
|
26224
|
+
const tok = this.next();
|
|
26225
|
+
switch (tok.type) {
|
|
26226
|
+
case "number":
|
|
26227
|
+
this.countNode();
|
|
26228
|
+
return {
|
|
26229
|
+
kind: "literal",
|
|
26230
|
+
value: tok.value
|
|
26231
|
+
};
|
|
26232
|
+
case "string":
|
|
26233
|
+
this.countNode();
|
|
26234
|
+
return {
|
|
26235
|
+
kind: "literal",
|
|
26236
|
+
value: tok.value
|
|
26237
|
+
};
|
|
26238
|
+
case "keyword":
|
|
26239
|
+
this.countNode();
|
|
26240
|
+
return {
|
|
26241
|
+
kind: "literal",
|
|
26242
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
26243
|
+
};
|
|
26244
|
+
case "identifier": {
|
|
26245
|
+
const nextTok = this.peek();
|
|
26246
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
26247
|
+
this.identifiers.add(tok.name);
|
|
26248
|
+
this.countNode();
|
|
26249
|
+
return {
|
|
26250
|
+
kind: "identifier",
|
|
26251
|
+
name: tok.name
|
|
26252
|
+
};
|
|
26253
|
+
}
|
|
26254
|
+
case "punct":
|
|
26255
|
+
if (tok.punct === "(") {
|
|
26256
|
+
const inner = this.parseTernary();
|
|
26257
|
+
this.expectPunct(")");
|
|
26258
|
+
return inner;
|
|
26259
|
+
}
|
|
26260
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
26261
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
26262
|
+
}
|
|
26263
|
+
}
|
|
26264
|
+
parseCall(callee, pos) {
|
|
26265
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
26266
|
+
this.expectPunct("(");
|
|
26267
|
+
const args = [];
|
|
26268
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
26269
|
+
args.push(this.parseTernary());
|
|
26270
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
26271
|
+
if (this.matchPunct(",")) continue;
|
|
26272
|
+
this.expectPunct(")");
|
|
26273
|
+
break;
|
|
26274
|
+
}
|
|
26275
|
+
this.callees.add(callee);
|
|
26276
|
+
this.countNode();
|
|
26277
|
+
return {
|
|
26278
|
+
kind: "call",
|
|
26279
|
+
callee,
|
|
26280
|
+
args
|
|
26281
|
+
};
|
|
26282
|
+
}
|
|
26283
|
+
};
|
|
26284
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
26285
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
26286
|
+
function parseExpression(source) {
|
|
26287
|
+
return new Parser(tokenize(source)).parse();
|
|
26288
|
+
}
|
|
26289
|
+
/**
|
|
26290
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
26291
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
26292
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
26293
|
+
* one per read on a hot resolve path.
|
|
26294
|
+
*
|
|
26295
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
26296
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
26297
|
+
* callers is safe and maximises hit rate.
|
|
26298
|
+
*/
|
|
26299
|
+
var cache = /* @__PURE__ */ new Map();
|
|
26300
|
+
function getCached(source) {
|
|
26301
|
+
const hit = cache.get(source);
|
|
26302
|
+
if (hit !== void 0) {
|
|
26303
|
+
cache.delete(source);
|
|
26304
|
+
cache.set(source, hit);
|
|
26305
|
+
return hit;
|
|
26505
26306
|
}
|
|
26506
|
-
|
|
26307
|
+
let result;
|
|
26308
|
+
try {
|
|
26309
|
+
result = {
|
|
26310
|
+
ok: true,
|
|
26311
|
+
parsed: parseExpression(source)
|
|
26312
|
+
};
|
|
26313
|
+
} catch (err) {
|
|
26314
|
+
result = {
|
|
26315
|
+
ok: false,
|
|
26316
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
26317
|
+
};
|
|
26318
|
+
}
|
|
26319
|
+
cache.set(source, result);
|
|
26320
|
+
if (cache.size > 256) {
|
|
26321
|
+
const oldest = cache.keys().next().value;
|
|
26322
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
26323
|
+
}
|
|
26324
|
+
return result;
|
|
26507
26325
|
}
|
|
26508
|
-
/**
|
|
26509
|
-
*
|
|
26510
|
-
|
|
26511
|
-
|
|
26512
|
-
|
|
26513
|
-
|
|
26514
|
-
|
|
26515
|
-
|
|
26516
|
-
|
|
26517
|
-
|
|
26326
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
26327
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
26328
|
+
function compileExpressionSafe(source) {
|
|
26329
|
+
return getCached(source);
|
|
26330
|
+
}
|
|
26331
|
+
Object.freeze({});
|
|
26332
|
+
/**
|
|
26333
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
26334
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
26335
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
26336
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
26337
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
26338
|
+
*/
|
|
26339
|
+
function validateExpressionSource(src) {
|
|
26340
|
+
const names = Object.keys(src.bindings);
|
|
26341
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
26342
|
+
for (const name of names) {
|
|
26343
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
26344
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
26345
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
26346
|
+
}
|
|
26347
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
26348
|
+
if (!compiled.ok) return compiled.error;
|
|
26349
|
+
const bound = new Set(names);
|
|
26350
|
+
for (const id of compiled.parsed.identifiers) {
|
|
26351
|
+
if (id === "now") continue;
|
|
26352
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
26353
|
+
}
|
|
26354
|
+
return null;
|
|
26518
26355
|
}
|
|
26519
26356
|
/**
|
|
26357
|
+
* What an expression's named bindings READ from.
|
|
26358
|
+
*
|
|
26359
|
+
* Salvaged verbatim from the deleted device-link mechanism. Wiring's source
|
|
26360
|
+
* kinds were the one part of it worth keeping: addressing a device field by
|
|
26361
|
+
* re-sync-stable `stableId`, a per-device constant, and a sibling-accessory
|
|
26362
|
+
* read are the vocabulary any cross-device derivation needs, and they were
|
|
26363
|
+
* already correct. What wiring got wrong was the DESTINATION — a field on
|
|
26364
|
+
* somebody else's device, with no identity — not the source.
|
|
26365
|
+
*
|
|
26366
|
+
* These shapes are therefore kept, re-homed next to the engine that consumes
|
|
26367
|
+
* them, and are the binding type of a composition recipe (the source picker
|
|
26368
|
+
* stays `deviceManager.getWireableFields`). They deliberately do NOT nest: a
|
|
26369
|
+
* binding is a read, never another expression.
|
|
26370
|
+
*
|
|
26371
|
+
* Schemas are authoritative; every type is `z.infer` of one, so a wire shape and
|
|
26372
|
+
* a TypeScript shape cannot drift apart (`scripts/check-schema-type-twins.ts`).
|
|
26373
|
+
*/
|
|
26374
|
+
/** Read a sibling accessory's status field, addressed by the sibling's key.
|
|
26375
|
+
* `kind` is optional for wire compatibility — absent means `'field'`. */
|
|
26376
|
+
var ExpressionFieldBindingSchema = zod.z.object({
|
|
26377
|
+
kind: zod.z.literal("field").optional(),
|
|
26378
|
+
sourceKey: zod.z.string(),
|
|
26379
|
+
cap: zod.z.string(),
|
|
26380
|
+
fieldPath: zod.z.string()
|
|
26381
|
+
});
|
|
26382
|
+
/** A constant. No device is read. */
|
|
26383
|
+
var ExpressionLiteralBindingSchema = zod.z.object({
|
|
26384
|
+
kind: zod.z.literal("literal"),
|
|
26385
|
+
value: zod.z.union([
|
|
26386
|
+
zod.z.string(),
|
|
26387
|
+
zod.z.number(),
|
|
26388
|
+
zod.z.boolean(),
|
|
26389
|
+
zod.z.null()
|
|
26390
|
+
])
|
|
26391
|
+
});
|
|
26392
|
+
/** Read ANY device's status field, addressed by its re-sync-stable `stableId` —
|
|
26393
|
+
* never by numeric id, which a re-adoption reissues. */
|
|
26394
|
+
var ExpressionGlobalBindingSchema = zod.z.object({
|
|
26395
|
+
kind: zod.z.literal("global"),
|
|
26396
|
+
sourceStableId: zod.z.string(),
|
|
26397
|
+
cap: zod.z.string(),
|
|
26398
|
+
fieldPath: zod.z.string()
|
|
26399
|
+
});
|
|
26400
|
+
var ExpressionBindingSourceSchema = zod.z.union([
|
|
26401
|
+
ExpressionFieldBindingSchema,
|
|
26402
|
+
ExpressionLiteralBindingSchema,
|
|
26403
|
+
ExpressionGlobalBindingSchema
|
|
26404
|
+
]);
|
|
26405
|
+
zod.z.object({
|
|
26406
|
+
expr: zod.z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
26407
|
+
bindings: zod.z.record(zod.z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
26408
|
+
}).superRefine((src, ctx) => {
|
|
26409
|
+
const err = validateExpressionSource(src);
|
|
26410
|
+
if (err !== null) ctx.addIssue({
|
|
26411
|
+
code: "custom",
|
|
26412
|
+
message: err,
|
|
26413
|
+
path: ["expr"]
|
|
26414
|
+
});
|
|
26415
|
+
});
|
|
26416
|
+
/**
|
|
26520
26417
|
* Runtime list of cap names with status. Used by the settings
|
|
26521
26418
|
* aggregator to enumerate caps whose `status` should be polled +
|
|
26522
26419
|
* streamed via `subscribeDeviceStatusAggregate`.
|
|
@@ -27572,6 +27469,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
27572
27469
|
addonId: null,
|
|
27573
27470
|
access: "view"
|
|
27574
27471
|
},
|
|
27472
|
+
"coreBlocks.restart": {
|
|
27473
|
+
capName: "core-blocks",
|
|
27474
|
+
capScope: "system",
|
|
27475
|
+
addonId: null,
|
|
27476
|
+
access: "create"
|
|
27477
|
+
},
|
|
27575
27478
|
"coreBlocks.setEnabled": {
|
|
27576
27479
|
capName: "core-blocks",
|
|
27577
27480
|
capScope: "system",
|
|
@@ -28208,12 +28111,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
28208
28111
|
addonId: null,
|
|
28209
28112
|
access: "create"
|
|
28210
28113
|
},
|
|
28211
|
-
"deviceManager.setDeviceLinks": {
|
|
28212
|
-
capName: "device-manager",
|
|
28213
|
-
capScope: "system",
|
|
28214
|
-
addonId: null,
|
|
28215
|
-
access: "create"
|
|
28216
|
-
},
|
|
28217
28114
|
"deviceManager.setDisabled": {
|
|
28218
28115
|
capName: "device-manager",
|
|
28219
28116
|
capScope: "system",
|
|
@@ -32250,60 +32147,6 @@ TimelapseRuleInputSchema.extend({
|
|
|
32250
32147
|
createdAt: zod.z.number(),
|
|
32251
32148
|
updatedAt: zod.z.number()
|
|
32252
32149
|
});
|
|
32253
|
-
/**
|
|
32254
|
-
* The addon id as the REGISTRY, the wire and the durable stores spell it.
|
|
32255
|
-
*
|
|
32256
|
-
* `AddonContext.id` does not agree with itself across the two context
|
|
32257
|
-
* factories:
|
|
32258
|
-
*
|
|
32259
|
-
* - a FORKED addon gets the bare manifest id
|
|
32260
|
-
* (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
|
|
32261
|
-
* - an addon co-located in hub-main gets it PREFIXED
|
|
32262
|
-
* (`server/backend/src/core/addon/addon-registry.service.ts` →
|
|
32263
|
-
* ``id: `addon:${addonId}` ``).
|
|
32264
|
-
*
|
|
32265
|
-
* Everything an addon might compare `ctx.id` AGAINST carries the bare form:
|
|
32266
|
-
* `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
|
|
32267
|
-
* `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
|
|
32268
|
-
*
|
|
32269
|
-
* So `entry.providerAddonId === this.ctx.id` is silently, permanently false
|
|
32270
|
-
* for a builtin — and only for a builtin, which is why it survives a green
|
|
32271
|
-
* suite whose fake supplies the bare id. That is exactly how camera 615's
|
|
32272
|
-
* virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
|
|
32273
|
-
* binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
|
|
32274
|
-
*
|
|
32275
|
-
* Route every comparison between `ctx.id` and a registry/store addon id
|
|
32276
|
-
* through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
|
|
32277
|
-
* enforces it in the processes where the prefix exists.
|
|
32278
|
-
*/
|
|
32279
|
-
/** The prefix the hub-main context factory prepends to the manifest id. */
|
|
32280
|
-
var ADDON_ID_PREFIX = "addon:";
|
|
32281
|
-
/** The manifest id, whichever spelling of `ctx.id` you were handed. */
|
|
32282
|
-
function bareAddonId(id) {
|
|
32283
|
-
return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
|
|
32284
|
-
}
|
|
32285
|
-
/** True when both ids name the same addon, prefixed or not. */
|
|
32286
|
-
function isSameAddonId(a, b) {
|
|
32287
|
-
return bareAddonId(a) === bareAddonId(b);
|
|
32288
|
-
}
|
|
32289
|
-
/**
|
|
32290
|
-
* Decode base64 little-endian Float32 back into a vector.
|
|
32291
|
-
*
|
|
32292
|
-
* Throws on a byte length that is not a multiple of 4 — a truncated vector
|
|
32293
|
-
* would otherwise silently rank against a shorter one and produce a plausible
|
|
32294
|
-
* score, which is worse than an error.
|
|
32295
|
-
*/
|
|
32296
|
-
function decodeVectorBase64(encoded) {
|
|
32297
|
-
const buffer = Buffer.from(encoded, "base64");
|
|
32298
|
-
if (buffer.byteLength % 4 !== 0) throw new Error(`decodeVectorBase64: ${buffer.byteLength} bytes is not a whole number of Float32 values`);
|
|
32299
|
-
const out = new Float32Array(buffer.byteLength / 4);
|
|
32300
|
-
for (let i = 0; i < out.length; i += 1) out[i] = buffer.readFloatLE(i * 4);
|
|
32301
|
-
return out;
|
|
32302
|
-
}
|
|
32303
|
-
/** Vector length implied by a base64 payload, without decoding it. */
|
|
32304
|
-
function vectorDimFromBase64(encoded) {
|
|
32305
|
-
return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
|
|
32306
|
-
}
|
|
32307
32150
|
zod.z.object({
|
|
32308
32151
|
/**
|
|
32309
32152
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -32411,6 +32254,60 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
|
|
|
32411
32254
|
DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
32412
32255
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
32413
32256
|
/**
|
|
32257
|
+
* The addon id as the REGISTRY, the wire and the durable stores spell it.
|
|
32258
|
+
*
|
|
32259
|
+
* `AddonContext.id` does not agree with itself across the two context
|
|
32260
|
+
* factories:
|
|
32261
|
+
*
|
|
32262
|
+
* - a FORKED addon gets the bare manifest id
|
|
32263
|
+
* (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
|
|
32264
|
+
* - an addon co-located in hub-main gets it PREFIXED
|
|
32265
|
+
* (`server/backend/src/core/addon/addon-registry.service.ts` →
|
|
32266
|
+
* ``id: `addon:${addonId}` ``).
|
|
32267
|
+
*
|
|
32268
|
+
* Everything an addon might compare `ctx.id` AGAINST carries the bare form:
|
|
32269
|
+
* `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
|
|
32270
|
+
* `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
|
|
32271
|
+
*
|
|
32272
|
+
* So `entry.providerAddonId === this.ctx.id` is silently, permanently false
|
|
32273
|
+
* for a builtin — and only for a builtin, which is why it survives a green
|
|
32274
|
+
* suite whose fake supplies the bare id. That is exactly how camera 615's
|
|
32275
|
+
* virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
|
|
32276
|
+
* binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
|
|
32277
|
+
*
|
|
32278
|
+
* Route every comparison between `ctx.id` and a registry/store addon id
|
|
32279
|
+
* through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
|
|
32280
|
+
* enforces it in the processes where the prefix exists.
|
|
32281
|
+
*/
|
|
32282
|
+
/** The prefix the hub-main context factory prepends to the manifest id. */
|
|
32283
|
+
var ADDON_ID_PREFIX = "addon:";
|
|
32284
|
+
/** The manifest id, whichever spelling of `ctx.id` you were handed. */
|
|
32285
|
+
function bareAddonId(id) {
|
|
32286
|
+
return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
|
|
32287
|
+
}
|
|
32288
|
+
/** True when both ids name the same addon, prefixed or not. */
|
|
32289
|
+
function isSameAddonId(a, b) {
|
|
32290
|
+
return bareAddonId(a) === bareAddonId(b);
|
|
32291
|
+
}
|
|
32292
|
+
/**
|
|
32293
|
+
* Decode base64 little-endian Float32 back into a vector.
|
|
32294
|
+
*
|
|
32295
|
+
* Throws on a byte length that is not a multiple of 4 — a truncated vector
|
|
32296
|
+
* would otherwise silently rank against a shorter one and produce a plausible
|
|
32297
|
+
* score, which is worse than an error.
|
|
32298
|
+
*/
|
|
32299
|
+
function decodeVectorBase64(encoded) {
|
|
32300
|
+
const buffer = Buffer.from(encoded, "base64");
|
|
32301
|
+
if (buffer.byteLength % 4 !== 0) throw new Error(`decodeVectorBase64: ${buffer.byteLength} bytes is not a whole number of Float32 values`);
|
|
32302
|
+
const out = new Float32Array(buffer.byteLength / 4);
|
|
32303
|
+
for (let i = 0; i < out.length; i += 1) out[i] = buffer.readFloatLE(i * 4);
|
|
32304
|
+
return out;
|
|
32305
|
+
}
|
|
32306
|
+
/** Vector length implied by a base64 payload, without decoding it. */
|
|
32307
|
+
function vectorDimFromBase64(encoded) {
|
|
32308
|
+
return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
|
|
32309
|
+
}
|
|
32310
|
+
/**
|
|
32414
32311
|
* Scores all applicable inference backends for the given hardware.
|
|
32415
32312
|
*
|
|
32416
32313
|
* Rules (hardware-driven only — no Python import probes):
|
|
@@ -32560,6 +32457,12 @@ Object.defineProperty(exports, "CAP_NAMES_WITH_STATUS", {
|
|
|
32560
32457
|
return CAP_NAMES_WITH_STATUS;
|
|
32561
32458
|
}
|
|
32562
32459
|
});
|
|
32460
|
+
Object.defineProperty(exports, "CORE_BLOCK_ADDON_PREFIX", {
|
|
32461
|
+
enumerable: true,
|
|
32462
|
+
get: function() {
|
|
32463
|
+
return CORE_BLOCK_ADDON_PREFIX;
|
|
32464
|
+
}
|
|
32465
|
+
});
|
|
32563
32466
|
Object.defineProperty(exports, "CoreBlockSchema", {
|
|
32564
32467
|
enumerable: true,
|
|
32565
32468
|
get: function() {
|
|
@@ -32692,12 +32595,6 @@ Object.defineProperty(exports, "alertsCapability", {
|
|
|
32692
32595
|
return alertsCapability;
|
|
32693
32596
|
}
|
|
32694
32597
|
});
|
|
32695
|
-
Object.defineProperty(exports, "applyTransform", {
|
|
32696
|
-
enumerable: true,
|
|
32697
|
-
get: function() {
|
|
32698
|
-
return applyTransform;
|
|
32699
|
-
}
|
|
32700
|
-
});
|
|
32701
32598
|
Object.defineProperty(exports, "asJsonObject", {
|
|
32702
32599
|
enumerable: true,
|
|
32703
32600
|
get: function() {
|
|
@@ -32740,6 +32637,18 @@ Object.defineProperty(exports, "buildStreamParamsConfigSchema", {
|
|
|
32740
32637
|
return buildStreamParamsConfigSchema;
|
|
32741
32638
|
}
|
|
32742
32639
|
});
|
|
32640
|
+
Object.defineProperty(exports, "coreBlockAddonId", {
|
|
32641
|
+
enumerable: true,
|
|
32642
|
+
get: function() {
|
|
32643
|
+
return coreBlockAddonId;
|
|
32644
|
+
}
|
|
32645
|
+
});
|
|
32646
|
+
Object.defineProperty(exports, "coreBlockIdFromAddonId", {
|
|
32647
|
+
enumerable: true,
|
|
32648
|
+
get: function() {
|
|
32649
|
+
return coreBlockIdFromAddonId;
|
|
32650
|
+
}
|
|
32651
|
+
});
|
|
32743
32652
|
Object.defineProperty(exports, "coreBlocksCapability", {
|
|
32744
32653
|
enumerable: true,
|
|
32745
32654
|
get: function() {
|
|
@@ -32824,12 +32733,6 @@ Object.defineProperty(exports, "errMsg", {
|
|
|
32824
32733
|
return errMsg;
|
|
32825
32734
|
}
|
|
32826
32735
|
});
|
|
32827
|
-
Object.defineProperty(exports, "evaluateLinkExpression", {
|
|
32828
|
-
enumerable: true,
|
|
32829
|
-
get: function() {
|
|
32830
|
-
return evaluateLinkExpression;
|
|
32831
|
-
}
|
|
32832
|
-
});
|
|
32833
32736
|
Object.defineProperty(exports, "expandCapMethods", {
|
|
32834
32737
|
enumerable: true,
|
|
32835
32738
|
get: function() {
|
|
@@ -32848,12 +32751,6 @@ Object.defineProperty(exports, "filesystemBrowseCapability", {
|
|
|
32848
32751
|
return filesystemBrowseCapability;
|
|
32849
32752
|
}
|
|
32850
32753
|
});
|
|
32851
|
-
Object.defineProperty(exports, "getByPath", {
|
|
32852
|
-
enumerable: true,
|
|
32853
|
-
get: function() {
|
|
32854
|
-
return getByPath;
|
|
32855
|
-
}
|
|
32856
|
-
});
|
|
32857
32754
|
Object.defineProperty(exports, "hydrateSchema", {
|
|
32858
32755
|
enumerable: true,
|
|
32859
32756
|
get: function() {
|
|
@@ -32950,6 +32847,12 @@ Object.defineProperty(exports, "normalizeUnit", {
|
|
|
32950
32847
|
return normalizeUnit;
|
|
32951
32848
|
}
|
|
32952
32849
|
});
|
|
32850
|
+
Object.defineProperty(exports, "oauthIntegrationCapability", {
|
|
32851
|
+
enumerable: true,
|
|
32852
|
+
get: function() {
|
|
32853
|
+
return oauthIntegrationCapability;
|
|
32854
|
+
}
|
|
32855
|
+
});
|
|
32953
32856
|
Object.defineProperty(exports, "objectInputDeclaresAddonId", {
|
|
32954
32857
|
enumerable: true,
|
|
32955
32858
|
get: function() {
|
|
@@ -33010,12 +32913,6 @@ Object.defineProperty(exports, "scoreRuntimes", {
|
|
|
33010
32913
|
return scoreRuntimes;
|
|
33011
32914
|
}
|
|
33012
32915
|
});
|
|
33013
|
-
Object.defineProperty(exports, "setByPath", {
|
|
33014
|
-
enumerable: true,
|
|
33015
|
-
get: function() {
|
|
33016
|
-
return setByPath;
|
|
33017
|
-
}
|
|
33018
|
-
});
|
|
33019
32916
|
Object.defineProperty(exports, "settingsStoreCapability", {
|
|
33020
32917
|
enumerable: true,
|
|
33021
32918
|
get: function() {
|
|
@@ -33052,24 +32949,12 @@ Object.defineProperty(exports, "streamQualityLabel", {
|
|
|
33052
32949
|
return streamQualityLabel;
|
|
33053
32950
|
}
|
|
33054
32951
|
});
|
|
33055
|
-
Object.defineProperty(exports, "toExpressionValue", {
|
|
33056
|
-
enumerable: true,
|
|
33057
|
-
get: function() {
|
|
33058
|
-
return toExpressionValue;
|
|
33059
|
-
}
|
|
33060
|
-
});
|
|
33061
32952
|
Object.defineProperty(exports, "userManagementCapability", {
|
|
33062
32953
|
enumerable: true,
|
|
33063
32954
|
get: function() {
|
|
33064
32955
|
return userManagementCapability;
|
|
33065
32956
|
}
|
|
33066
32957
|
});
|
|
33067
|
-
Object.defineProperty(exports, "validateExpressionSource", {
|
|
33068
|
-
enumerable: true,
|
|
33069
|
-
get: function() {
|
|
33070
|
-
return validateExpressionSource;
|
|
33071
|
-
}
|
|
33072
|
-
});
|
|
33073
32958
|
Object.defineProperty(exports, "vectorDimFromBase64", {
|
|
33074
32959
|
enumerable: true,
|
|
33075
32960
|
get: function() {
|