@camstack/addon-provider-amcrest 0.2.20 → 0.2.22

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 (3) hide show
  1. package/dist/addon.js +187 -45
  2. package/dist/addon.mjs +187 -45
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -4,7 +4,7 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
4
4
  //#endregion
5
5
  let node_crypto = require("node:crypto");
6
6
  let node_os = require("node:os");
7
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
7
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
8
8
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
9
9
  EventCategory["SystemBoot"] = "system.boot";
10
10
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -21,9 +21,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
21
21
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
22
22
  /**
23
23
  * A newer addon or server-root package version was found by the
24
- * authoritative registry check. Emitted once per
25
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
26
- * polling of the same result is deduplicated by the checker.
24
+ * authoritative registry check. Emitted once when any observed
25
+ * `latestVersion` changes (or a package/node first appears behind);
26
+ * the payload carries the full currently-available list. Repeated
27
+ * polling of the same latests is silent.
27
28
  */
28
29
  EventCategory["UpdateAvailable"] = "update.available";
29
30
  /**
@@ -42,6 +43,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
42
43
  EventCategory["AddonInstalled"] = "addon.installed";
43
44
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
44
45
  EventCategory["AddonCrashed"] = "addon.crashed";
46
+ /**
47
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
48
+ *
49
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
50
+ * respawned"); this is the one that never resolves itself. Emitted exactly
51
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
52
+ * addons it hosted and the crash count that tripped the breaker.
53
+ *
54
+ * It exists because a runner marked terminally `failed` used to be SILENT:
55
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
56
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
57
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
58
+ * operator's phone told him. The Notification Center's system-event intake
59
+ * consumes this category and maps it to the `addon-crash-loop` kind.
60
+ */
61
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
45
62
  EventCategory["AddonError"] = "addon.error";
46
63
  EventCategory["AddonPageReady"] = "addon.page-ready";
47
64
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7541,6 +7558,10 @@ var RecordingBandSchema = object({
7541
7558
  preBufferSec: number().min(0).optional(),
7542
7559
  postBufferSec: number().min(0).optional()
7543
7560
  });
7561
+ ({
7562
+ preBufferSec: 10,
7563
+ postBufferSec: 30
7564
+ }).postBufferSec * 1e3;
7544
7565
  /**
7545
7566
  * Per-device retention overrides. Every field is optional; an unset or `0`
7546
7567
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7588,6 +7609,12 @@ var RecordingConfigSchema = object({
7588
7609
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7589
7610
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7590
7611
  mode: RecordingStorageModeSchema.optional(),
7612
+ /**
7613
+ * Which assigned broker slots to record. Absent / empty = {@link
7614
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7615
+ * camera's currently assigned slots — never `mid` unless the operator
7616
+ * picks it, and never a slot the broker has not assigned.
7617
+ */
7591
7618
  profiles: array(CamProfileSchema).optional(),
7592
7619
  segmentSeconds: number().int().positive().optional(),
7593
7620
  /**
@@ -7668,7 +7695,11 @@ var RelocateFootageInputSchema = object({
7668
7695
  profiles: array(string()).optional(),
7669
7696
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7670
7697
  * never allowed to starve live writers. */
7671
- throttleMbps: number().min(1).max(1e3).optional()
7698
+ throttleMbps: number().min(1).max(1e3).optional(),
7699
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7700
+ * pile. Used when a full drain is too expensive and the operator only
7701
+ * wants the recent window on the new disk. */
7702
+ sinceMs: number().int().optional()
7672
7703
  });
7673
7704
  /** Internal, lease-scoped participant operation. It is intentionally separate
7674
7705
  * from persistent recording settings: a migration never changes
@@ -14533,6 +14564,7 @@ var NcSystemEventKindSchema = _enum([
14533
14564
  "node-offline",
14534
14565
  "node-inference-unavailable",
14535
14566
  "detection-blind",
14567
+ "addon-crash-loop",
14536
14568
  "addon-update-available",
14537
14569
  "server-update-available",
14538
14570
  "alarm-triggered",
@@ -14540,6 +14572,9 @@ var NcSystemEventKindSchema = _enum([
14540
14572
  "alarm-disarmed",
14541
14573
  "alarm-arming",
14542
14574
  "alarm-arm-refused",
14575
+ "addon-updated",
14576
+ "server-updated",
14577
+ "export-completed",
14543
14578
  "camera-online",
14544
14579
  "camera-offline",
14545
14580
  "camera-disabled",
@@ -16433,13 +16468,19 @@ var RetrainStatusSchema = _enum([
16433
16468
  * "never marked" from "already trained" must read `retrainStatus`.
16434
16469
  *
16435
16470
  * `debug` does NOT pin; it is attention, not durability.
16471
+ *
16472
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16473
+ * A favourited track is skipped by retention the same way `staging` is, but
16474
+ * it does not enter `none|staging|trained` and has no staging budget.
16436
16475
  */
