@camstack/system 1.2.24 → 1.2.26

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 (53) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/device-manager/device-aggregation.d.ts +18 -0
  14. package/dist/builtins/device-manager/device-manager.addon.js +47 -1
  15. package/dist/builtins/device-manager/device-manager.addon.mjs +47 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  18. package/dist/builtins/hub-forwarder/index.js +1 -1
  19. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  20. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  22. package/dist/builtins/local-network/local-network.addon.js +1 -1
  23. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  24. package/dist/builtins/loki-logging/index.js +1 -1
  25. package/dist/builtins/loki-logging/index.mjs +1 -1
  26. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  28. package/dist/builtins/platform-probe/index.js +1 -1
  29. package/dist/builtins/platform-probe/index.mjs +1 -1
  30. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  32. package/dist/builtins/snapshot/index.js +1 -1
  33. package/dist/builtins/snapshot/index.mjs +1 -1
  34. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  35. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  36. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  38. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +0 -19
  39. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +143 -67
  40. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +143 -67
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +67 -7
  42. package/dist/builtins/system-config/system-config.addon.js +1 -1
  43. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  44. package/dist/builtins/winston-logging/index.js +1 -1
  45. package/dist/builtins/winston-logging/index.mjs +1 -1
  46. package/dist/{dist-CS0pdC8l.js → dist-Cu72wsz5.js} +132 -3
  47. package/dist/{dist-CIek6F9X.mjs → dist-DimX9eZy.mjs} +132 -3
  48. package/dist/index.js +2 -2
  49. package/dist/index.mjs +2 -2
  50. package/dist/kernel/moleculer/settings-read-guard.d.ts +49 -0
  51. package/dist/{manifest-python-deps-BbOZZbVt.js → manifest-python-deps-COgal7Jy.js} +68 -17
  52. package/dist/{manifest-python-deps-C9qhvMUi.mjs → manifest-python-deps-julrfvyf.mjs} +68 -17
  53. package/package.json +1 -1
@@ -37,6 +37,21 @@ export type StorageProviderLookup = () => readonly IStorageProviderImpl[];
37
37
  * remote), the safe default for the early-boot / in-memory path.
38
38
  */
39
39
  export type NodeLocalResolver = (providerId: string) => boolean | undefined;
