@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.
Files changed (90) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +3 -3
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/block-package.d.ts +9 -27
  16. package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
  17. package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
  18. package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
  19. package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
  20. package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
  21. package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
  22. package/dist/builtins/device-manager/device-manager.addon.js +123 -884
  23. package/dist/builtins/device-manager/device-manager.addon.mjs +123 -884
  24. package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
  25. package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
  26. package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
  27. package/dist/builtins/device-manager/device-projection.d.ts +1 -2
  28. package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
  29. package/dist/builtins/device-manager/device-queries.d.ts +1 -1
  30. package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
  31. package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
  32. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  33. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  34. package/dist/builtins/hub-forwarder/index.js +1 -1
  35. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  37. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  38. package/dist/builtins/local-auth/homeassistant-integration.d.ts +7 -0
  39. package/dist/builtins/local-auth/index.d.ts +1 -0
  40. package/dist/builtins/local-auth/index.js +1 -0
  41. package/dist/builtins/local-auth/index.mjs +2 -2
  42. package/dist/builtins/local-auth/local-auth.addon.js +561 -497
  43. package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
  44. package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
  45. package/dist/builtins/local-auth/oauth-session-manager.d.ts +1 -1
  46. package/dist/builtins/local-network/local-network.addon.js +1 -1
  47. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  48. package/dist/builtins/loki-logging/index.js +1 -1
  49. package/dist/builtins/loki-logging/index.mjs +1 -1
  50. package/dist/builtins/native-metrics/native-metrics.addon.js +2 -2
  51. package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
  52. package/dist/builtins/platform-probe/index.js +2 -2
  53. package/dist/builtins/platform-probe/index.mjs +2 -2
  54. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  55. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  56. package/dist/builtins/snapshot/index.js +319 -20
  57. package/dist/builtins/snapshot/index.mjs +319 -20
  58. package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
  59. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
  60. package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -1
  61. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  62. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  63. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +15 -0
  64. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +73 -7
  65. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +73 -7
  66. package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +19 -0
  67. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  69. package/dist/builtins/system-config/system-config.addon.js +1 -1
  70. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  71. package/dist/builtins/winston-logging/index.js +1 -1
  72. package/dist/builtins/winston-logging/index.mjs +1 -1
  73. package/dist/{dist-DnKpFaP1.mjs → dist-CMb7aKS0.mjs} +1227 -1330
  74. package/dist/{dist-DgPUarcs.js → dist-DP3nHlNM.js} +1251 -1366
  75. package/dist/http/asset-compression.d.ts +93 -0
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.js +64 -9
  78. package/dist/index.mjs +65 -11
  79. package/dist/kernel/heap-watch.d.ts +55 -0
  80. package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
  81. package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-BE6l8Y4W.js} +21 -15
  82. package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-Dz1ryHIN.mjs} +21 -15
  83. package/dist/{model-download-service-D8B-4ktF.mjs → model-download-service-CTBHzORJ.mjs} +220 -2
  84. package/dist/{model-download-service-Cpc6SWzV.js → model-download-service-Caj1kaSi.js} +220 -2
  85. package/package.json +5 -1
  86. package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
  87. package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
  88. package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
  89. package/dist/main-CVLq8IzA.js +0 -2356
  90. package/dist/main-CzH19GkU.mjs +0 -2353