16437
16476
  var TrackFlagFields = {
16438
16477
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16439
16478
  * `'staging'`. */
16440
16479
  markForTrain: boolean().optional(),
16441
16480
  /** Operator marked this track for diagnostic attention. */
16442
- debug: boolean().optional()
16481
+ debug: boolean().optional(),
16482
+ /** Operator favourited this track. Pins it against pruning. */
16483
+ favourited: boolean().optional()
16443
16484
  };
16444
16485
  /**
16445
16486
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16464,6 +16505,7 @@ var TrackFlagsSchema = object({
16464
16505
  trackId: string(),
16465
16506
  markForTrain: boolean(),
16466
16507
  debug: boolean(),
16508
+ favourited: boolean(),
16467
16509
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16468
16510
  * a track row) because this shape is only ever produced by the write body,
16469
16511
  * which always knows it — and a surface that has just written needs to render
@@ -17096,6 +17138,12 @@ var TrackCascadeCountsSchema = object({
17096
17138
  /** Per-track CLIP search vectors removed (best-effort). */
17097
17139
  embeddings: number().int()
17098
17140
  });
17141
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17142
+ var DiskReconcileCountsSchema = object({
17143
+ mediaDropped: number().int(),
17144
+ tracks: number().int(),
17145
+ events: number().int()
17146
+ });
17099
17147
  /** Event-store footprint for one camera. */
17100
17148
  var EventStoreDeviceFootprintSchema = object({
17101
17149
  deviceId: number(),
@@ -17272,6 +17320,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17272
17320
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17273
17321
  kind: "mutation",
17274
17322
  auth: "admin"
17323
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17324
+ kind: "mutation",
17325
+ auth: "admin"
17275
17326
  }), method(object({
17276
17327
  deviceId: number(),
17277
17328
  trackIds: array(string()).min(1)
@@ -18822,6 +18873,24 @@ var CameraStatusDegradationSchema = object({
18822
18873
  *
18823
18874
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18824
18875
  */
18876
+ var DiskReconcileJobSchema = object({
18877
+ state: _enum([
18878
+ "idle",
18879
+ "running",
18880
+ "done",
18881
+ "error"
18882
+ ]),
18883
+ total: number().int().nonnegative(),
18884
+ completed: number().int().nonnegative(),
18885
+ currentDeviceId: number().int().nullable(),
18886
+ failed: array(number().int()).readonly(),
18887
+ mediaDropped: number().int().nonnegative(),
18888
+ tracks: number().int().nonnegative(),
18889
+ events: number().int().nonnegative(),
18890
+ startedAtMs: number().int().nullable(),
18891
+ finishedAtMs: number().int().nullable(),
18892
+ error: string().nullable()
18893
+ });
18825
18894
  var CameraStatusSchema = object({
18826
18895
  deviceId: number(),
18827
18896
  assignment: CameraAssignmentStatusSchema,
@@ -19053,7 +19122,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19053
19122
  }), CameraSwitchGroupSchema, {
19054
19123
  kind: "mutation",
19055
19124
  auth: "admin"
19056
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19125
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19126
+ kind: "mutation",
19127
+ auth: "admin"
19128
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19057
19129
  name: string(),
19058
19130
  description: string().optional(),
19059
19131
  config: CameraPipelineConfigSchema
@@ -19556,7 +19628,14 @@ targets: array(object({
19556
19628
  "sleeping",
19557
19629
  "unreachable",
19558
19630
  "waking"
19559
- ]).nullable()
19631
+ ]).nullable(),
19632
+ /** A battery camera (whatever its current state). An AWAKE battery
19633
+ * camera is deliberately NOT recaptured on the poll cadence — every
19634
+ * capture is a camera hit that would keep it out of sleep — so its
19635
+ * cached frame legitimately ages past the currency ceiling while
19636
+ * nothing is streaming. A surface must keep painting it (a fresh
19637
+ * frame is captured at each wake), not blank to "unavailable". */
19638
+ battery: boolean()
19560
19639
  })))
19561
19640
  },