40
+ /**
41
+ * Where a single dispatch must execute: which `storage-provider`, on which
42
+ * node. `providerId` alone is NOT a routing key — every node's filesystem
43
+ * provider registers under the same id, so a target without its `nodeId`
44
+ * silently resolves to whichever node asked (the hub).
45
+ *
46
+ * `nodeId` absent = node-agnostic (S3/SFTP/WebDAV): reachable from anywhere,
47
+ * so the local provider runs it.
48
+ */
49
+ export interface StorageDispatchTarget {
50
+ readonly providerId: string;
51
+ readonly nodeId?: string;
52
+ }
53
+ /** The two chunked-I/O session families the orchestrator tracks. */
54
+ export type StorageSessionKind = 'upload' | 'download';
40
55
  export declare class StorageOrchestratorService {
41
56
  private readonly logger;
42
57
  private readonly getProviders;
@@ -54,8 +69,6 @@ export declare class StorageOrchestratorService {
54
69
  * the provider appears. See {@link attachStore}.
55
70
  */
56
71
  private locationStore;
57
- /** This node's id — locations pinned to OTHER nodes dispatch remotely. */
58
- private readonly localNodeId;
59
72
  /** Cross-node provider resolver (hub wiring: a node-routed capability
60
73
  * proxy over `storage-provider`). Absent → remote locations error. */
61
74
  private readonly getRemoteProvider?;
@@ -76,6 +89,15 @@ export declare class StorageOrchestratorService {
76
89
  * early-boot path that predates the resolver wiring.
77
90
  */
78
91
  private nodeLocalResolver;
92
+ /** `localNodeId` with any forked-child `/addon` suffix stripped. */
93
+ private readonly localNode;
94
+ /**
95
+ * `<kind>:<sessionId>` → the dispatch target that OPENED the session.
96
+ * Chunked I/O carries no `StorageLocation` after the first hop, so without
97
+ * this the follow-up chunks fall back to a `providerId`-only lookup — which
98
+ * on a multi-node cluster means the hub's filesystem, not the agent's.
99
+ */
100
+ private readonly sessionTargets;
79
101
  constructor(logger: IScopedLogger, getProviders: StorageProviderLookup,
80
102
  /**
81
103
  * Optional persistence backend. When supplied, `initialize()`
@@ -96,6 +118,12 @@ export declare class StorageOrchestratorService {
96
118
  /** Cross-node provider resolver (hub wiring: a node-routed capability
97
119
  * proxy over `storage-provider`). Absent → remote locations error. */
98
120
  getRemoteProvider?: ((nodeId: string) => IStorageProviderImpl | null) | undefined);
121
+ /**
122
+ * The node this orchestrator runs on, with any forked-child `/addon`
123
+ * suffix stripped. A location whose `nodeId` differs is NOT this node's
124
+ * to touch — its bytes, and its free space, live elsewhere.
125
+ */
126
+ getLocalNodeId(): string;
99
127
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
100
128
  hasStore(): boolean;
101
129
  /**
@@ -210,13 +238,45 @@ export declare class StorageOrchestratorService {
210
238
  */
211
239
  resolveRef(ref: StorageLocationRef): StorageLocation;
212
240
  /**
213
- * Find the storage-provider that backs a given location. Lookup is by
214
- * `location.providerId` against `getProviderInfo().providerId` from
215
- * each registered collection provider. Throws with both the missing
216
- * providerId and the offending location id so the operator can pick
217
- * the right place to fix the config.
241
+ * Find the storage-provider that backs a given location the entry point
242
+ * every consumer-facing `storage` cap method funnels through.
243
+ *
244
+ * Routing key is `(providerId, nodeId)`, never `providerId` alone: every
245
+ * node's FilesystemStorageProvider registers under the SAME providerId, so
246
+ * a providerId-only match resolves to whichever node asked. That is how a
247
+ * location created on an agent ended up writing to the HUB's filesystem
248
+ * (root-caused 2026-07-29).
218
249
  */
219
250
  getProviderFor(location: StorageLocation): Promise<IStorageProviderImpl>;
251
+ /**
252
+ * Resolve one dispatch target to the provider that must execute it.
253
+ *
254
+ * `descriptor` is the operator-facing subject of the error message
255
+ * (`location "recordings:disk"`, `uploadId "u-42"`); `locationId` is the
256
+ * structured tag for the log line when there is a location behind the
257
+ * dispatch (chunked sessions carry only the session id).
258
+ *
259
+ * Both failure branches LOG before throwing, and NEITHER falls back to the
260
+ * local provider — a silent fallback is the bug this method exists to fix.
261
+ */
262
+ private getProviderForTarget;
263
+ /**
264
+ * Remember which provider — and on which NODE — opened a chunked session.
265
+ * `beginUpload` / `beginDownload` carry a `StorageLocation`; every later
266
+ * chunk carries only the session id, and a session id issued by an agent
267
+ * means nothing to the hub's provider.
268
+ */
269
+ rememberSession(kind: StorageSessionKind, sessionId: string, location: StorageLocation): void;
270
+ /** Drop a session's routing entry (finalize / abort / end). */
271
+ forgetSession(kind: StorageSessionKind, sessionId: string): void;
272
+ /** Drop every session entry (addon shutdown). */
273
+ clearSessions(): void;
274
+ /**
275
+ * Resolve the provider that owns an in-flight chunked session, on the node
276
+ * that opened it. Throws when the orchestrator has no record of the open
277
+ * call (provider gone, or a chunk for a session it never issued).
278
+ */
279
+ getProviderForSession(kind: StorageSessionKind, sessionId: string): Promise<IStorageProviderImpl>;
220
280
  /** Convenience: lookup by id (returns `undefined` if not found). */
221
281
  getLocationById(id: string): StorageLocation | undefined;
222
282
  /**
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CS0pdC8l.js");
6
+ const require_dist = require("../../dist-Cu72wsz5.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-CIek6F9X.mjs";
1
+ import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-DimX9eZy.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CS0pdC8l.js");
6
+ const require_dist = require("../../dist-Cu72wsz5.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { at as BaseAddon, z as logDestinationCapability } from "../../dist-CIek6F9X.mjs";
1
+ import { at as BaseAddon, z as logDestinationCapability } from "../../dist-DimX9eZy.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -1,5 +1,5 @@
1
1
  let zod = require("zod");
2
- //#region ../types/dist/event-category-BLcNejAE.mjs
2
+ //#region ../types/dist/event-category-Bz24uP1U.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -270,6 +270,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
270
270
  */
271
271
  EventCategory["DeviceStateChanged"] = "device.state-changed";
272
272
  /**
273
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
274
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
275
+ *
276
+ * Emitted only on a change, so a steady scene is silent. It exists so a
277
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
278
+ * one live badge with no push signal at all, and it cost a request every
279
+ * four seconds per visible camera.
280
+ *
281
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
282
+ * keeps a slow reconcile rather than trusting it alone.
283
+ */
284
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
285
+ /**
273
286
  * Cap event fired by every device that registers the `battery`
274
287
  * capability. Mirrors the cap definition's `onStatusChanged`. Carries
275
288
  * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
@@ -5326,6 +5339,29 @@ var NcOccupancyConditionSchema = zod.z.object({
5326
5339
  count: zod.z.number().int().min(0).default(1),
5327
5340
  sustainSeconds: zod.z.number().int().min(0).max(3600).default(15)
5328
5341
  });
5342
+ /**
5343
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
5344
+ *
5345
+ * The values are not symmetric, and deliberately so — the absent value has to
5346
+ * mean exactly what every rule authored before this condition existed already
5347
+ * does:
5348
+ * - `enter` — entries and every NON-crossing record (movement state,
5349
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
5350
+ * an operator who never asked for exits must not start receiving them.
5351
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
5352
+ * fails closed, because "the car left the drive" is a question about a
5353
+ * boundary, not about a detection.
5354
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
5355
+ *
5356
+ * A rule asking for a direction should normally also scope `zones`, which the
5357
+ * engine evaluates against the crossed zone as well as the current membership
5358
+ * (an exit's membership no longer contains the zone it just left).
5359
+ */
5360
+ var NcCrossingSchema = zod.z.enum([
5361
+ "enter",
5362
+ "exit",
5363
+ "any"
5364
+ ]);
5329
5365
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
5330
5366
  var NcZoneConditionSchema = zod.z.object({
5331
5367
  ids: zod.z.array(zod.z.string().min(1)).min(1),
@@ -5350,6 +5386,13 @@ var NcConditionsSchema = zod.z.object({
5350
5386
  /** Veto zones — any hit fails the rule. */
5351
5387
  zonesExclude: zod.z.array(zod.z.string().min(1)).optional(),
5352
5388
  /**
5389
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
5390
+ * and a closed track carries none, so a `track-end` rule asking for one
5391
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
5392
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
5393
+ */
5394
+ crossing: NcCrossingSchema.optional(),
5395
+ /**
5353
5396
  * Exact (case-insensitive) match on the record's collapsed `label`
5354
5397
  * (identity name / plate text / subclass).
5355
5398
  */
@@ -5538,11 +5581,31 @@ var NcMediaPolicySchema = zod.z.object({
5538
5581
  */
5539
5582
  profile: CamProfileSchema.optional()
5540
5583
  });
5584
+ /**
5585
+ * Cooldown GRANULARITY over the subject's class — how much a fired
5586
+ * notification suppresses.
5587
+ * - `shared` (default, and the absent value) — one window for the whole
5588
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
5589
+ * - `per-class` — an independent window per detected class, so cat→dog fires
5590
+ * at once and cat→cat still waits.
5591
+ *
5592
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
5593
+ * must not be swallowed by a bark's window (the precedent this generalizes —
5594
+ * see `cooldownKey` in the rule engine).
5595
+ */
5596
+ var NcThrottleGranularitySchema = zod.z.enum(["shared", "per-class"]);
5541
5597
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
5542
5598
  var NcThrottleSchema = zod.z.object({
5543
5599
  cooldownSec: zod.z.number().int().min(0).max(86400).default(60),
5544
5600
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
5545
- scope: zod.z.enum(["rule", "rule-device"]).default("rule-device")
5601
+ scope: zod.z.enum(["rule", "rule-device"]).default("rule-device"),
5602
+ /**
5603
+ * Class granularity of the cooldown key. Optional rather than defaulted:
5604
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
5605
+ * rule authored before this field simply carries none — and the engine
5606
+ * reads absent as `shared`, the pre-existing behaviour.
5607
+ */
5608
+ granularity: NcThrottleGranularitySchema.optional()
5546
5609
  });
5547
5610
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
5548
5611
  var NcRuleInputSchema = zod.z.object({
@@ -5650,6 +5713,7 @@ var NcConditionDescriptorSchema = zod.z.object({
5650
5713
  "schedule",
5651
5714
  "plateMatcher",
5652
5715
  "packagePhase",
5716
+ "crossingSelect",
5653
5717
  "polygonDraw",
5654
5718
  "occupancy"
5655
5719
  ]),
@@ -15673,7 +15737,29 @@ var deviceManagerCapability = {
15673
15737
  getDeviceStatusAggregate: method(zod.z.object({
15674
15738
  deviceId: zod.z.number(),
15675
15739
  caps: zod.z.array(zod.z.string()).readonly().optional()
15676
- }), zod.z.record(zod.z.string(), zod.z.unknown().nullable()))
15740
+ }), zod.z.record(zod.z.string(), zod.z.unknown().nullable())),
15741
+ /**
15742
+ * The same aggregate, for a LIST of devices in one round-trip.
15743
+ *
15744
+ * The viewer's camera list is the caller this exists for. Each card asked
15745
+ * for its own badges — battery here, recording mode there — so a ten-camera
15746
+ * install issued N separate procedures per reconcile, each its own message
15747
+ * on the one WebSocket that also carries the WebRTC signalling (the client
15748
+ * has no `httpBatchLink`, so there is no transport-level batching to lean
15749
+ * on: collapsing has to happen at the procedure level, which is here).
15750
+ *
15751
+ * Keyed by device id as a STRING — a JSON object cannot key by number, and
15752
+ * pretending otherwise is how a record survives the wire only in tests.
15753
+ * The per-device value is exactly what `getDeviceStatusAggregate` returns,
15754
+ * so a caller can move between the two without reshaping anything.
15755
+ *
15756
+ * `caps` matters more here than in the single call: a client that needs two
15757
+ * caps should not pay for every status-carrying cap × every device.
15758
+ */
15759
+ getDeviceStatusAggregateBatch: method(zod.z.object({
15760
+ deviceIds: zod.z.array(zod.z.number()).readonly(),
15761
+ caps: zod.z.array(zod.z.string()).readonly().optional()
15762
+ }), zod.z.record(zod.z.string(), zod.z.record(zod.z.string(), zod.z.unknown().nullable())))
15677
15763
  }
15678
15764
  };
15679
15765
  /**
@@ -17399,6 +17485,37 @@ var MotionEventSchema = zod.z.object({
17399
17485
  * Absent on legacy rows ⇒ treat as `pipeline`.
17400
17486
  */
17401
17487
  var DetectionSourceSchema = zod.z.enum(["pipeline", "onboard"]);
17488
+ /**
17489
+ * Which WAY a subject crossed a zone boundary. An entry and an exit are two
17490
+ * different notifications ("the parcel arrived" / "the car left the drive"),
17491
+ * and until this existed they were indistinguishable on the persisted record —
17492
+ * which is why exits were computed and dropped at the emit boundary rather
17493
+ * than doubling every zone-scoped rule.
17494
+ */
17495
+ var ZoneCrossingDirectionSchema = zod.z.enum(["enter", "exit"]);
17496
+ /**
17497
+ * The confirmed zone crossing that produced an object event. Present ONLY on
17498
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
17499
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
17500
+ * appearance event carry none, so a rule asking for a direction fails closed
17501
+ * on them.
17502
+ *
17503
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
17504
+ * into its own event, so a frame in which a track enters A while leaving B
17505
+ * produces two events with two directions — never one ambiguous row.
17506
+ *
17507
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
17508
+ * membership the box has NOW, and by definition it no longer contains the zone
17509
+ * that was just left. Without the id here, a zone-scoped rule could never match
17510
+ * the exit it asked for.
17511
+ */
17512
+ var ZoneCrossingSchema = zod.z.object({
17513
+ direction: ZoneCrossingDirectionSchema,
17514
+ /** Admin zone id crossed. */
17515
+ zoneId: zod.z.string(),
17516
+ /** Zone display name at crossing time (falls back to the id). */
17517
+ zoneName: zod.z.string().optional()
17518
+ });
17402
17519
  var ObjectEventSchema = zod.z.object({
17403
17520
  ...BaseEventFields,
17404
17521
  kind: zod.z.literal("object"),
@@ -17425,6 +17542,12 @@ var ObjectEventSchema = zod.z.object({
17425
17542
  zones: zod.z.array(zod.z.string()).readonly().optional(),
17426
17543
  /** Omitted in slim projection. */
17427
17544
  state: TrackStateSchema.optional(),
17545
+ /**
17546
+ * The zone crossing this event IS, when it is one. Absent on every other
17547
+ * event kind (movement state, appearance, package) — see
17548
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
17549
+ */
17550
+ zoneCrossing: ZoneCrossingSchema.optional(),
17428
17551
  /** Detection-frame dimensions in pixels — let consumers normalize the
17429
17552
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
17430
17553
  frameWidth: zod.z.number().optional(),
@@ -24958,6 +25081,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
24958
25081
  addonId: null,
24959
25082
  access: "view"
24960
25083
  },
25084
+ "deviceManager.getDeviceStatusAggregateBatch": {
25085
+ capName: "device-manager",
25086
+ capScope: "system",
25087
+ addonId: null,
25088
+ access: "view"
25089
+ },
24961
25090
  "deviceManager.getLinkedDevices": {
24962
25091
  capName: "device-manager",
24963
25092
  capScope: "system",
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- //#region ../types/dist/event-category-BLcNejAE.mjs
2
+ //#region ../types/dist/event-category-Bz24uP1U.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -270,6 +270,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
270
270
  */
271
271
  EventCategory["DeviceStateChanged"] = "device.state-changed";
272
272
  /**
273
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
274
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
275
+ *
276
+ * Emitted only on a change, so a steady scene is silent. It exists so a
277
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
278
+ * one live badge with no push signal at all, and it cost a request every
279
+ * four seconds per visible camera.
280
+ *
281
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
282
+ * keeps a slow reconcile rather than trusting it alone.
283
+ */
284
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
285
+ /**
273
286
  * Cap event fired by every device that registers the `battery`
274
287
  * capability. Mirrors the cap definition's `onStatusChanged`. Carries
275
288
  * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
@@ -5326,6 +5339,29 @@ var NcOccupancyConditionSchema = z.object({
5326
5339
  count: z.number().int().min(0).default(1),
5327
5340
  sustainSeconds: z.number().int().min(0).max(3600).default(15)
5328
5341
  });
5342
+ /**
5343
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
5344
+ *
5345
+ * The values are not symmetric, and deliberately so — the absent value has to
5346
+ * mean exactly what every rule authored before this condition existed already
5347
+ * does:
5348
+ * - `enter` — entries and every NON-crossing record (movement state,
5349
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
5350
+ * an operator who never asked for exits must not start receiving them.
5351
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
5352
+ * fails closed, because "the car left the drive" is a question about a
5353
+ * boundary, not about a detection.
5354
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
5355
+ *
5356
+ * A rule asking for a direction should normally also scope `zones`, which the
5357
+ * engine evaluates against the crossed zone as well as the current membership
5358
+ * (an exit's membership no longer contains the zone it just left).
5359
+ */
5360
+ var NcCrossingSchema = z.enum([
5361
+ "enter",
5362
+ "exit",
5363
+ "any"
5364
+ ]);
5329
5365
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
5330
5366
  var NcZoneConditionSchema = z.object({
5331
5367
  ids: z.array(z.string().min(1)).min(1),
@@ -5350,6 +5386,13 @@ var NcConditionsSchema = z.object({
5350
5386
  /** Veto zones — any hit fails the rule. */
5351
5387
  zonesExclude: z.array(z.string().min(1)).optional(),
5352
5388
  /**
5389
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
5390
+ * and a closed track carries none, so a `track-end` rule asking for one
5391
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
5392
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
5393
+ */
5394
+ crossing: NcCrossingSchema.optional(),
5395
+ /**
5353
5396
  * Exact (case-insensitive) match on the record's collapsed `label`
5354
5397
  * (identity name / plate text / subclass).
5355
5398
  */
@@ -5538,11 +5581,31 @@ var NcMediaPolicySchema = z.object({
5538
5581
  */
5539
5582
  profile: CamProfileSchema.optional()
5540
5583
  });
5584
+ /**
5585
+ * Cooldown GRANULARITY over the subject's class — how much a fired
5586
+ * notification suppresses.
5587
+ * - `shared` (default, and the absent value) — one window for the whole
5588
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
5589
+ * - `per-class` — an independent window per detected class, so cat→dog fires
5590
+ * at once and cat→cat still waits.
5591
+ *
5592
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
5593
+ * must not be swallowed by a bark's window (the precedent this generalizes —
5594
+ * see `cooldownKey` in the rule engine).
5595
+ */
5596
+ var NcThrottleGranularitySchema = z.enum(["shared", "per-class"]);
5541
5597
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
5542
5598
  var NcThrottleSchema = z.object({
5543
5599
  cooldownSec: z.number().int().min(0).max(86400).default(60),
5544
5600
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
5545
- scope: z.enum(["rule", "rule-device"]).default("rule-device")
5601
+ scope: z.enum(["rule", "rule-device"]).default("rule-device"),
5602
+ /**
5603
+ * Class granularity of the cooldown key. Optional rather than defaulted:
5604
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
5605
+ * rule authored before this field simply carries none — and the engine
5606
+ * reads absent as `shared`, the pre-existing behaviour.
5607
+ */
5608
+ granularity: NcThrottleGranularitySchema.optional()
5546
5609
  });
5547
5610
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
5548
5611
  var NcRuleInputSchema = z.object({
@@ -5650,6 +5713,7 @@ var NcConditionDescriptorSchema = z.object({
5650
5713
  "schedule",
5651
5714
  "plateMatcher",
5652
5715
  "packagePhase",
5716
+ "crossingSelect",
5653
5717
  "polygonDraw",
5654
5718
  "occupancy"
5655
5719
  ]),
@@ -15673,7 +15737,29 @@ var deviceManagerCapability = {
15673
15737
  getDeviceStatusAggregate: method(z.object({
15674
15738
  deviceId: z.number(),
15675
15739
  caps: z.array(z.string()).readonly().optional()
15676
- }), z.record(z.string(), z.unknown().nullable()))
15740
+ }), z.record(z.string(), z.unknown().nullable())),
15741
+ /**
15742
+ * The same aggregate, for a LIST of devices in one round-trip.
15743
+ *
15744
+ * The viewer's camera list is the caller this exists for. Each card asked
15745
+ * for its own badges — battery here, recording mode there — so a ten-camera
15746
+ * install issued N separate procedures per reconcile, each its own message
15747
+ * on the one WebSocket that also carries the WebRTC signalling (the client
15748
+ * has no `httpBatchLink`, so there is no transport-level batching to lean
15749
+ * on: collapsing has to happen at the procedure level, which is here).
15750
+ *
15751
+ * Keyed by device id as a STRING — a JSON object cannot key by number, and
15752
+ * pretending otherwise is how a record survives the wire only in tests.
15753
+ * The per-device value is exactly what `getDeviceStatusAggregate` returns,
15754
+ * so a caller can move between the two without reshaping anything.
15755
+ *
15756
+ * `caps` matters more here than in the single call: a client that needs two
15757
+ * caps should not pay for every status-carrying cap × every device.
15758
+ */
15759
+ getDeviceStatusAggregateBatch: method(z.object({
15760
+ deviceIds: z.array(z.number()).readonly(),
15761
+ caps: z.array(z.string()).readonly().optional()
15762
+ }), z.record(z.string(), z.record(z.string(), z.unknown().nullable())))
15677
15763
  }
15678
15764
  };
15679
15765
  /**
@@ -17399,6 +17485,37 @@ var MotionEventSchema = z.object({
17399
17485
  * Absent on legacy rows ⇒ treat as `pipeline`.
17400
17486
  */
17401
17487
  var DetectionSourceSchema = z.enum(["pipeline", "onboard"]);
17488
+ /**
17489
+ * Which WAY a subject crossed a zone boundary. An entry and an exit are two
17490
+ * different notifications ("the parcel arrived" / "the car left the drive"),
17491
+ * and until this existed they were indistinguishable on the persisted record —
17492
+ * which is why exits were computed and dropped at the emit boundary rather
17493
+ * than doubling every zone-scoped rule.
17494
+ */
17495
+ var ZoneCrossingDirectionSchema = z.enum(["enter", "exit"]);
17496
+ /**
17497
+ * The confirmed zone crossing that produced an object event. Present ONLY on
17498
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
17499
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
17500
+ * appearance event carry none, so a rule asking for a direction fails closed
17501
+ * on them.
17502
+ *
17503
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
17504
+ * into its own event, so a frame in which a track enters A while leaving B
17505
+ * produces two events with two directions — never one ambiguous row.
17506
+ *
17507
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
17508
+ * membership the box has NOW, and by definition it no longer contains the zone
17509
+ * that was just left. Without the id here, a zone-scoped rule could never match
17510
+ * the exit it asked for.
17511
+ */
17512
+ var ZoneCrossingSchema = z.object({
17513
+ direction: ZoneCrossingDirectionSchema,
17514
+ /** Admin zone id crossed. */
17515
+ zoneId: z.string(),
17516
+ /** Zone display name at crossing time (falls back to the id). */
17517
+ zoneName: z.string().optional()
17518
+ });
17402
17519
  var ObjectEventSchema = z.object({
17403
17520
  ...BaseEventFields,
17404
17521
  kind: z.literal("object"),
@@ -17425,6 +17542,12 @@ var ObjectEventSchema = z.object({
17425
17542
  zones: z.array(z.string()).readonly().optional(),
17426
17543
  /** Omitted in slim projection. */
17427
17544
  state: TrackStateSchema.optional(),
17545
+ /**
17546
+ * The zone crossing this event IS, when it is one. Absent on every other
17547
+ * event kind (movement state, appearance, package) — see
17548
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
17549
+ */
17550
+ zoneCrossing: ZoneCrossingSchema.optional(),
17428
17551
  /** Detection-frame dimensions in pixels — let consumers normalize the
17429
17552
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
17430
17553
  frameWidth: z.number().optional(),
@@ -24958,6 +25081,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
24958
25081
  addonId: null,
24959
25082
  access: "view"
24960
25083
  },
25084
+ "deviceManager.getDeviceStatusAggregateBatch": {
25085
+ capName: "device-manager",
25086
+ capScope: "system",
25087
+ addonId: null,
25088
+ access: "view"
25089
+ },
24961
25090
  "deviceManager.getLinkedDevices": {
24962
25091
  capName: "device-manager",
24963
25092
  capScope: "system",
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-CS0pdC8l.js");
3
+ const require_dist = require("./dist-Cu72wsz5.js");
4
4
  const require_model_download_service = require("./model-download-service-hf0ookyy.js");
5
- const require_manifest_python_deps = require("./manifest-python-deps-BbOZZbVt.js");
5
+ const require_manifest_python_deps = require("./manifest-python-deps-COgal7Jy.js");
6
6
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
7
7
  const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
8
8
  const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
- import { B as logLevelAtMost, Dt as readinessKey, Et as parseJsonUnknown$1, F as isVoidInput, I as kebabToCamel, L as lifecycleJobSchema, M as isArrayOutputSchema, N as isCollectionArrayMethod, Ot as resolveCapMount, P as isObjectInput, Tt as parseJsonObject, V as looseSchema, W as objectInputDeclaresAddonId, _t as asString$1, c as RUNTIME_DEFAULTS, ct as DEVICE_STATUS_METHOD, ft as ReadinessRegistry, gt as asNumber, ht as asJsonObject$1, it as errMsg$1, jt as EventCategory$1, k as extractNestedAddonId, kt as scopeKey, m as addonSettingsCapability, ot as DATAPLANE_SECRET_HEADER$1, pt as ReadinessTimeoutError, q as procedureAuthKey, s as METHOD_ACCESS_MAP, st as DEVICE_SETTINGS_CONTRIBUTION_METHODS, t as ALL_CAPABILITY_DEFINITIONS, vt as createEvent, xt as expandCapMethods, yt as emitDownForOwnedCaps } from "./dist-CIek6F9X.mjs";
2
+ import { B as logLevelAtMost, Dt as readinessKey, Et as parseJsonUnknown$1, F as isVoidInput, I as kebabToCamel, L as lifecycleJobSchema, M as isArrayOutputSchema, N as isCollectionArrayMethod, Ot as resolveCapMount, P as isObjectInput, Tt as parseJsonObject, V as looseSchema, W as objectInputDeclaresAddonId, _t as asString$1, c as RUNTIME_DEFAULTS, ct as DEVICE_STATUS_METHOD, ft as ReadinessRegistry, gt as asNumber, ht as asJsonObject$1, it as errMsg$1, jt as EventCategory$1, k as extractNestedAddonId, kt as scopeKey, m as addonSettingsCapability, ot as DATAPLANE_SECRET_HEADER$1, pt as ReadinessTimeoutError, q as procedureAuthKey, s as METHOD_ACCESS_MAP, st as DEVICE_SETTINGS_CONTRIBUTION_METHODS, t as ALL_CAPABILITY_DEFINITIONS, vt as createEvent, xt as expandCapMethods, yt as emitDownForOwnedCaps } from "./dist-DimX9eZy.mjs";
3
3
  import { a as downloadModel, c as getModelFilePath, d as contentTypeFor, f as createAuthenticatedFileServer, h as resolveFilePath, i as downloadFile, l as isModelDownloaded, m as parseTokenizedUrl, n as collectModelFiles, o as ensureModel, p as parseRangeHeader, r as deleteModelFromDisk, s as fetchJson, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-Cp9f4dk6.mjs";
4
- import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-C9qhvMUi.mjs";
4
+ import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-julrfvyf.mjs";
5
5
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
6
6
  import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
7
7
  import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Read-modify-write guard for the addon settings stores.
3
+ *
4
+ * ## The data loss this prevents
5
+ *
6
+ * Every settings mutation is `read current → merge patch → write back`. The
7
+ * read used to answer `{}` for BOTH "this addon has no settings yet" and "the
8
+ * read failed", so a failed read merged the patch onto nothing and the write
9
+ * persisted a blob containing ONLY the patch. Everything else in it was gone.
10
+ *
11
+ * It happened on 2026-07-31. The UDS transport stalled, `settings write failed
12
+ * (addon-settings/root) — transport error ... UDS request timed out after
13
+ * 60000ms` appeared in the log, and the recording configuration for all three
14
+ * recording cameras came back as `{"enabled": false, "bands": []}`. Two and a
15
+ * half hours of footage were never written, and nothing looked broken: an
16
+ * emptied config is indistinguishable from an operator switching recording off.
17
+ *
18
+ * The rule this module enforces: **a read that FAILED is not an empty value.**
19
+ * A miss still merges onto `{}` — that is a real empty. A failure refuses the
20
+ * write and says so, leaving the stored blob untouched so the next successful
21
+ * write starts from the truth.
22
+ */
23
+ /**
24
+ * Outcome of a settings read. `missing` and `failed` are deliberately distinct
25
+ * — collapsing them is the bug.
26
+ */
27
+ export type SettingsRead = {
28
+ readonly kind: 'value';
29
+ readonly value: Record<string, unknown>;
30
+ } | {
31
+ readonly kind: 'missing';
32
+ } | {
33
+ readonly kind: 'failed';
34
+ readonly error: string;
35
+ };
36
+ export interface ReadModifyWriteInput {
37
+ readonly read: () => Promise<SettingsRead>;
38
+ readonly write: (next: Record<string, unknown>) => Promise<void>;
39
+ readonly patch: Record<string, unknown>;
40
+ /** Called instead of writing when the read failed — always log this. */
41
+ readonly onRefused: (error: string) => void;
42
+ }
43
+ /**
44
+ * Merge `patch` over the current value and write it — unless the read failed,
45
+ * in which case write NOTHING.
46
+ *
47
+ * Returns whether the write was attempted, so a caller can surface the refusal.
48
+ */
49
+ export declare function readModifyWrite(input: ReadModifyWriteInput): Promise<boolean>;