@@ -5967,12 +5967,34 @@ var streamBrokerCapability = {
5967
5967
  kinds: z.array(EventMediaKindSchema).min(1).default(["mp4"]),
5968
5968
  /** GIF geometry. The video keeps the source's own. */
5969
5969
  gifMaxWidth: z.number().int().min(120).max(1280).default(640),
5970
- gifFps: z.number().int().min(1).max(15).default(8),
5971
5970
  /**
5972
- * Playback rate, applied to EVERY container so they stay one clip.
5973
- * `1` is real time and is what allows the copy branch.
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
- speed: z.number().min(1).max(8).default(1)
5976
+ gifFps: z.number().int().min(1).max(15).default(12),
5977
+ /**
5978
+ * How fast the GIF plays against real time, independent of `speed`.
5979
+ *
5980
+ * 4× by default, by operator request: a notification gif is glanced at
5981
+ * on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
5982
+ * a separate knob from `speed` even though both now default to 4 —
5983
+ * a caller wanting a real-time video and a fast gif must not have to
5984
+ * choose.
5985
+ */
5986
+ gifSpeed: z.number().min(1).max(8).default(4),
5987
+ /**
5988
+ * Playback rate of the VIDEO. Also 4× by default, by operator decision.
5989
+ *
5990
+ * `1` is real time and is the ONLY value that allows the copy branch —
5991
+ * anything else forces `libx264` over the window. That was priced
5992
+ * before it was chosen: a per-event burst measured at 0.23 s and 254 KB
5993
+ * on a real 615 720p cut, against 922 KB for the copy it replaces. A
5994
+ * re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
5995
+ * once the decode is forced the width stops being free.
5996
+ */
5997
+ speed: z.number().min(1).max(8).default(4)
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 = z.union([z.literal("hub"), 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(z.object({ blockId: z.string() }), 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, z.void(), { kind: "mutation" })
7694
7755
  }
7695
7756
  };
7757
+ var ProviderStatusSchema = z.object({
7758
+ connected: z.boolean(),
7759
+ deviceCount: z.number(),
7760
+ error: z.string().optional()
7761
+ });
7762
+ z.object({
7763
+ externalId: z.string(),
7764
+ name: z.string(),
7765
+ type: z.string(),
7766
+ metadata: z.record(z.string(), z.unknown()).optional()
7767
+ });
7696
7768
  /**
7697
- * Resource-bound constants for the safe expression engine.
7698
- *
7699
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
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
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
7706
- * rejected without allocation. */
7707
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
7708
- /** A legal binding / identifier name. */
7709
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7710
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
7711
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
7712
- var RESERVED_BINDING_NAMES = new Set([
7713
- "now",
7714
- "true",
7715
- "false",
7716
- "null"
7717
- ]);
7773
+ var DiscoveryCandidateSchema = z.object({
7774
+ stableId: z.string(),
7775
+ type: z.enum(DeviceType),
7776
+ suggestedName: z.string(),
7777
+ prefilledConfig: z.record(z.string(), z.unknown()),
7778
+ /**
7779
+ * Optional upstream-system identity (HA entity_id, vendor MAC, …).
7780
+ * Discovery pre-populates this for systems that know the upstream
7781
+ * identity ahead of adoption. Rendering metadata (unit, precision)
7782
+ * flows live through the cap STATUS SLICE after adoption.
7783
+ */
7784
+ sourceInfo: SourceInfoSchema.optional()
7785
+ });
7718
7786
  /**
7719
- * Error types for the safe expression engine. Two distinct classes so callers
7720
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
7721
- * failure both are non-fatal to the host: read paths degrade to "skip link".
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
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
7724
- * the failure is anchored to a character (author-facing inline feedback). */
7725
- var ExpressionParseError = class extends Error {
7726
- position;
7727
- constructor(message, position) {
7728
- super(message);
7729
- this.name = "ExpressionParseError";
7730
- this.position = position;
7731
- }
7732
- };
7733
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
7734
- * result, unknown builtin, step-budget exceeded). */
7735
- var ExpressionEvalError = class extends Error {
7736
- constructor(message) {
7737
- super(message);
7738
- this.name = "ExpressionEvalError";
7791
+ var DeviceSummarySchema = z.object({
7792
+ id: z.number(),
7793
+ stableId: z.string(),
7794
+ addonId: z.string(),
7795
+ type: z.string(),
7796
+ name: z.string(),
7797
+ parentDeviceId: z.number().nullable(),
7798
+ online: z.boolean(),
7799
+ features: z.array(z.string()),
7800
+ config: z.record(z.string(), z.unknown()),
7801
+ /** Optional upstream-system identity (dispatch key + system tag).
7802
+ * See `SourceInfo`. Present when the device has a non-synthetic
7803
+ * source identifier (HA entities, vendor MAC, …); omitted when the
7804
+ * synthetic backfill is in effect. */
7805
+ sourceInfo: SourceInfoSchema.optional()
7806
+ });
7807
+ /**
7808
+ * Result of a live field test (e.g. probing an RTSP URL during device
7809
+ * creation). Matches the UI-side `FieldProbeResult` in
7810
+ * `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
7811
+ * returned `labels` as chips next to the input.
7812
+ */
7813
+ var FieldProbeResultSchema = z.object({
7814
+ status: z.enum(["ok", "error"]),
7815
+ labels: z.array(z.string()).optional(),
7816
+ error: z.string().optional()
7817
+ });
7818
+ /**
7819
+ * The output of `getChildCreationSchema` is a UI schema tree. We store
7820
+ * it as `unknown` at the capability layer — the router just passes it
7821
+ * through and the admin UI renders it via `FormBuilder`. The actual
7822
+ * type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
7823
+ * but we deliberately avoid a Zod mirror because the union is large and
7824
+ * not meant for runtime validation at this seam.
7825
+ */
7826
+ var CreationSchemaOutputSchema = z.unknown();
7827
+ var deviceProviderCapability = {
7828
+ name: "device-provider",
7829
+ scope: "system",
7830
+ mode: "collection",
7831
+ methods: {
7832
+ start: method(z.void(), z.void(), { kind: "mutation" }),
7833
+ stop: method(z.void(), z.void(), { kind: "mutation" }),
7834
+ getStatus: method(z.void(), ProviderStatusSchema),
7835
+ getDevices: method(z.void(), z.array(z.object({
7836
+ id: z.string(),
7837
+ name: z.string(),
7838
+ type: z.string()
7839
+ }))),
7840
+ supportsDiscovery: method(z.object({}), z.boolean()),
7841
+ /**
7842
+ * Run a network scan. `params` carries optional provider-specific scan
7843
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
7844
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
7845
+ * (provider uses its local-network default).
7846
+ */
7847
+ discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
7848
+ kind: "mutation",
7849
+ auth: "admin"
7850
+ }),
7851
+ /**
7852
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
7853
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
7854
+ * `null` when the provider takes no extra scan params — the generic
7855
+ * aggregated scan never renders this; the per-integration scan does.
7856
+ */
7857
+ getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
7858
+ /**
7859
+ * The DeviceType this provider creates via manual add (Camera for
7860
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
7861
+ * provider does not support manual creation. Lets the Add-Device dialog
7862
+ * pick the right type instead of assuming Camera.
7863
+ */
7864
+ getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
7865
+ adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
7866
+ kind: "mutation",
7867
+ auth: "admin"
7868
+ }),
7869
+ supportsManualCreation: method(z.object({}), z.boolean()),
7870
+ /**
7871
+ * Fetch the creation form schema for a given DeviceType. Returns
7872
+ * `null` when the provider does not support manually creating
7873
+ * devices of that type. The output is a `ConfigUISchema` — the
7874
+ * router type-asserts it at the boundary.
7875
+ */
7876
+ getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
7877
+ createDevice: method(z.object({
7878
+ type: z.enum(DeviceType),
7879
+ config: z.record(z.string(), z.unknown())
7880
+ }), DeviceSummarySchema, {
7881
+ kind: "mutation",
7882
+ auth: "admin"
7883
+ }),
7884
+ /**
7885
+ * Test a single field in the creation form before the device has
7886
+ * been persisted. Typical use: probing an RTSP URL entered by the
7887
+ * user. Providers that don't support field probing return
7888
+ * `{ success: true, message: 'Field test not supported' }`.
7889
+ *
7890
+ * `formValues` is the live snapshot of every field in the form at
7891
+ * the moment the user clicked Test — useful for probes that depend
7892
+ * on multiple fields together (e.g. Reolink autodetect needs host
7893
+ * + credentials + UID + transport mode in a single call). Optional
7894
+ * for backwards compatibility; providers free to ignore it.
7895
+ */
7896
+ testCreationField: method(z.object({
7897
+ type: z.enum(DeviceType),
7898
+ key: z.string(),
7899
+ value: z.unknown(),
7900
+ formValues: z.record(z.string(), z.unknown()).optional()
7901
+ }), FieldProbeResultSchema, {
7902
+ kind: "mutation",
7903
+ auth: "admin"
7904
+ })
7739
7905
  }