19562
19641
  status: {
@@ -20223,20 +20302,6 @@ var VectorStatsResultSchema = object({
20223
20302
  exact: boolean()
20224
20303
  });
20225
20304
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20226
- /**
20227
- * `videoclips` — the unified, navigable-clip surface for a camera.
20228
- *
20229
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20230
- * provider per device, substitutable. The DEFAULT provider (registered by
20231
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20232
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20233
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20234
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20235
- * wrapper provider for its device and serve its own clip catalog + URLs.
20236
- *
20237
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20238
- * temporal overlap, so the API never decides `continuous` vs `events`.
20239
- */
20240
20305
  var ClipSchema = object({
20241
20306
  /** Opaque, provider-namespaced id. The default provider encodes the time
20242
20307
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20253,8 +20318,57 @@ var ClipSchema = object({
20253
20318
  startMs: number(),
20254
20319
  endMs: number()
20255
20320
  }),
20256
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20257
- thumbnail: string().optional()
20321
+ /**
20322
+ * Lazy thumbnail URL, never inlined.
20323
+ *
20324
+ * Recording-derived clips (events-mode keep-window, and the prepared
20325
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20326
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20327
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20328
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20329
+ * mint their own stills.
20330
+ *
20331
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20332
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20333
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20334
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20335
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20336
+ * A read that FAILS drops the claim; it never invents it.
20337
+ */
20338
+ thumbnail: string().optional(),
20339
+ /**
20340
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20341
+ * can be decoded. Present whenever the visit came from recorded availability;
20342
+ * absent on a per-event padded window (there is no footage to promise).
20343
+ *
20344
+ * This is not a thumbnail and not a second byte path: it is the argument to
20345
+ * the recorder's existing still route. The surface — never the provider —
20346
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20347
+ * contiguous range, not of the visit: a visit spans its holes by
20348
+ * construction, so a naive midpoint lands in dead air.
20349
+ */
20350
+ stillAtMs: number().optional(),
20351
+ /**
20352
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20353
+ * first within kind (object → motion → audio), capped at
20354
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20355
+ *
20356
+ * Bounded because it is not a payload the surface pages through: one visit on
20357
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20358
+ * camera-day. Read {@link eventCount} for the true total.
20359
+ */
20360
+ eventIds: array(string()).optional(),
20361
+ /** How many analytics events actually overlap this visit. Differs from
20362
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20363
+ * list is never mistaken for a quiet visit. */
20364
+ eventCount: number().int().nonnegative().optional(),
20365
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20366
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20367
+ * bar keeps showing them via `recording.getAvailability`. */
20368
+ holes: array(object({
20369
+ startMs: number(),
20370
+ endMs: number()
20371
+ })).optional()
20258
20372
  });
20259
20373
  var ClipPlaybackSchema = object({
20260
20374
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20718,7 +20832,14 @@ var SearchResultSchema = object({
20718
20832
  });
20719
20833
  var AutoUpdateSettingsSchema = object({
20720
20834
  channel: ChannelSchema,
20721
- intervalSeconds: number()
20835
+ intervalSeconds: number(),
20836
+ /**
20837
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20838
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20839
+ * about a publish and installing it are different decisions. Clamped
20840
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20841
+ */
20842
+ updateCheckIntervalSeconds: number()
20722
20843
  });
20723
20844
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20724
20845
  var RestartAddonResultSchema = unknown();
@@ -20859,7 +20980,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20859
20980
  auth: "admin"
20860
20981
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20861
20982
  channel: ChannelSchema,
20862
- intervalSeconds: number().min(300).max(86400).optional()
20983
+ intervalSeconds: number().min(300).max(86400).optional(),
20984
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20985
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20863
20986
  }), unknown(), {
20864
20987
  kind: "mutation",
20865
20988
  auth: "admin"
@@ -26628,7 +26751,9 @@ var ExportOptionsSchema = object({
26628
26751
  includeAudio: boolean(),
26629
26752
  maxLifeMs: number().int().positive(),
26630
26753
  deleteAfterDownload: boolean(),
26631
- title: string().max(200).optional()
26754
+ title: string().max(200).optional(),
26755
+ /** Notification-output target ids to ping when this export becomes ready. */
26756
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26632
26757
  }).superRefine((v, ctx) => {
26633
26758
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26634
26759
  code: ZodIssueCode.custom,
@@ -26687,10 +26812,18 @@ var ExportBytesSchema = object({
26687
26812
  });
26688
26813
  method(object({
26689
26814
  deviceId: number(),
26690
- profile: string(),
26815
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26816
+ profile: string().optional(),
26817
+ profiles: array(string()).min(1).optional(),
26691
26818
  fromMs: number(),
26692
26819
  toMs: number(),
26693
26820
  options: ExportOptionsSchema
26821
+ }).superRefine((v, ctx) => {
26822
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26823
+ code: ZodIssueCode.custom,
26824
+ message: "pass profiles[] (min 1) or legacy profile",
26825
+ path: ["profiles"]
26826
+ });
26694
26827
  }), ExportRecordSchema, {
26695
26828
  kind: "mutation",
26696
26829
  auth: "protected"
@@ -29830,15 +29963,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29830
29963
  labels: ["probe not implemented"]
29831
29964
  };
29832
29965
  }
29833
- /**
29834
- * Top-level devices restored at once in {@link onRestoreDevices}.
29835
- *
29836
- * Four covers the fleets this ships to without turning a boot into a burst a
29837
- * camera NVR answers with a refusal. A provider whose upstream is a single
29838
- * session with a serial command channel (a Baichuan hub, an NVR that
29839
- * serialises ISAPI) should lower it; nothing needs to raise it.
29840
- */
29841
- restoreConcurrency = 4;
29842
29966
  async restoreDevices(savedDevices) {
29843
29967
  await this.onRestoreDevices(savedDevices);
29844
29968
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29893,14 +30017,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29893
30017
  });
29894
30018
  }
