@camstack/system 1.2.25 → 1.2.27

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-CIek6F9X.mjs → dist-BXmFvn3h.mjs} +174 -6
  47. package/dist/{dist-CS0pdC8l.js → dist-BxtIzexq.js} +174 -6
  48. package/dist/index.js +2 -2
  49. package/dist/index.mjs +2 -2
  50. package/dist/kernel/moleculer/readiness-context.d.ts +2 -2
  51. package/dist/{manifest-python-deps-julrfvyf.mjs → manifest-python-deps-BNOApgK0.mjs} +4 -4
  52. package/dist/{manifest-python-deps-COgal7Jy.js → manifest-python-deps-KZ00cbfP.js} +4 -4
  53. package/package.json +2 -2
@@ -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-BxtIzexq.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-BXmFvn3h.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-BxtIzexq.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-BXmFvn3h.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
  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,
@@ -1849,6 +1862,7 @@ var ReadinessRegistry = class {
1849
1862
  logger;
1850
1863
  now;
1851
1864
  generation;
1865
+ verboseDeviceScopeLogs;
1852
1866
  snapshot = /* @__PURE__ */ new Map();
1853
1867
  subscriptions = /* @__PURE__ */ new Set();
1854
1868
  unsubscribeBus;
@@ -1859,6 +1873,7 @@ var ReadinessRegistry = class {
1859
1873
  this.logger = options.logger;
1860
1874
  this.now = options.now ?? (() => Date.now());
1861
1875
  this.generation = options.generation ?? randomGeneration();
1876
+ this.verboseDeviceScopeLogs = options.verboseDeviceScopeLogs ?? false;
1862
1877
  this.unsubscribeBus = this.bus.subscribe({ category: "system.ready-state" }, (event) => this.ingest(event.data));
1863
1878
  this.unsubscribeAgentOffline = this.bus.subscribe({ category: "agent.offline" }, (event) => this.synthesizeDownForNode(event.data.agentId));
1864
1879
  if (typeof this.bus.getRecent === "function") try {
@@ -1912,9 +1927,16 @@ var ReadinessRegistry = class {
1912
1927
  * Every applied entry dispatches a one-shot transition to matching
1913
1928
  * subscriptions so pending `awaitReady` callers unblock without having
1914
1929
  * to wait for a fresh event.
1930
+ *
1931
+ * Returns the number of records APPLIED (added or advanced). Callers
1932
+ * doing periodic reconciles use this to stay silent when the snapshot
1933
+ * changed nothing — the every-60s "hydrated 1446 record(s)" heartbeat
1934
+ * from every child process was pure noise (≈2 800 lines/hour observed
1935
+ * live on 2026-08-01).
1915
1936
  */
1916
1937
  hydrate(records) {
1917
1938
  const now = this.now();
1939
+ let applied = 0;
1918
1940
  for (const record of records) {
1919
1941
  const key = readinessKey(record.capName, record.scope);
1920
1942
  const prev = this.snapshot.get(key) ?? null;
@@ -1940,7 +1962,8 @@ var ReadinessRegistry = class {
1940
1962
  sourceNodeId: record.sourceNodeId
1941
1963
  };
1942
1964
  this.snapshot.set(key, hydrated);
1943
- if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated${prev !== null ? " update" : ""}, gen=${record.generation.slice(0, 6)})`);
1965
+ applied++;
1966
+ this.logPerRecord(record.scope, `readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated${prev !== null ? " update" : ""}, gen=${record.generation.slice(0, 6)})`);
1944
1967
  const transition = {
1945
1968
  capName: record.capName,
1946
1969
  scope: record.scope,
@@ -1961,6 +1984,23 @@ var ReadinessRegistry = class {
1961
1984
  }
1962
1985
  }
1963
1986
  }
1987
+ return applied;
1988
+ }
1989
+ /**
1990
+ * Per-record debug line, volume-gated: device-scoped records are logged
1991
+ * only when `verboseDeviceScopeLogs` is set (see the option's doc for
1992
+ * the 21 577-lines-per-boot-minute incident that forced the gate);
1993
+ * node/global records always log. Device lines carry `tags.deviceId`
1994
+ * so they group per-camera (CLAUDE.md logging rule).
1995
+ */
1996
+ logPerRecord(scope, message) {
1997
+ if (!this.logger) return;
1998
+ if (scope.type === "device") {
1999
+ if (!this.verboseDeviceScopeLogs) return;
2000
+ this.logger.debug(message, { tags: { deviceId: scope.deviceId } });
2001
+ return;
2002
+ }
2003
+ this.logger.debug(message);
1964
2004
  }
1965
2005
  /**
1966
2006
  * True when THIS process is the origin authority for `record` — a
@@ -2135,7 +2175,7 @@ var ReadinessRegistry = class {
2135
2175
  ts: payload.ts,
2136
2176
  durationInPrevState
2137
2177
  };
2138
- if (this.logger) this.logger.debug(`readiness: ${payload.capName} (${scopeKey(payload.scope)}) → ${payload.state} epoch=${epoch} gen=${payload.generation.slice(0, 6)} (prev ${durationInPrevState}ms)`);
2178
+ this.logPerRecord(payload.scope, `readiness: ${payload.capName} (${scopeKey(payload.scope)}) → ${payload.state} epoch=${epoch} gen=${payload.generation.slice(0, 6)} (prev ${durationInPrevState}ms)`);
2139
2179
  for (const sub of this.subscriptions) {
2140
2180
  if (sub.capName !== payload.capName) continue;
2141
2181
  if (!scopesEqual(sub.scope, payload.scope)) continue;
@@ -2180,7 +2220,7 @@ var ReadinessRegistry = class {
2180
2220
  ts: now,
2181
2221
  durationInPrevState
2182
2222
  };
2183
- if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → down (synthesized from agent.offline ${offlineNodeId})`);
2223
+ this.logPerRecord(record.scope, `readiness: ${record.capName} (${scopeKey(record.scope)}) → down (synthesized from agent.offline ${offlineNodeId})`);
2184
2224
  for (const sub of this.subscriptions) {
2185
2225
  if (sub.capName !== record.capName) continue;
2186
2226
  if (!scopesEqual(sub.scope, record.scope)) continue;
@@ -5326,6 +5366,29 @@ var NcOccupancyConditionSchema = z.object({
5326
5366
  count: z.number().int().min(0).default(1),
5327
5367
  sustainSeconds: z.number().int().min(0).max(3600).default(15)
5328
5368
  });
5369
+ /**
5370
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
5371
+ *
5372
+ * The values are not symmetric, and deliberately so — the absent value has to
5373
+ * mean exactly what every rule authored before this condition existed already
5374
+ * does:
5375
+ * - `enter` — entries and every NON-crossing record (movement state,
5376
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
5377
+ * an operator who never asked for exits must not start receiving them.
5378
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
5379
+ * fails closed, because "the car left the drive" is a question about a
5380
+ * boundary, not about a detection.
5381
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
5382
+ *
5383
+ * A rule asking for a direction should normally also scope `zones`, which the
5384
+ * engine evaluates against the crossed zone as well as the current membership
5385
+ * (an exit's membership no longer contains the zone it just left).
5386
+ */
5387
+ var NcCrossingSchema = z.enum([
5388
+ "enter",
5389
+ "exit",
5390
+ "any"
5391
+ ]);
5329
5392
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
5330
5393
  var NcZoneConditionSchema = z.object({
5331
5394
  ids: z.array(z.string().min(1)).min(1),
@@ -5350,6 +5413,13 @@ var NcConditionsSchema = z.object({
5350
5413
  /** Veto zones — any hit fails the rule. */
5351
5414
  zonesExclude: z.array(z.string().min(1)).optional(),
5352
5415
  /**
5416
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
5417
+ * and a closed track carries none, so a `track-end` rule asking for one
5418
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
5419
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
5420
+ */
5421
+ crossing: NcCrossingSchema.optional(),
5422
+ /**
5353
5423
  * Exact (case-insensitive) match on the record's collapsed `label`
5354
5424
  * (identity name / plate text / subclass).
5355
5425
  */
@@ -5538,11 +5608,31 @@ var NcMediaPolicySchema = z.object({
5538
5608
  */
5539
5609
  profile: CamProfileSchema.optional()
5540
5610
  });
5611
+ /**
5612
+ * Cooldown GRANULARITY over the subject's class — how much a fired
5613
+ * notification suppresses.
5614
+ * - `shared` (default, and the absent value) — one window for the whole
5615
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
5616
+ * - `per-class` — an independent window per detected class, so cat→dog fires
5617
+ * at once and cat→cat still waits.
5618
+ *
5619
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
5620
+ * must not be swallowed by a bark's window (the precedent this generalizes —
5621
+ * see `cooldownKey` in the rule engine).
5622
+ */
5623
+ var NcThrottleGranularitySchema = z.enum(["shared", "per-class"]);
5541
5624
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
5542
5625
  var NcThrottleSchema = z.object({
5543
5626
  cooldownSec: z.number().int().min(0).max(86400).default(60),
5544
5627
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
5545
- scope: z.enum(["rule", "rule-device"]).default("rule-device")
5628
+ scope: z.enum(["rule", "rule-device"]).default("rule-device"),
5629
+ /**
5630
+ * Class granularity of the cooldown key. Optional rather than defaulted:
5631
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
5632
+ * rule authored before this field simply carries none — and the engine
5633
+ * reads absent as `shared`, the pre-existing behaviour.
5634
+ */
5635
+ granularity: NcThrottleGranularitySchema.optional()
5546
5636
  });
5547
5637
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
5548
5638
  var NcRuleInputSchema = z.object({
@@ -5650,6 +5740,7 @@ var NcConditionDescriptorSchema = z.object({
5650
5740
  "schedule",
5651
5741
  "plateMatcher",
5652
5742
  "packagePhase",
5743
+ "crossingSelect",
5653
5744
  "polygonDraw",
5654
5745
  "occupancy"
5655
5746
  ]),
@@ -10422,6 +10513,18 @@ var motionCapability = {
10422
10513
  name: "motion",
10423
10514
  scope: "device",
10424
10515
  mode: "singleton",
10516
+ /**
10517
+ * Providers register per-device natives via `ctx.registerNativeCap`
10518
+ * (Hikvision/Reolink/Amcrest/Wyze/HA/Homematic/Alexa/Matter) — there is
10519
+ * NO system singleton provider. Without this flag `resolveCapMount`
10520
+ * derived `{ kind: 'singleton' }`, so `motion.getStatus`/`isDetected`
10521
+ * resolved via `registry.getSingleton('motion')` (always null) and every
10522
+ * call 412'd "provider not available" while bindings listed a live
10523
+ * `motion` native (2026-08-02). The flag routes the router through
10524
+ * `requireDeviceScoped` → `getProviderForDevice`, like `motion-trigger`,
10525
+ * `snapshot` and every other per-device native cap.
10526
+ */
10527
+ deviceNative: true,
10425
10528
  deviceTypes: [DeviceType.Camera, DeviceType.Sensor],
10426
10529
  methods: {
10427
10530
  /**
@@ -15673,7 +15776,29 @@ var deviceManagerCapability = {
15673
15776
  getDeviceStatusAggregate: method(z.object({
15674
15777
  deviceId: z.number(),
15675
15778
  caps: z.array(z.string()).readonly().optional()
15676
- }), z.record(z.string(), z.unknown().nullable()))
15779
+ }), z.record(z.string(), z.unknown().nullable())),
15780
+ /**
15781
+ * The same aggregate, for a LIST of devices in one round-trip.
15782
+ *
15783
+ * The viewer's camera list is the caller this exists for. Each card asked
15784
+ * for its own badges — battery here, recording mode there — so a ten-camera
15785
+ * install issued N separate procedures per reconcile, each its own message
15786
+ * on the one WebSocket that also carries the WebRTC signalling (the client
15787
+ * has no `httpBatchLink`, so there is no transport-level batching to lean
15788
+ * on: collapsing has to happen at the procedure level, which is here).
15789
+ *
15790
+ * Keyed by device id as a STRING — a JSON object cannot key by number, and
15791
+ * pretending otherwise is how a record survives the wire only in tests.
15792
+ * The per-device value is exactly what `getDeviceStatusAggregate` returns,
15793
+ * so a caller can move between the two without reshaping anything.
15794
+ *
15795
+ * `caps` matters more here than in the single call: a client that needs two
15796
+ * caps should not pay for every status-carrying cap × every device.
15797
+ */
15798
+ getDeviceStatusAggregateBatch: method(z.object({
15799
+ deviceIds: z.array(z.number()).readonly(),
15800
+ caps: z.array(z.string()).readonly().optional()
15801
+ }), z.record(z.string(), z.record(z.string(), z.unknown().nullable())))
15677
15802
  }
15678
15803
  };
15679
15804
  /**
@@ -17399,6 +17524,37 @@ var MotionEventSchema = z.object({
17399
17524
  * Absent on legacy rows ⇒ treat as `pipeline`.
17400
17525
  */
17401
17526
  var DetectionSourceSchema = z.enum(["pipeline", "onboard"]);
17527
+ /**
17528
+ * Which WAY a subject crossed a zone boundary. An entry and an exit are two
17529
+ * different notifications ("the parcel arrived" / "the car left the drive"),
17530
+ * and until this existed they were indistinguishable on the persisted record —
17531
+ * which is why exits were computed and dropped at the emit boundary rather
17532
+ * than doubling every zone-scoped rule.
17533
+ */
17534
+ var ZoneCrossingDirectionSchema = z.enum(["enter", "exit"]);
17535
+ /**
17536
+ * The confirmed zone crossing that produced an object event. Present ONLY on
17537
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
17538
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
17539
+ * appearance event carry none, so a rule asking for a direction fails closed
17540
+ * on them.
17541
+ *
17542
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
17543
+ * into its own event, so a frame in which a track enters A while leaving B
17544
+ * produces two events with two directions — never one ambiguous row.
17545
+ *
17546
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
17547
+ * membership the box has NOW, and by definition it no longer contains the zone
17548
+ * that was just left. Without the id here, a zone-scoped rule could never match
17549
+ * the exit it asked for.
17550
+ */
17551
+ var ZoneCrossingSchema = z.object({
17552
+ direction: ZoneCrossingDirectionSchema,
17553
+ /** Admin zone id crossed. */
17554
+ zoneId: z.string(),
17555
+ /** Zone display name at crossing time (falls back to the id). */
17556
+ zoneName: z.string().optional()
17557
+ });
17402
17558
  var ObjectEventSchema = z.object({
17403
17559
  ...BaseEventFields,
17404
17560
  kind: z.literal("object"),
@@ -17425,6 +17581,12 @@ var ObjectEventSchema = z.object({
17425
17581
  zones: z.array(z.string()).readonly().optional(),
17426
17582
  /** Omitted in slim projection. */
17427
17583
  state: TrackStateSchema.optional(),
17584
+ /**
17585
+ * The zone crossing this event IS, when it is one. Absent on every other
17586
+ * event kind (movement state, appearance, package) — see
17587
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
17588
+ */
17589
+ zoneCrossing: ZoneCrossingSchema.optional(),
17428
17590
  /** Detection-frame dimensions in pixels — let consumers normalize the
17429
17591
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
17430
17592
  frameWidth: z.number().optional(),
@@ -24958,6 +25120,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
24958
25120
  addonId: null,
24959
25121
  access: "view"
24960
25122
  },
25123
+ "deviceManager.getDeviceStatusAggregateBatch": {
25124
+ capName: "device-manager",
25125
+ capScope: "system",
25126
+ addonId: null,
25127
+ access: "view"
25128
+ },
24961
25129
  "deviceManager.getLinkedDevices": {
24962
25130
  capName: "device-manager",
24963
25131
  capScope: "system",