7740
7906
  };
7741
7907
  /**
7742
- * Frozen, null-prototype builtin function table for the expression engine
7743
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
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
- * Because the object has a NULL prototype AND is `Object.freeze`d:
7748
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
7749
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
7750
- * (there is no `Object.prototype` in the chain), so those names are not
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
- * Every numeric argument is validated as a finite number and every numeric
7754
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
7755
- * (`pow(10,400)` Infinity) all raise `ExpressionEvalError` and fail the link
7756
- * closed rather than emitting a garbage value.
7757
- */
7758
- function asFiniteNumber(value, name, index) {
7759
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
7760
- return value;
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 = z.object({
8433
- connected: z.boolean(),
8434
- deviceCount: z.number(),
8435
- error: z.string().optional()
8436
- });
8437
- z.object({
8438
- externalId: z.string(),
8439
- name: z.string(),
8440
- type: z.string(),
8441
- metadata: z.record(z.string(), 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 = z.object({
8449
- stableId: z.string(),
8450
- type: z.enum(DeviceType),
8451
- suggestedName: z.string(),
8452
- prefilledConfig: z.record(z.string(), 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 = z.object({
8467
- id: z.number(),
8468
- stableId: z.string(),
8469
- addonId: z.string(),
8470
- type: z.string(),
8471
- name: z.string(),
8472
- parentDeviceId: z.number().nullable(),
8473
- online: z.boolean(),
8474
- features: z.array(z.string()),
8475
- config: z.record(z.string(), 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 = z.object({
8489
- status: z.enum(["ok", "error"]),
8490
- labels: z.array(z.string()).optional(),
8491
- error: 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 = z.unknown();
8502
- var deviceProviderCapability = {
8503
- name: "device-provider",
8504
- scope: "system",
8505
- mode: "collection",
8506
- methods: {
8507
- start: method(z.void(), z.void(), { kind: "mutation" }),
8508
- stop: method(z.void(), z.void(), { kind: "mutation" }),
8509
- getStatus: method(z.void(), ProviderStatusSchema),
8510
- getDevices: method(z.void(), z.array(z.object({
8511
- id: z.string(),
8512
- name: z.string(),
8513
- type: z.string()
8514
- }))),
8515
- supportsDiscovery: method(z.object({}), 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(z.object({ params: z.record(z.string(), z.unknown()).optional() }), 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(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(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
8540
- adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
8541
- kind: "mutation",
8542
- auth: "admin"
8543
- }),
8544
- supportsManualCreation: method(z.object({}), 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(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
8552
- createDevice: method(z.object({
8553
- type: z.enum(DeviceType),
8554
- config: z.record(z.string(), 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(z.object({
8572
- type: z.enum(DeviceType),
8573
- key: z.string(),
8574
- value: z.unknown(),
8575
- formValues: z.record(z.string(), 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 = z.object({
8608
7933
  order: z.number().optional(),
8609
7934
  collapsed: 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 = z.object({
8618
- kind: z.literal("field").optional(),
8619
- sourceKey: z.string(),
8620
- cap: z.string(),
8621
- fieldPath: z.string()
8622
- });
8623
- var DeviceLinkLiteralSourceSchema = z.object({
8624
- kind: z.literal("literal"),
8625
- value: z.union([
8626
- z.string(),
8627
- z.number(),
8628
- z.boolean(),
8629
- z.null()
8630
- ])
8631
- });
8632
- var DeviceLinkGlobalSourceSchema = z.object({
8633
- kind: z.literal("global"),
8634
- sourceStableId: z.string(),
8635
- cap: z.string(),
8636
- fieldPath: 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 = z.object({
8646
- kind: z.literal("expression"),
8647
- expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
8648
- bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), 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 = z.object({
8662
- id: z.string(),
8663
- source: z.union([
8664
- DeviceLinkFieldSourceSchema,
8665
- DeviceLinkLiteralSourceSchema,
8666
- DeviceLinkGlobalSourceSchema,
8667
- DeviceLinkExpressionSourceSchema
8668
- ]),
8669
- target: z.object({
8670
- cap: z.string(),
8671
- fieldPath: z.string(),
8672
- itemKey: z.string().optional()
8673
- }),
8674
- transform: z.discriminatedUnion("kind", [
8675
- z.object({ kind: z.literal("identity") }),
8676
- z.object({
8677
- kind: z.literal("enum-map"),
8678
- mapping: z.record(z.string(), z.union([
8679
- z.string(),
8680
- z.number(),
8681
- z.boolean()
8682
- ])),
8683
- fallback: z.union([
8684
- z.string(),
8685
- z.number(),
8686
- z.boolean()
8687
- ]).optional()
8688
- }),
8689
- z.object({
8690
- kind: z.literal("linear"),
8691
- scale: z.number(),
8692
- offset: z.number(),
8693
- clamp: z.tuple([z.number(), 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 = z.object({
@@ -8773,8 +8012,6 @@ var DeviceInfoSchema = z.object({
8773
8012
  * named accordion sections (with optional intra-section order). See
8774
8013
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
8775
8014
  childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
8776
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
8777
- deviceLinks: 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 = z.object({
8783
8020
  value: z.unknown(),
8784
8021
  description: z.string().optional()
8785
8022
  });
8786
- var DeviceLinkModeSchema = z.enum(["auto", "manual"]);
8023
+ var LinkedDevicesModeSchema = z.enum(["auto", "manual"]);
8787
8024
  /** One resolved linked device — the compact projection consumers need. */
8788
8025
  var LinkedDeviceSchema = z.object({
8789
8026
  deviceId: z.number(),
@@ -8846,8 +8083,6 @@ var DeviceMetaSchema = z.object({
8846
8083
  * accordion sections (with optional intra-section order). See
8847
8084
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
8848
8085
  childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
8849
- /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
8850
- deviceLinks: 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: 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(z.object({
9006
- deviceId: z.number(),
9007
- deviceLinks: z.array(DeviceLinkSchema).readonly()
9008
- }), 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
- /** List the wireable status-schema fields per cap bound to a device.
9036
- * Powers the Wiring tab's field pickers. Caps without a status schema are
9037
- * omitted. Item-array caps (`status.itemArray`, e.g. consumables) also
9038
- * emit their per-item fields tagged `item: true` (a link targeting one
9039
- * must carry a `target.itemKey`) plus the cap-level `itemArray`
9040
- * descriptor. `includeSynthesizable: true` (TARGET pickers only) unions
9041
- * in unbound device-scoped caps that declare `status.empty` and match
9042
- * the device's type so the FIRST link to a synthesize-only cap
9043
- * (consumables on an HA vacuum) can be authored. */
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(z.object({
9045
8270
  deviceId: z.number(),
9046
8271
  includeSynthesizable: 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(z.object({ deviceId: z.number() }), z.object({
9185
- mode: DeviceLinkModeSchema,
8410
+ mode: LinkedDevicesModeSchema,
9186
8411
  devices: z.array(LinkedDeviceSchema)
9187
8412
  })),
9188
8413
  /** Get stream sources for a camera device. */
@@ -9246,11 +8471,7 @@ var deviceManagerCapability = {
9246
8471
  deviceId: z.number(),
9247
8472
  entries: z.array(z.object({
9248
8473
  capName: z.string(),
9249
- kind: z.enum([
9250
- "native",
9251
- "wrapped",
9252
- "linked"
9253
- ]),
8474
+ kind: z.enum(["native", "wrapped"]),
9254
8475
  providerAddonId: z.string(),
9255
8476
  providerNodeId: z.string(),
9256
8477
  nativeAddonId: z.string()
@@ -9268,11 +8489,7 @@ var deviceManagerCapability = {
9268
8489
  deviceId: z.number(),
9269
8490
  entries: z.array(z.object({
9270
8491
  capName: z.string(),
9271
- kind: z.enum([
9272
- "native",
9273
- "wrapped",
9274
- "linked"
9275
- ]),
8492
+ kind: z.enum(["native", "wrapped"]),
9276
8493
  providerAddonId: z.string(),
9277
8494
  providerNodeId: z.string(),
9278
8495
  nativeAddonId: 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(z.object({
@@ -12873,6 +12094,18 @@ var OauthIntegrationDescriptorSchema = 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: z.array(z.string()).min(1),
12097
+ /** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
12098
+ * RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
12099
+ * `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
12100
+ * whose address the hub cannot know in advance (a Home Assistant at
12101
+ * `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
12102
+ * exactly; a public host never satisfies this branch, so it is not a
12103
+ * wildcard prefix by another name. */
12104
+ allowedPrivateHostPaths: z.array(z.string()).optional(),
12105
+ /** When true this is a PUBLIC client (source is published, no secret can be
12106
+ * protected) and PKCE is mandatory: `/authorize` refuses without an S256
12107
+ * `code_challenge`, `/token` refuses without the matching `code_verifier`. */
12108
+ requiresPkce: z.boolean().optional(),
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 = 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 = 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: z.boolean().optional(),
13269
12520
  ...TrackFlagFields,
13270
12521
  ...TrackRetrainFields
13271
12522
  });
@@ -17015,6 +16266,10 @@ var SsoBridgeClaimsSchema = z.object({
17015
16266
  integrationId: z.string().optional(),
17016
16267
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
17017
16268
  jti: z.string().optional(),
16269
+ /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
16270
+ * client. Its PRESENCE is what makes the verifier mandatory at exchange,
16271
+ * so the requirement travels with the code and not with mutable config. */
16272
+ codeChallenge: z.string().optional(),
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: 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(z.object({
17776
17035
  deviceId: z.number(),
@@ -20266,7 +19525,29 @@ var FaceInfoSchema = z.object({
20266
19525
  recognizedIdentityId: z.string().optional(),
20267
19526
  identityName: z.string().optional(),
20268
19527
  assigned: 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: 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: 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(z.object({
20344
19625
  limit: 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: z.boolean().optional()
20346
19643
  }).optional(), z.array(FaceInfoSchema).readonly()),
20347
19644
  getFaceByTrack: method(z.object({
20348
19645
  deviceId: z.number().int(),
@@ -25697,14 +24994,19 @@ var userManagementCapability = {
25697
24994
  username: z.string(),
25698
24995
  scopes: z.array(TokenScopeSchema),
25699
24996
  redirectUri: z.string(),
25700
- hubUrl: z.string()
24997
+ hubUrl: 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: z.string().optional()
25701
25001
  }), z.object({ code: z.string() }), {
25702
25002
  kind: "mutation",
25703
25003
  access: "create"
25704
25004
  }),
25705
25005
  oauthExchangeCode: method(z.object({
25706
25006
  code: z.string(),
25707
- redirectUri: z.string()
25007
+ redirectUri: z.string(),
25008
+ /** PKCE verifier. REQUIRED when the code carries a challenge. */
25009
+ codeVerifier: z.string().optional()
25708
25010
  }), z.object({
25709
25011
  accessToken: z.string(),
25710
25012
  refreshToken: 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.
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 = 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(z.object({
25590
+ deviceId: z.number(),
25591
+ stage: ZoneRuleStageEnum
25592
+ }), 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(z.object({
25598
+ deviceId: z.number(),
25599
+ stage: ZoneRuleStageEnum,
25600
+ rules: z.array(ZoneRuleSchema).readonly()
25601
+ }), 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: z.object({
25623
+ motion: z.array(ZoneRuleSchema).readonly(),
25624
+ detection: z.array(ZoneRuleSchema).readonly(),
25625
+ package: 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.
26253
25667
  *
26254
- * Extend the enum here when a new gating consumer comes online (audio
26255
- * gating, alert filtering, …) no other surface needs to change.
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.
26256
25683
  */
26257
- var ZoneRuleStageEnum = z.enum([
26258
- "motion",
26259
- "detection",
26260
- "package"
26261
- ]);
26262
25684
  /**
26263
- * Zone rules capability per-camera CRUD over the {@link ZoneRule}
26264
- * arrays that decide how each pipeline stage uses the polygon zones.
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 z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
25712
+ return s;
25713
+ }
25714
+ function leafKind(schema) {
25715
+ const s = unwrap(schema);
25716
+ if (s instanceof z.ZodEnum) return {
25717
+ kind: "enum",
25718
+ enumValues: s.options
25719
+ };
25720
+ if (s instanceof z.ZodNumber) return { kind: "number" };
25721
+ if (s instanceof z.ZodString) return { kind: "string" };
25722
+ if (s instanceof 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 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 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".
25765
+ */
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
+ };
25784
+ /**
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
- * Hosted by `addon-pipeline-orchestrator` alongside the zones provider
26267
- * so the operator has a single hub-side source of truth for both
26268
- * geometry and behaviour. Per-stage rules are stored under the
26269
- * `zoneRules.<stage>` key in the orchestrator's per-device store and
26270
- * mirrored to the device-state slice `<stage>-zone-rules` on every
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
- * Sets are bulk-replace the operator UI sends the new rule list
26276
- * wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
26277
- * naturally in the rule editor without per-rule mutation chatter.
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
- var zoneRulesCapability = {
26280
- name: "zone-rules",
26281
- scope: "device",
26282
- mode: "singleton",
26283
- deviceTypes: [DeviceType.Camera],
26284
- methods: {
26285
- /** Read the full rule list for a given stage (empty when no rules
26286
- * are defined yet). */
26287
- listRules: method(z.object({
26288
- deviceId: z.number(),
26289
- stage: ZoneRuleStageEnum
26290
- }), z.array(ZoneRuleSchema).readonly()),
26291
- /** Bulk-replace the rule list for one stage. The provider validates
26292
- * each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
26293
- * thresholds in range) and rejects the whole patch if any entry
26294
- * is invalid partial writes are a configuration footgun. */
26295
- setRules: method(z.object({
26296
- deviceId: z.number(),
26297
- stage: ZoneRuleStageEnum,
26298
- rules: z.array(ZoneRuleSchema).readonly()
26299
- }), z.void(), {
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")
25822
+ },
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
+ }
26303
25874
  },
26304
- /**
26305
- * Runtime-state slice — every stage mirrored together so consumers
26306
- * see one reactive handle (`device.state.zoneRules.value`) instead
26307
- * of one per stage. Bulk-replace mutations on any stage write the full
26308
- * `{motion, detection, package}` shape, so subscribers always get the
26309
- * complete current set. Consumers that only care about one stage
26310
- * just read the matching property.
26311
- *
26312
- * `package` backs the package-drop detector — a package zone is a
26313
- * `ZoneRule` on the `'package'` stage referencing drawn polygons
26314
- * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
26315
- * The orchestrator provider writes this stage as a first-class slice
26316
- * (Phase 4): every mutation mirrors the full `{motion, detection,
26317
- * package}` shape, so consumers read the current package rules directly
26318
- * off `device.state.zoneRules.value.package`.
26319
- */
26320
- runtimeState: z.object({
26321
- motion: z.array(ZoneRuleSchema).readonly(),
26322
- detection: z.array(ZoneRuleSchema).readonly(),
26323
- package: z.array(ZoneRuleSchema).readonly()
26324
- })
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
- * Runtime defaults -- used by ConfigManager.get() for backward compatibility
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
- * Moved from @camstack/system/config-schema to @camstack/types so that
26331
- * @camstack/system can reference it without a cross-package kernel import.
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
- var RUNTIME_DEFAULTS = {
26334
- "features.streaming": true,
26335
- "features.notifications": true,
26336
- "features.objectDetection": false,
26337
- "features.remoteAccess": true,
26338
- "features.agentCluster": false,
26339
- "features.smartHome": true,
26340
- "features.recordings": true,
26341
- "features.backup": true,
26342
- "features.repl": true,
26343
- "retention.detectionEventsDays": 30,
26344
- "retention.audioLevelsDays": 7,
26345
- "logging.level": "info",
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
+ ]);
25944
+ /**
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.
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
+ }
26363
26086
  /**
26364
- * Accessory device helpers shared across drivers.
26087
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
26365
26088
  *
26366
- * Many vendor-specific drivers register accessory child devices on
26367
- * top of a parent (Reolink: siren / floodlight / PIR / autotrack /
26368
- * chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
26369
- * driver picks the right `DeviceType` + `DeviceRole` explicitly when
26370
- * spawning, builds a name derived from the parent, and produces a
26371
- * stableId tied to the parent so boot-restore can reconstruct the
26372
- * relationship.
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.
26373
26095
  *
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.
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`.
26381
26098
  */
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
26114
+ };
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;
26152
+ }
26153
+ matchPunct(punct) {
26154
+ const tok = this.peek();
26155
+ if (tok.type === "punct" && tok.punct === punct) {
26156
+ this.pos += 1;
26157
+ return true;
26158
+ }
26159
+ return false;
26160
+ }
26161
+ countNode() {
26162
+ this.nodeCount += 1;
26163
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
26164
+ }
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
+ };
26178
+ }
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
+ }
26382
26289
  /**
26383
- * Subset of `DeviceRole` values that drivers register as child
26384
- * accessories of a parent device. Sourced verbatim from `DeviceRole`
26385
- * `AccessoryKind` is the alias drivers use when building accessory
26386
- * children, so the call site reads as
26387
- * `accessoryStableId(parent, AccessoryKind.Siren)` rather than
26388
- * `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
26389
- * any role works, including non-accessory ones like Doorbell).
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.
26390
26298
  */
26391
- var AccessoryKind = {
26392
- Siren: DeviceRole.Siren,
26393
- Floodlight: DeviceRole.Floodlight,
26394
- Spotlight: DeviceRole.Spotlight,
26395
- PirSensor: DeviceRole.PirSensor,
26396
- Chime: DeviceRole.Chime,
26397
- Autotrack: DeviceRole.Autotrack,
26398
- Nightvision: DeviceRole.Nightvision,
26399
- PrivacyMask: DeviceRole.PrivacyMask
26400
- };
26401
- AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
26402
- 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;
26403
- new Set(Object.values(DeviceType));
26404
- /** Apply a link transform to a resolved source value. `identity`/undefined
26405
- * pass through; `enum-map` remaps strings (fallback or null when unknown);
26406
- * `linear` does scale*x+offset with optional clamp (null for non-numbers). */
26407
- function applyTransform(value, transform) {
26408
- if (!transform || transform.kind === "identity") return value;
26409
- if (transform.kind === "enum-map") {
26410
- const key = String(value);
26411
- if (Object.prototype.hasOwnProperty.call(transform.mapping, key)) return transform.mapping[key];
26412
- return transform.fallback ?? null;
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;
26413
26306
  }
26414
- if (transform.kind === "linear") {
26415
- if (typeof value !== "number" || Number.isNaN(value)) return null;
26416
- let out = value * transform.scale + transform.offset;
26417
- if (transform.clamp) {
26418
- const min = transform.clamp[0];
26419
- const max = transform.clamp[1];
26420
- out = Math.min(max, Math.max(min, out));
26421
- }
26422
- return out;
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
+ };
26423
26318
  }
26424
- return value;
26425
- }
26426
- DeviceFeature.BatteryOperated;
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];
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);
26439
26323
  }
26440
- return cur;
26441
- }
26442
- /** Immutably set a dot-path on an object, returning a new object. Intermediate
26443
- * objects are created when a segment is missing or null. Arrays are NOT
26444
- * traversed (P1 has no array-index syntax — see `itemKey` grouping in P2): if a
26445
- * segment resolves to an array it is replaced by a fresh object, so the array
26446
- * is discarded — the only caller (`mergeLinkedStatus`) re-validates the result
26447
- * with the target cap's Zod schema and rejects a malformed overlay.
26448
- * The return type `T` preserves the OUTER shape; the rewritten key is not
26449
- * narrowed, so callers should re-validate against the status schema. */
26450
- function setByPath(obj, path, value) {
26451
- const dotIndex = path.indexOf(".");
26452
- if (dotIndex === -1) return {
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 z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
26470
- return s;
26324
+ return result;
26471
26325
  }
26472
- function leafKind(schema) {
26473
- const s = unwrap(schema);
26474
- if (s instanceof z.ZodEnum) return {
26475
- kind: "enum",
26476
- enumValues: s.options
26477
- };
26478
- if (s instanceof z.ZodNumber) return { kind: "number" };
26479
- if (s instanceof z.ZodString) return { kind: "string" };
26480
- if (s instanceof z.ZodBoolean) return { kind: "boolean" };
26481
- return null;
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);
26482
26330
  }
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 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 z.ZodObject) {
26496
- out.push(...enumerateSchemaFields(inner, path));
26497
- continue;
26498
- }
26499
- const leaf = leafKind(field);
26500
- if (leaf) out.push({
26501
- path,
26502
- kind: leaf.kind,
26503
- ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
26504
- });
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`;
26505
26346
  }
26506
- return out;
26507
- }
26508
- /** Enumerate the per-item wireable fields of an item-array cap (see
26509
- * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
26510
- * `keyField` (the key comes from the link's `itemKey`, never from a wired
26511
- * source), each tagged `item: true` so the authoring UI collects an
26512
- * `itemKey` alongside the field. Never throws. */
26513
- function enumerateItemArrayFields(itemArray) {
26514
- return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
26515
- ...f,
26516
- item: true
26517
- }));
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 = z.object({
26377
+ kind: z.literal("field").optional(),
26378
+ sourceKey: z.string(),
26379
+ cap: z.string(),
26380
+ fieldPath: z.string()
26381
+ });
26382
+ /** A constant. No device is read. */
26383
+ var ExpressionLiteralBindingSchema = z.object({
26384
+ kind: z.literal("literal"),
26385
+ value: z.union([
26386
+ z.string(),
26387
+ z.number(),
26388
+ z.boolean(),
26389
+ 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 = z.object({
26395
+ kind: z.literal("global"),
26396
+ sourceStableId: z.string(),
26397
+ cap: z.string(),
26398
+ fieldPath: z.string()
26399
+ });
26400
+ var ExpressionBindingSourceSchema = z.union([
26401
+ ExpressionFieldBindingSchema,
26402
+ ExpressionLiteralBindingSchema,
26403
+ ExpressionGlobalBindingSchema
26404
+ ]);
26405
+ z.object({
26406
+ expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
26407
+ bindings: z.record(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: z.number(),
32251
32148
  updatedAt: 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
  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):
@@ -32524,4 +32421,4 @@ function enumerateInferenceDevices(hw) {
32524
32421
  return out;
32525
32422
  }
32526
32423
  //#endregion
32527
- export { procedureAuthKey as $, enumerateInferenceDevices as A, hydrateSchema as At, isSameAddonId as B, coreBlocksCapability as C, asJsonObject as Ct, deviceStateCapability as D, emitDownForOwnedCaps as Dt, deviceManagerCapability as E, createEvent as Et, filesystemBrowseCapability as F, readinessKey as Ft, logDestinationCapability as G, kebabToCamel as H, getByPath as I, resolveCapMount as It, metricsProviderCapability as J, logLevelAtMost as K, isArrayOutputSchema as L, scopeKey as Lt, enumerateSchemaFields as M, nodePin as Mt, evaluateLinkExpression as N, parseJsonObject as Nt, deviceStatusCapability as O, emitReadiness as Ot, extractNestedAddonId as P, parseJsonUnknown as Pt, platformProbeCapability as Q, isCollectionArrayMethod as R, sleep as Rt, buildStreamParamsConfigSchema as S, WELL_KNOWN_TAB_MAP as St, decodeVectorBase64 as T, asString as Tt, lifecycleJobSchema as U, isVoidInput as V, localNetworkCapability as W, objectInputDeclaresAddonId as X, normalizeUnit as Y, parseStreamParamsFormPatch as Z, alertsCapability as _, DeviceFeature as _t, CAP_NAMES_WITH_STATUS as a, storageProviderCapability as at, backupCapability as b, ReadinessRegistry as bt, METHOD_ACCESS_MAP as c, userManagementCapability as ct, ScopedTokenSchema as d, vectorStoreCapability as dt, scoreRuntimes as et, StorageLocationTypeSchema as f, errMsg as ft, addonWidgetsCapability as g, DEVICE_STATUS_METHOD as gt, addonSettingsCapability as h, DEVICE_SETTINGS_CONTRIBUTION_METHODS as ht, BatteryStatusSchema as i, storageCapability as it, enumerateItemArrayFields as j, isDeviceConfigCap as jt, doorbellCapability as k, expandCapMethods as kt, RUNTIME_DEFAULTS as l, validateExpressionSource as lt, addonPagesCapability as m, DATAPLANE_SECRET_HEADER as mt, AlertSchema as n, settingsStoreCapability as nt, CoreBlockSchema as o, streamQualityLabel as ot, UserRecordSchema as p, BaseAddon as pt, looseSchema as q, ApiKeyRecordSchema as r, snapshotCapability as rt, DeviceStatusSchema as s, toExpressionValue as st, ALL_CAPABILITY_DEFINITIONS as t, setByPath as tt, STREAM_PROFILE_META as u, vectorDimFromBase64 as ut, applyTransform as v, DeviceRole as vt, dataStoreProviderCapability as w, asNumber as wt, bareAddonId as x, ReadinessTimeoutError as xt, authProviderCapability as y, DeviceType as yt, isObjectInput as z, EventCategory as zt };
32424
+ export { platformProbeCapability as $, deviceStatusCapability as A, nodePin as At, isSameAddonId as B, coreBlockAddonId as C, asString as Ct, decodeVectorBase64 as D, expandCapMethods as Dt, dataStoreProviderCapability as E, emitReadiness as Et, extractNestedAddonId as F, scopeKey as Ft, logDestinationCapability as G, kebabToCamel as H, filesystemBrowseCapability as I, sleep as It, metricsProviderCapability as J, logLevelAtMost as K, isArrayOutputSchema as L, EventCategory as Lt, enumerateInferenceDevices as M, parseJsonUnknown as Mt, enumerateItemArrayFields as N, readinessKey as Nt, deviceManagerCapability as O, hydrateSchema as Ot, enumerateSchemaFields as P, resolveCapMount as Pt, parseStreamParamsFormPatch as Q, isCollectionArrayMethod as R, buildStreamParamsConfigSchema as S, asNumber as St, coreBlocksCapability as T, emitDownForOwnedCaps as Tt, lifecycleJobSchema as U, isVoidInput as V, localNetworkCapability as W, oauthIntegrationCapability as X, normalizeUnit as Y, objectInputDeclaresAddonId as Z, addonWidgetsCapability as _, DeviceType as _t, CAP_NAMES_WITH_STATUS as a, storageProviderCapability as at, backupCapability as b, WELL_KNOWN_TAB_MAP as bt, DeviceStatusSchema as c, vectorDimFromBase64 as ct, STREAM_PROFILE_META as d, BaseAddon as dt, procedureAuthKey as et, ScopedTokenSchema as f, DATAPLANE_SECRET_HEADER as ft, addonSettingsCapability as g, DeviceRole as gt, addonPagesCapability as h, DeviceFeature as ht, BatteryStatusSchema as i, storageCapability as it, doorbellCapability as j, parseJsonObject as jt, deviceStateCapability as k, isDeviceConfigCap as kt, METHOD_ACCESS_MAP as l, vectorStoreCapability as lt, UserRecordSchema as m, DEVICE_STATUS_METHOD as mt, AlertSchema as n, settingsStoreCapability as nt, CORE_BLOCK_ADDON_PREFIX as o, streamQualityLabel as ot, StorageLocationTypeSchema as p, DEVICE_SETTINGS_CONTRIBUTION_METHODS as pt, looseSchema as q, ApiKeyRecordSchema as r, snapshotCapability as rt, CoreBlockSchema as s, userManagementCapability as st, ALL_CAPABILITY_DEFINITIONS as t, scoreRuntimes as tt, RUNTIME_DEFAULTS as u, errMsg as ut, alertsCapability as v, ReadinessRegistry as vt, coreBlockIdFromAddonId as w, createEvent as wt, bareAddonId as x, asJsonObject as xt, authProviderCapability as y, ReadinessTimeoutError as yt, isObjectInput as z };