29895
30019
  };
29896
- let nextTopLevel = 0;
29897
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29898
- for (;;) {
29899
- const saved = topLevel[nextTopLevel++];
29900
- if (saved === void 0) return;
29901
- await restoreOne(saved);
29902
- }
29903
- }));
30020
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29904
30021
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29905
30022
  for (const saved of childRows) {
29906
30023
  const Class = this.deviceClasses[saved.type];
@@ -33304,6 +33421,12 @@ Object.freeze({
33304
33421
  addonId: null,
33305
33422
  access: "create"
33306
33423
  },
33424
+ "pipelineAnalytics.reconcileFromDisk": {
33425
+ capName: "pipeline-analytics",
33426
+ capScope: "device",
33427
+ addonId: null,
33428
+ access: "create"
33429
+ },
33307
33430
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
33308
33431
  capName: "pipeline-analytics",
33309
33432
  capScope: "device",
@@ -33706,6 +33829,12 @@ Object.freeze({
33706
33829
  addonId: null,
33707
33830
  access: "view"
33708
33831
  },
33832
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33833
+ capName: "pipeline-orchestrator",
33834
+ capScope: "system",
33835
+ addonId: null,
33836
+ access: "view"
33837
+ },
33709
33838
  "pipelineOrchestrator.listAgentSettings": {
33710
33839
  capName: "pipeline-orchestrator",
33711
33840
  capScope: "system",
@@ -33730,6 +33859,12 @@ Object.freeze({
33730
33859
  addonId: null,
33731
33860
  access: "create"
33732
33861
  },
33862
+ "pipelineOrchestrator.reconcileFromDisk": {
33863
+ capName: "pipeline-orchestrator",
33864
+ capScope: "system",
33865
+ addonId: null,
33866
+ access: "create"
33867
+ },
33733
33868
  "pipelineOrchestrator.removeAgentSettings": {
33734
33869
  capName: "pipeline-orchestrator",
33735
33870
  capScope: "system",
@@ -36721,6 +36856,11 @@ Object.freeze({
36721
36856
  form: "single",
36722
36857
  optional: true
36723
36858
  }],
36859
+ "pipelineAnalytics.reconcileFromDisk": [{
36860
+ name: "deviceId",
36861
+ form: "single",
36862
+ optional: false
36863
+ }],
36724
36864
  "pipelineAnalytics.restageRetrainTrack": [{
36725
36865
  name: "deviceId",
36726
36866
  form: "single",
@@ -37786,6 +37926,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
37786
37926
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
37787
37927
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
37788
37928
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
37929
+ var MB = 1024 * 1024;
37930
+ 1024 * MB, 3072 * MB;
37789
37931
  //#endregion
37790
37932
  //#region src/digest-auth.ts
37791
37933
  /**
package/dist/addon.mjs CHANGED
@@ -5,7 +5,7 @@ import { networkInterfaces } from "node:os";
5
5
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
6
6
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
7
7
  //#endregion
8
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
8
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
9
9
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
10
10
  EventCategory["SystemBoot"] = "system.boot";
11
11
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -22,9 +22,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
22
22
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
23
23
  /**
24
24
  * A newer addon or server-root package version was found by the
25
- * authoritative registry check. Emitted once per
26
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
27
- * polling of the same result is deduplicated by the checker.
25
+ * authoritative registry check. Emitted once when any observed
26
+ * `latestVersion` changes (or a package/node first appears behind);
27
+ * the payload carries the full currently-available list. Repeated
28
+ * polling of the same latests is silent.
28
29
  */
29
30
  EventCategory["UpdateAvailable"] = "update.available";
30
31
  /**
@@ -43,6 +44,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
43
44
  EventCategory["AddonInstalled"] = "addon.installed";
44
45
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
45
46
  EventCategory["AddonCrashed"] = "addon.crashed";
47
+ /**
48
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
49
+ *
50
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
51
+ * respawned"); this is the one that never resolves itself. Emitted exactly
52
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
53
+ * addons it hosted and the crash count that tripped the breaker.
54
+ *
55
+ * It exists because a runner marked terminally `failed` used to be SILENT:
56
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
57
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
58
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
59
+ * operator's phone told him. The Notification Center's system-event intake
60
+ * consumes this category and maps it to the `addon-crash-loop` kind.
61
+ */
62
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
46
63
  EventCategory["AddonError"] = "addon.error";
47
64
  EventCategory["AddonPageReady"] = "addon.page-ready";
48
65
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7542,6 +7559,10 @@ var RecordingBandSchema = object({
7542
7559
  preBufferSec: number().min(0).optional(),
7543
7560
  postBufferSec: number().min(0).optional()
7544
7561
  });
7562
+ ({
7563
+ preBufferSec: 10,
7564
+ postBufferSec: 30
7565
+ }).postBufferSec * 1e3;
7545
7566
  /**
7546
7567
  * Per-device retention overrides. Every field is optional; an unset or `0`
7547
7568
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7589,6 +7610,12 @@ var RecordingConfigSchema = object({
7589
7610
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7590
7611
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7591
7612
  mode: RecordingStorageModeSchema.optional(),
7613
+ /**
7614
+ * Which assigned broker slots to record. Absent / empty = {@link
7615
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7616
+ * camera's currently assigned slots — never `mid` unless the operator
7617
+ * picks it, and never a slot the broker has not assigned.
7618
+ */
7592
7619
  profiles: array(CamProfileSchema).optional(),
7593
7620
  segmentSeconds: number().int().positive().optional(),
7594
7621
  /**
@@ -7669,7 +7696,11 @@ var RelocateFootageInputSchema = object({
7669
7696
  profiles: array(string()).optional(),
7670
7697
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7671
7698
  * never allowed to starve live writers. */
7672
- throttleMbps: number().min(1).max(1e3).optional()
7699
+ throttleMbps: number().min(1).max(1e3).optional(),
7700
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7701
+ * pile. Used when a full drain is too expensive and the operator only
7702
+ * wants the recent window on the new disk. */
7703
+ sinceMs: number().int().optional()
7673
7704
  });
7674
7705
  /** Internal, lease-scoped participant operation. It is intentionally separate
7675
7706
  * from persistent recording settings: a migration never changes
@@ -14534,6 +14565,7 @@ var NcSystemEventKindSchema = _enum([
14534
14565
  "node-offline",
14535
14566
  "node-inference-unavailable",
14536
14567
  "detection-blind",
14568
+ "addon-crash-loop",
14537
14569
  "addon-update-available",
14538
14570
  "server-update-available",
14539
14571
  "alarm-triggered",
@@ -14541,6 +14573,9 @@ var NcSystemEventKindSchema = _enum([
14541
14573
  "alarm-disarmed",
14542
14574
  "alarm-arming",
14543
14575
  "alarm-arm-refused",
14576
+ "addon-updated",
14577
+ "server-updated",
14578
+ "export-completed",
14544
14579
  "camera-online",
14545
14580
  "camera-offline",
14546
14581
  "camera-disabled",
@@ -16434,13 +16469,19 @@ var RetrainStatusSchema = _enum([
16434
16469
  * "never marked" from "already trained" must read `retrainStatus`.
16435
16470
  *
16436
16471
  * `debug` does NOT pin; it is attention, not durability.
16472
+ *
16473
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16474
+ * A favourited track is skipped by retention the same way `staging` is, but
16475
+ * it does not enter `none|staging|trained` and has no staging budget.
16437
16476
  */
16438
16477
  var TrackFlagFields = {
16439
16478
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16440
16479
  * `'staging'`. */
16441
16480
  markForTrain: boolean().optional(),
16442
16481
  /** Operator marked this track for diagnostic attention. */
16443
- debug: boolean().optional()
16482
+ debug: boolean().optional(),
16483
+ /** Operator favourited this track. Pins it against pruning. */
16484
+ favourited: boolean().optional()
16444
16485
  };
16445
16486
  /**
16446
16487
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16465,6 +16506,7 @@ var TrackFlagsSchema = object({
16465
16506
  trackId: string(),
16466
16507
  markForTrain: boolean(),
16467
16508
  debug: boolean(),
16509
+ favourited: boolean(),
16468
16510
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16469
16511
  * a track row) because this shape is only ever produced by the write body,
16470
16512
  * which always knows it — and a surface that has just written needs to render
@@ -17097,6 +17139,12 @@ var TrackCascadeCountsSchema = object({
17097
17139
  /** Per-track CLIP search vectors removed (best-effort). */
17098
17140
  embeddings: number().int()
17099
17141
  });
17142
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17143
+ var DiskReconcileCountsSchema = object({
17144
+ mediaDropped: number().int(),
17145
+ tracks: number().int(),
17146
+ events: number().int()
17147
+ });
17100
17148
  /** Event-store footprint for one camera. */
17101
17149
  var EventStoreDeviceFootprintSchema = object({
17102
17150
  deviceId: number(),
@@ -17273,6 +17321,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17273
17321
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17274
17322
  kind: "mutation",
17275
17323
  auth: "admin"
17324
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17325
+ kind: "mutation",
17326
+ auth: "admin"
17276
17327
  }), method(object({
17277
17328
  deviceId: number(),
17278
17329
  trackIds: array(string()).min(1)
@@ -18823,6 +18874,24 @@ var CameraStatusDegradationSchema = object({
18823
18874
  *
18824
18875
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18825
18876
  */
18877
+ var DiskReconcileJobSchema = object({
18878
+ state: _enum([
18879
+ "idle",
18880
+ "running",
18881
+ "done",
18882
+ "error"
18883
+ ]),
18884
+ total: number().int().nonnegative(),
18885
+ completed: number().int().nonnegative(),
18886
+ currentDeviceId: number().int().nullable(),
18887
+ failed: array(number().int()).readonly(),
18888
+ mediaDropped: number().int().nonnegative(),
18889
+ tracks: number().int().nonnegative(),
18890
+ events: number().int().nonnegative(),
18891
+ startedAtMs: number().int().nullable(),
18892
+ finishedAtMs: number().int().nullable(),
18893
+ error: string().nullable()
18894
+ });
18826
18895
  var CameraStatusSchema = object({
18827
18896
  deviceId: number(),
18828
18897
  assignment: CameraAssignmentStatusSchema,
@@ -19054,7 +19123,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19054
19123
  }), CameraSwitchGroupSchema, {
19055
19124
  kind: "mutation",
19056
19125
  auth: "admin"
19057
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19126
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19127
+ kind: "mutation",
19128
+ auth: "admin"
19129
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19058
19130
  name: string(),
19059
19131
  description: string().optional(),
19060
19132
  config: CameraPipelineConfigSchema
@@ -19557,7 +19629,14 @@ targets: array(object({
19557
19629
  "sleeping",
19558
19630
  "unreachable",
19559
19631
  "waking"
19560
- ]).nullable()
19632
+ ]).nullable(),
19633
+ /** A battery camera (whatever its current state). An AWAKE battery
19634
+ * camera is deliberately NOT recaptured on the poll cadence — every
19635
+ * capture is a camera hit that would keep it out of sleep — so its
19636
+ * cached frame legitimately ages past the currency ceiling while
19637
+ * nothing is streaming. A surface must keep painting it (a fresh
19638
+ * frame is captured at each wake), not blank to "unavailable". */
19639
+ battery: boolean()
19561
19640
  })))
19562
19641
  },
19563
19642
  status: {
@@ -20224,20 +20303,6 @@ var VectorStatsResultSchema = object({
20224
20303
  exact: boolean()
20225
20304
  });
20226
20305
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20227
- /**
20228
- * `videoclips` — the unified, navigable-clip surface for a camera.
20229
- *
20230
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20231
- * provider per device, substitutable. The DEFAULT provider (registered by
20232
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20233
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20234
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20235
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20236
- * wrapper provider for its device and serve its own clip catalog + URLs.
20237
- *
20238
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20239
- * temporal overlap, so the API never decides `continuous` vs `events`.
20240
- */
20241
20306
  var ClipSchema = object({
20242
20307
  /** Opaque, provider-namespaced id. The default provider encodes the time
20243
20308
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20254,8 +20319,57 @@ var ClipSchema = object({
20254
20319
  startMs: number(),
20255
20320
  endMs: number()
20256
20321
  }),
20257
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20258
- thumbnail: string().optional()
20322
+ /**
20323
+ * Lazy thumbnail URL, never inlined.
20324
+ *
20325
+ * Recording-derived clips (events-mode keep-window, and the prepared
20326
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20327
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20328
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20329
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20330
+ * mint their own stills.
20331
+ *
20332
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20333
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20334
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20335
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20336
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20337
+ * A read that FAILS drops the claim; it never invents it.
20338
+ */
20339
+ thumbnail: string().optional(),
20340
+ /**
20341
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20342
+ * can be decoded. Present whenever the visit came from recorded availability;
20343
+ * absent on a per-event padded window (there is no footage to promise).
20344
+ *
20345
+ * This is not a thumbnail and not a second byte path: it is the argument to
20346
+ * the recorder's existing still route. The surface — never the provider —
20347
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20348
+ * contiguous range, not of the visit: a visit spans its holes by
20349
+ * construction, so a naive midpoint lands in dead air.
20350
+ */
20351
+ stillAtMs: number().optional(),
20352
+ /**
20353
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20354
+ * first within kind (object → motion → audio), capped at
20355
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20356
+ *
20357
+ * Bounded because it is not a payload the surface pages through: one visit on
20358
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20359
+ * camera-day. Read {@link eventCount} for the true total.
20360
+ */
20361
+ eventIds: array(string()).optional(),
20362
+ /** How many analytics events actually overlap this visit. Differs from
20363
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20364
+ * list is never mistaken for a quiet visit. */
20365
+ eventCount: number().int().nonnegative().optional(),
20366
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20367
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20368
+ * bar keeps showing them via `recording.getAvailability`. */
20369
+ holes: array(object({
20370
+ startMs: number(),
20371
+ endMs: number()
20372
+ })).optional()
20259
20373
  });
20260
20374
  var ClipPlaybackSchema = object({
20261
20375
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20719,7 +20833,14 @@ var SearchResultSchema = object({
20719
20833
  });
20720
20834
  var AutoUpdateSettingsSchema = object({
20721
20835
  channel: ChannelSchema,
20722
- intervalSeconds: number()
20836
+ intervalSeconds: number(),
20837
+ /**
20838
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20839
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20840
+ * about a publish and installing it are different decisions. Clamped
20841
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20842
+ */
20843
+ updateCheckIntervalSeconds: number()
20723
20844
  });
20724
20845
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20725
20846
  var RestartAddonResultSchema = unknown();
@@ -20860,7 +20981,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20860
20981
  auth: "admin"
20861
20982
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20862
20983
  channel: ChannelSchema,
20863
- intervalSeconds: number().min(300).max(86400).optional()
20984
+ intervalSeconds: number().min(300).max(86400).optional(),
20985
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
20986
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20864
20987
  }), unknown(), {
20865
20988
  kind: "mutation",
20866
20989
  auth: "admin"
@@ -26629,7 +26752,9 @@ var ExportOptionsSchema = object({
26629
26752
  includeAudio: boolean(),
26630
26753
  maxLifeMs: number().int().positive(),
26631
26754
  deleteAfterDownload: boolean(),
26632
- title: string().max(200).optional()
26755
+ title: string().max(200).optional(),
26756
+ /** Notification-output target ids to ping when this export becomes ready. */
26757
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26633
26758
  }).superRefine((v, ctx) => {
26634
26759
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26635
26760
  code: ZodIssueCode.custom,
@@ -26688,10 +26813,18 @@ var ExportBytesSchema = object({
26688
26813
  });
26689
26814
  method(object({
26690
26815
  deviceId: number(),
26691
- profile: string(),
26816
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26817
+ profile: string().optional(),
26818
+ profiles: array(string()).min(1).optional(),
26692
26819
  fromMs: number(),
26693
26820
  toMs: number(),
26694
26821
  options: ExportOptionsSchema
26822
+ }).superRefine((v, ctx) => {
26823
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26824
+ code: ZodIssueCode.custom,
26825
+ message: "pass profiles[] (min 1) or legacy profile",
26826
+ path: ["profiles"]
26827
+ });
26695
26828
  }), ExportRecordSchema, {
26696
26829
  kind: "mutation",
26697
26830
  auth: "protected"
@@ -29831,15 +29964,6 @@ var BaseDeviceProvider = class extends BaseAddon {
29831
29964
  labels: ["probe not implemented"]
29832
29965
  };
29833
29966
  }
29834
- /**
29835
- * Top-level devices restored at once in {@link onRestoreDevices}.
29836
- *
29837
- * Four covers the fleets this ships to without turning a boot into a burst a
29838
- * camera NVR answers with a refusal. A provider whose upstream is a single
29839
- * session with a serial command channel (a Baichuan hub, an NVR that
29840
- * serialises ISAPI) should lower it; nothing needs to raise it.
29841
- */
29842
- restoreConcurrency = 4;
29843
29967
  async restoreDevices(savedDevices) {
29844
29968
  await this.onRestoreDevices(savedDevices);
29845
29969
  if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
@@ -29894,14 +30018,7 @@ var BaseDeviceProvider = class extends BaseAddon {
29894
30018
  });
29895
30019
  }
29896
30020
  };
29897
- let nextTopLevel = 0;
29898
- await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
29899
- for (;;) {
29900
- const saved = topLevel[nextTopLevel++];
29901
- if (saved === void 0) return;
29902
- await restoreOne(saved);
29903
- }
29904
- }));
30021
+ await Promise.all(topLevel.map((saved) => restoreOne(saved)));
29905
30022
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
29906
30023
  for (const saved of childRows) {
29907
30024
  const Class = this.deviceClasses[saved.type];
@@ -33305,6 +33422,12 @@ Object.freeze({
33305
33422
  addonId: null,
33306
33423
  access: "create"
33307
33424
  },
33425
+ "pipelineAnalytics.reconcileFromDisk": {
33426
+ capName: "pipeline-analytics",
33427
+ capScope: "device",
33428
+ addonId: null,
33429
+ access: "create"
33430
+ },
33308
33431
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
33309
33432
  capName: "pipeline-analytics",
33310
33433
  capScope: "device",
@@ -33707,6 +33830,12 @@ Object.freeze({
33707
33830
  addonId: null,
33708
33831
  access: "view"
33709
33832
  },
33833
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33834
+ capName: "pipeline-orchestrator",
33835
+ capScope: "system",
33836
+ addonId: null,
33837
+ access: "view"
33838
+ },
33710
33839
  "pipelineOrchestrator.listAgentSettings": {
33711
33840
  capName: "pipeline-orchestrator",
33712
33841
  capScope: "system",
@@ -33731,6 +33860,12 @@ Object.freeze({
33731
33860
  addonId: null,
33732
33861
  access: "create"
33733
33862
  },
33863
+ "pipelineOrchestrator.reconcileFromDisk": {
33864
+ capName: "pipeline-orchestrator",
33865
+ capScope: "system",
33866
+ addonId: null,
33867
+ access: "create"
33868
+ },
33734
33869
  "pipelineOrchestrator.removeAgentSettings": {
33735
33870
  capName: "pipeline-orchestrator",
33736
33871
  capScope: "system",
@@ -36722,6 +36857,11 @@ Object.freeze({
36722
36857
  form: "single",
36723
36858
  optional: true
36724
36859
  }],
36860
+ "pipelineAnalytics.reconcileFromDisk": [{
36861
+ name: "deviceId",
36862
+ form: "single",
36863
+ optional: false
36864
+ }],
36725
36865
  "pipelineAnalytics.restageRetrainTrack": [{
36726
36866
  name: "deviceId",
36727
36867
  form: "single",
@@ -37787,6 +37927,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
37787
37927
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
37788
37928
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
37789
37929
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
37930
+ var MB = 1024 * 1024;
37931
+ 1024 * MB, 3072 * MB;
37790
37932
  //#endregion
37791
37933
  //#region src/digest-auth.ts
37792
37934
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",