@camstack/addon-smtp-nodemailer 1.2.46 → 1.2.48

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.
@@ -8039,6 +8039,20 @@ var RelocateJobSchema = object({
8039
8039
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8040
8040
  */
8041
8041
  rowsReconciled: number().int().nonnegative().optional(),
8042
+ /**
8043
+ * Rows this run FORGOT because the file they name is not on disk.
8044
+ *
8045
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8046
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8047
+ * and the durable row is dropped through the same channel eviction uses. It
8048
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8049
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8050
+ * the same failure as one that quietly skips them (D295).
8051
+ *
8052
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8053
+ * ledger claimed 5.65 GB of footage that no longer existed.
8054
+ */
8055
+ rowsForgotten: number().int().nonnegative().optional(),
8042
8056
  startedAt: number(),
8043
8057
  finishedAt: number().nullable(),
8044
8058
  error: string().nullable()
@@ -8402,6 +8416,91 @@ var RelocateResidueSchema = object({
8402
8416
  segments: number().int().nonnegative(),
8403
8417
  bytes: number().int().nonnegative()
8404
8418
  }).nullable();
8419
+ /**
8420
+ * Ask one location whether its durable hour rows describe the disk — the walk
8421
+ * (D319).
8422
+ *
8423
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8424
+ * missing tool is the question, and the dry run is how they sanity-check the
8425
+ * destructive run before authorising it.
8426
+ */
8427
+ var LedgerWalkInputSchema = object({
8428
+ locationId: string().min(1),
8429
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8430
+ apply: boolean().optional(),
8431
+ /** Narrow to one camera. */
8432
+ deviceId: number().int().positive().optional(),
8433
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8434
+ profiles: array(string().min(1)).optional()
8435
+ });
8436
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8437
+ var LedgerWalkRefusalSchema = _enum([
8438
+ "location-unknown",
8439
+ "source-writable",
8440
+ "no-ledger",
8441
+ "archive-unreadable",
8442
+ "anchor-absent",
8443
+ "anchor-unreadable",
8444
+ "anchor-moved"
8445
+ ]);
8446
+ _enum([
8447
+ "live-tail",
8448
+ "listing-error",
8449
+ "path-mismatch",
8450
+ "durable-refused"
8451
+ ]);
8452
+ /** Every skip reason, always present, always a number — so a reason that never
8453
+ * fired reports as zero rather than absent and the report shape is constant
8454
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8455
+ var LedgerWalkSkipCountsSchema = object({
8456
+ "live-tail": number().int().nonnegative(),
8457
+ "listing-error": number().int().nonnegative(),
8458
+ "path-mismatch": number().int().nonnegative(),
8459
+ "durable-refused": number().int().nonnegative()
8460
+ });
8461
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8462
+ var LedgerWalkDeviceReportSchema = object({
8463
+ deviceId: number().int(),
8464
+ hoursWalked: number().int().nonnegative(),
8465
+ hoursMissing: number().int().nonnegative(),
8466
+ ghostSegments: number().int().nonnegative(),
8467
+ ghostBytes: number().int().nonnegative(),
8468
+ forgottenSegments: number().int().nonnegative(),
8469
+ orphanFiles: number().int().nonnegative()
8470
+ });
8471
+ /**
8472
+ * What one walk claimed, listed, found and (only when armed) forgot.
8473
+ *
8474
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8475
+ * walk that saw a fraction of the location is visible in its own report rather
8476
+ * than in the absence of one.
8477
+ */
8478
+ var LedgerWalkReportSchema = object({
8479
+ locationId: string(),
8480
+ applied: boolean(),
8481
+ refused: LedgerWalkRefusalSchema.nullable(),
8482
+ archiveSegments: number().int().nonnegative().nullable(),
8483
+ archiveBytes: number().int().nonnegative().nullable(),
8484
+ hoursClaimed: number().int().nonnegative(),
8485
+ hoursWalked: number().int().nonnegative(),
8486
+ hoursMissing: number().int().nonnegative(),
8487
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8488
+ listings: number().int().nonnegative(),
8489
+ segmentsClaimed: number().int().nonnegative(),
8490
+ ghostSegments: number().int().nonnegative(),
8491
+ ghostBytes: number().int().nonnegative(),
8492
+ ghostHoursWhole: number().int().nonnegative(),
8493
+ forgottenSegments: number().int().nonnegative(),
8494
+ forgottenBytes: number().int().nonnegative(),
8495
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8496
+ orphanFiles: number().int().nonnegative(),
8497
+ orphanSample: array(string()).readonly(),
8498
+ hoursSkipped: number().int().nonnegative(),
8499
+ skippedByReason: LedgerWalkSkipCountsSchema,
8500
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8501
+ bounded: boolean(),
8502
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8503
+ });
8405
8504
  /** How many rows a media pass would still act on against a given target — the
8406
8505
  * media lane's denominator AND its residue, from ONE derivation so the two can
8407
8506
  * never disagree. `null` = the count could not be taken. */
@@ -18675,13 +18774,15 @@ var ListGroupsPageSchema = object({
18675
18774
  groups: array(AnalyticsGroupRecordSchema).readonly(),
18676
18775
  nextCursor: string().nullable()
18677
18776
  });
18777
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
18778
+ var KEY_EVENTS_MAX_LIMIT = 200;
18678
18779
  var KeyEventQueryInput = object({
18679
18780
  deviceId: number(),
18680
18781
  /** Window lower bound (track firstSeen ≥ since). */
18681
18782
  since: number(),
18682
18783
  /** Window upper bound (track firstSeen ≤ until). */
18683
18784
  until: number(),
18684
- limit: number().int().min(1).max(200).default(50),
18785
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18685
18786
  /** Drop tracks scoring below this importance. */
18686
18787
  minImportance: number().min(0).max(1).optional(),
18687
18788
  /** Restrict to a single class (e.g. 'person'). */
@@ -18703,6 +18804,32 @@ var KeyEventSchema = object({
18703
18804
  ...TrackFlagFields,
18704
18805
  ...TrackRetrainFields
18705
18806
  });
18807
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
18808
+ var KeyEventBatchQueryInput = object({
18809
+ deviceIds: array(number()).min(1).max(200),
18810
+ since: number(),
18811
+ until: number(),
18812
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
18813
+ * across the set, which would let a busy camera starve a quiet one of its
18814
+ * rows and change what the merged feed contains. */
18815
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18816
+ minImportance: number().min(0).max(1).optional(),
18817
+ classFilter: string().optional()
18818
+ });
18819
+ /**
18820
+ * One camera's key events in a batch answer.
18821
+ *
18822
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
18823
+ * error rather than throwing, so a camera whose store read failed and one with
18824
+ * no events in the window were ALREADY indistinguishable per camera — the
18825
+ * batch does not make that worse, and the row keeps the deviceId the single
18826
+ * method's output never carried (the caller used to stamp it from the fan-out
18827
+ * key, which only worked because there was one query per camera).
18828
+ */
18829
+ var KeyEventsForDeviceSchema = object({
18830
+ deviceId: number(),
18831
+ events: array(KeyEventSchema).readonly()
18832
+ });
18706
18833
  object({
18707
18834
  trackId: string(),
18708
18835
  className: string(),
@@ -18774,6 +18901,50 @@ var EventStoreFootprintSchema = object({
18774
18901
  totalBytes: number().int(),
18775
18902
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18776
18903
  });
18904
+ /** Event-media footprint for one {@link MediaFileKind}. */
18905
+ var EventMediaKindFootprintSchema = object({
18906
+ kind: MediaFileKindEnum,
18907
+ /** Media rows of this kind. */
18908
+ rows: number().int(),
18909
+ /** Bytes on disk held by those rows. */
18910
+ bytes: number().int()
18911
+ });
18912
+ /**
18913
+ * The media footprint broken down by KIND — the axis a deletion decision
18914
+ * actually turns on.
18915
+ *
18916
+ * A byte total says how much there is; it cannot say what is safe to remove.
18917
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18918
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18919
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18920
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18921
+ * nothing else, so sizing a deletion means summing per kind.
18922
+ *
18923
+ * ## Why `unaccounted*` exists
18924
+ *
18925
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18926
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18927
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18928
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18929
+ * retired code path, or by a version that knew a kind this one does not) would
18930
+ * otherwise vanish from the total silently, and an operator would delete
18931
+ * against a denominator smaller than the disk.
18932
+ *
18933
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18934
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18935
+ */
18936
+ var EventMediaKindBreakdownSchema = object({
18937
+ /** Every media row in scope, from one unfiltered aggregate. */
18938
+ totalRows: number().int(),
18939
+ /** Every media byte in scope, from that same aggregate. */
18940
+ totalBytes: number().int(),
18941
+ /** Per-kind footprint, ordered by bytes descending. */
18942
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18943
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18944
+ unaccountedRows: number().int(),
18945
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18946
+ unaccountedBytes: number().int()
18947
+ });
18777
18948
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18778
18949
  var EventPruneCountsSchema = object({
18779
18950
  motion: number().int(),
@@ -18928,7 +19099,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18928
19099
  until: number().optional(),
18929
19100
  kinds: array(string()).optional(),
18930
19101
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
18931
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19102
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
18932
19103
  deviceId: number(),
18933
19104
  since: number(),
18934
19105
  until: number(),
@@ -18977,6 +19148,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18977
19148
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
18978
19149
  kind: "query",
18979
19150
  auth: "admin"
19151
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19152
+ kind: "query",
19153
+ auth: "admin"
18980
19154
  }), method(object({
18981
19155
  olderThanMs: number(),
18982
19156
  reason: OpsLogReasonSchema.optional()
@@ -20941,6 +21115,20 @@ method(object({
20941
21115
  error: string().optional()
20942
21116
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20943
21117
  providerId: string(),
21118
+ /**
21119
+ * The location this config is an UNSAVED edit of, when there is one.
21120
+ *
21121
+ * `listLocations` replaces every declared secret with the redaction
21122
+ * sentinel, so the edit modal's form state holds the sentinel for any
21123
+ * credential the operator did not retype — and posting that here
21124
+ * without a way to resolve it makes the provider try to authenticate
21125
+ * as `__camstack_redacted__` and report the operator's own working
21126
+ * password as wrong. Given this id, the orchestrator restores each
21127
+ * sentinel from the stored config (same rule as `upsertLocation`)
21128
+ * before dispatching. Omitted by the "Add location" wizard, where
21129
+ * every value was typed just now and nothing is stored yet.
21130
+ */
21131
+ locationId: string().optional(),
20944
21132
  config: record(string(), unknown())
20945
21133
  }), object({
20946
21134
  ok: boolean(),
@@ -26576,6 +26764,9 @@ method(object({
26576
26764
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26577
26765
  kind: "query",
26578
26766
  auth: "admin"
26767
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
26768
+ kind: "mutation",
26769
+ auth: "admin"
26579
26770
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26580
26771
  kind: "mutation",
26581
26772
  auth: "admin"
@@ -26967,7 +27158,26 @@ var SceneMonitorStatusSchema = object({
26967
27158
  monitors: array(SceneMonitorSchema),
26968
27159
  lastFetchedAt: number()
26969
27160
  });
26970
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
27161
+ /**
27162
+ * One camera's row in a `listScenesBatch` answer.
27163
+ *
27164
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27165
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27166
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27167
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27168
+ * configured. Fanned out per camera the difference was visible — one query
27169
+ * errored while the others resolved — and a batch that returned only the rows
27170
+ * it managed would have destroyed it, silently, by making an unreachable camera
27171
+ * indistinguishable from one that answered `monitors: []`.
27172
+ *
27173
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27174
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27175
+ */
27176
+ var SceneMonitorStatusForDeviceSchema = object({
27177
+ deviceId: number(),
27178
+ status: SceneMonitorStatusSchema.nullable()
27179
+ });
27180
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
26971
27181
  deviceId: number(),
26972
27182
  label: string(),
26973
27183
  roi: MaskRectShapeSchema,
@@ -28291,6 +28501,27 @@ var CameraOccupancySnapshotSchema = object({
28291
28501
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
28292
28502
  });
28293
28503
  /**
28504
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
28505
+ *
28506
+ * THREE outcomes, and the single-camera method could only express two of them
28507
+ * because `snapshot: null` was already spoken for:
28508
+ *
28509
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
28510
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
28511
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
28512
+ * - `read: 'unreadable'` — the owner could not answer for this
28513
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
28514
+ *
28515
+ * Collapsing the last two is the failure this field exists to prevent: a
28516
+ * hydration that threw would otherwise render as an empty Stationary section,
28517
+ * which is a definite claim about a camera nobody could read.
28518
+ */
28519
+ var CameraOccupancySnapshotForDeviceSchema = object({
28520
+ deviceId: number(),
28521
+ read: _enum(["read", "unreadable"]),
28522
+ snapshot: CameraOccupancySnapshotSchema.nullable()
28523
+ });
28524
+ /**
28294
28525
  * Time-series resolution. The history methods return one bucket per
28295
28526
  * step over the requested range. Smaller resolutions cost more
28296
28527
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -28314,7 +28545,7 @@ var HistoryPointSchema = object({
28314
28545
  /** Object count averaged over the bucket (rounded to nearest integer). */
28315
28546
  count: number().int().nonnegative()
28316
28547
  });
28317
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
28548
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
28318
28549
  deviceId: number(),
28319
28550
  zoneId: string(),
28320
28551
  className: string().optional()
@@ -31595,6 +31826,12 @@ Object.freeze({
31595
31826
  addonId: null,
31596
31827
  access: "view"
31597
31828
  },
31829
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31830
+ capName: "pipeline-analytics",
31831
+ capScope: "device",
31832
+ addonId: null,
31833
+ access: "view"
31834
+ },
31598
31835
  "pipelineAnalytics.getEventStoreFootprint": {
31599
31836
  capName: "pipeline-analytics",
31600
31837
  capScope: "device",
@@ -31613,6 +31850,12 @@ Object.freeze({
31613
31850
  addonId: null,
31614
31851
  access: "view"
31615
31852
  },
31853
+ "pipelineAnalytics.getKeyEventsBatch": {
31854
+ capName: "pipeline-analytics",
31855
+ capScope: "device",
31856
+ addonId: null,
31857
+ access: "view"
31858
+ },
31616
31859
  "pipelineAnalytics.getMotionEvents": {
31617
31860
  capName: "pipeline-analytics",
31618
31861
  capScope: "device",
@@ -32789,6 +33032,12 @@ Object.freeze({
32789
33032
  addonId: null,
32790
33033
  access: "view"
32791
33034
  },
33035
+ "recording.reconcileLedgerAgainstDisk": {
33036
+ capName: "recording",
33037
+ capScope: "system",
33038
+ addonId: null,
33039
+ access: "create"
33040
+ },
32792
33041
  "recording.refreshStorageLocationsForMigration": {
32793
33042
  capName: "recording",
32794
33043
  capScope: "system",
@@ -32915,6 +33164,12 @@ Object.freeze({
32915
33164
  addonId: null,
32916
33165
  access: "view"
32917
33166
  },
33167
+ "sceneMonitor.listScenesBatch": {
33168
+ capName: "scene-monitor",
33169
+ capScope: "device",
33170
+ addonId: null,
33171
+ access: "view"
33172
+ },
32918
33173
  "sceneMonitor.recheckNow": {
32919
33174
  capName: "scene-monitor",
32920
33175
  capScope: "device",
@@ -34271,6 +34526,12 @@ Object.freeze({
34271
34526
  addonId: null,
34272
34527
  access: "view"
34273
34528
  },
34529
+ "zoneAnalytics.getCurrentSnapshotBatch": {
34530
+ capName: "zone-analytics",
34531
+ capScope: "device",
34532
+ addonId: null,
34533
+ access: "view"
34534
+ },
34274
34535
  "zoneAnalytics.getUnzonedHistory": {
34275
34536
  capName: "zone-analytics",
34276
34537
  capScope: "device",
@@ -35260,6 +35521,11 @@ Object.freeze({
35260
35521
  form: "single",
35261
35522
  optional: false
35262
35523
  }],
35524
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35525
+ name: "deviceId",
35526
+ form: "single",
35527
+ optional: true
35528
+ }],
35263
35529
  "pipelineAnalytics.getGroup": [{
35264
35530
  name: "deviceId",
35265
35531
  form: "single",
@@ -35270,6 +35536,11 @@ Object.freeze({
35270
35536
  form: "single",
35271
35537
  optional: false
35272
35538
  }],
35539
+ "pipelineAnalytics.getKeyEventsBatch": [{
35540
+ name: "deviceIds",
35541
+ form: "array",
35542
+ optional: false
35543
+ }],
35273
35544
  "pipelineAnalytics.getMotionEvents": [{
35274
35545
  name: "deviceId",
35275
35546
  form: "single",
@@ -35710,6 +35981,11 @@ Object.freeze({
35710
35981
  form: "single",
35711
35982
  optional: false
35712
35983
  }],
35984
+ "recording.reconcileLedgerAgainstDisk": [{
35985
+ name: "deviceId",
35986
+ form: "single",
35987
+ optional: true
35988
+ }],
35713
35989
  "recording.relocateFootage": [{
35714
35990
  name: "deviceId",
35715
35991
  form: "single",
@@ -35775,6 +36051,11 @@ Object.freeze({
35775
36051
  form: "single",
35776
36052
  optional: false
35777
36053
  }],
36054
+ "sceneMonitor.listScenesBatch": [{
36055
+ name: "deviceIds",
36056
+ form: "array",
36057
+ optional: false
36058
+ }],
35778
36059
  "sceneMonitor.recheckNow": [{
35779
36060
  name: "deviceId",
35780
36061
  form: "single",
@@ -36036,6 +36317,11 @@ Object.freeze({
36036
36317
  form: "single",
36037
36318
  optional: false
36038
36319
  }],
36320
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
36321
+ name: "deviceIds",
36322
+ form: "array",
36323
+ optional: false
36324
+ }],
36039
36325
  "zoneAnalytics.getUnzonedHistory": [{
36040
36326
  name: "deviceId",
36041
36327
  form: "single",
@@ -8037,6 +8037,20 @@ var RelocateJobSchema = object({
8037
8037
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8038
8038
  */
8039
8039
  rowsReconciled: number().int().nonnegative().optional(),
8040
+ /**
8041
+ * Rows this run FORGOT because the file they name is not on disk.
8042
+ *
8043
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8044
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8045
+ * and the durable row is dropped through the same channel eviction uses. It
8046
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8047
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8048
+ * the same failure as one that quietly skips them (D295).
8049
+ *
8050
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8051
+ * ledger claimed 5.65 GB of footage that no longer existed.
8052
+ */
8053
+ rowsForgotten: number().int().nonnegative().optional(),
8040
8054
  startedAt: number(),
8041
8055
  finishedAt: number().nullable(),
8042
8056
  error: string().nullable()
@@ -8400,6 +8414,91 @@ var RelocateResidueSchema = object({
8400
8414
  segments: number().int().nonnegative(),
8401
8415
  bytes: number().int().nonnegative()
8402
8416
  }).nullable();
8417
+ /**
8418
+ * Ask one location whether its durable hour rows describe the disk — the walk
8419
+ * (D319).
8420
+ *
8421
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8422
+ * missing tool is the question, and the dry run is how they sanity-check the
8423
+ * destructive run before authorising it.
8424
+ */
8425
+ var LedgerWalkInputSchema = object({
8426
+ locationId: string().min(1),
8427
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8428
+ apply: boolean().optional(),
8429
+ /** Narrow to one camera. */
8430
+ deviceId: number().int().positive().optional(),
8431
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8432
+ profiles: array(string().min(1)).optional()
8433
+ });
8434
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8435
+ var LedgerWalkRefusalSchema = _enum([
8436
+ "location-unknown",
8437
+ "source-writable",
8438
+ "no-ledger",
8439
+ "archive-unreadable",
8440
+ "anchor-absent",
8441
+ "anchor-unreadable",
8442
+ "anchor-moved"
8443
+ ]);
8444
+ _enum([
8445
+ "live-tail",
8446
+ "listing-error",
8447
+ "path-mismatch",
8448
+ "durable-refused"
8449
+ ]);
8450
+ /** Every skip reason, always present, always a number — so a reason that never
8451
+ * fired reports as zero rather than absent and the report shape is constant
8452
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8453
+ var LedgerWalkSkipCountsSchema = object({
8454
+ "live-tail": number().int().nonnegative(),
8455
+ "listing-error": number().int().nonnegative(),
8456
+ "path-mismatch": number().int().nonnegative(),
8457
+ "durable-refused": number().int().nonnegative()
8458
+ });
8459
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8460
+ var LedgerWalkDeviceReportSchema = object({
8461
+ deviceId: number().int(),
8462
+ hoursWalked: number().int().nonnegative(),
8463
+ hoursMissing: number().int().nonnegative(),
8464
+ ghostSegments: number().int().nonnegative(),
8465
+ ghostBytes: number().int().nonnegative(),
8466
+ forgottenSegments: number().int().nonnegative(),
8467
+ orphanFiles: number().int().nonnegative()
8468
+ });
8469
+ /**
8470
+ * What one walk claimed, listed, found and (only when armed) forgot.
8471
+ *
8472
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8473
+ * walk that saw a fraction of the location is visible in its own report rather
8474
+ * than in the absence of one.
8475
+ */
8476
+ var LedgerWalkReportSchema = object({
8477
+ locationId: string(),
8478
+ applied: boolean(),
8479
+ refused: LedgerWalkRefusalSchema.nullable(),
8480
+ archiveSegments: number().int().nonnegative().nullable(),
8481
+ archiveBytes: number().int().nonnegative().nullable(),
8482
+ hoursClaimed: number().int().nonnegative(),
8483
+ hoursWalked: number().int().nonnegative(),
8484
+ hoursMissing: number().int().nonnegative(),
8485
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8486
+ listings: number().int().nonnegative(),
8487
+ segmentsClaimed: number().int().nonnegative(),
8488
+ ghostSegments: number().int().nonnegative(),
8489
+ ghostBytes: number().int().nonnegative(),
8490
+ ghostHoursWhole: number().int().nonnegative(),
8491
+ forgottenSegments: number().int().nonnegative(),
8492
+ forgottenBytes: number().int().nonnegative(),
8493
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8494
+ orphanFiles: number().int().nonnegative(),
8495
+ orphanSample: array(string()).readonly(),
8496
+ hoursSkipped: number().int().nonnegative(),
8497
+ skippedByReason: LedgerWalkSkipCountsSchema,
8498
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8499
+ bounded: boolean(),
8500
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8501
+ });
8403
8502
  /** How many rows a media pass would still act on against a given target — the
8404
8503
  * media lane's denominator AND its residue, from ONE derivation so the two can
8405
8504
  * never disagree. `null` = the count could not be taken. */
@@ -18673,13 +18772,15 @@ var ListGroupsPageSchema = object({
18673
18772
  groups: array(AnalyticsGroupRecordSchema).readonly(),
18674
18773
  nextCursor: string().nullable()
18675
18774
  });
18775
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
18776
+ var KEY_EVENTS_MAX_LIMIT = 200;
18676
18777
  var KeyEventQueryInput = object({
18677
18778
  deviceId: number(),
18678
18779
  /** Window lower bound (track firstSeen ≥ since). */
18679
18780
  since: number(),
18680
18781
  /** Window upper bound (track firstSeen ≤ until). */
18681
18782
  until: number(),
18682
- limit: number().int().min(1).max(200).default(50),
18783
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18683
18784
  /** Drop tracks scoring below this importance. */
18684
18785
  minImportance: number().min(0).max(1).optional(),
18685
18786
  /** Restrict to a single class (e.g. 'person'). */
@@ -18701,6 +18802,32 @@ var KeyEventSchema = object({
18701
18802
  ...TrackFlagFields,
18702
18803
  ...TrackRetrainFields
18703
18804
  });
18805
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
18806
+ var KeyEventBatchQueryInput = object({
18807
+ deviceIds: array(number()).min(1).max(200),
18808
+ since: number(),
18809
+ until: number(),
18810
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
18811
+ * across the set, which would let a busy camera starve a quiet one of its
18812
+ * rows and change what the merged feed contains. */
18813
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18814
+ minImportance: number().min(0).max(1).optional(),
18815
+ classFilter: string().optional()
18816
+ });
18817
+ /**
18818
+ * One camera's key events in a batch answer.
18819
+ *
18820
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
18821
+ * error rather than throwing, so a camera whose store read failed and one with
18822
+ * no events in the window were ALREADY indistinguishable per camera — the
18823
+ * batch does not make that worse, and the row keeps the deviceId the single
18824
+ * method's output never carried (the caller used to stamp it from the fan-out
18825
+ * key, which only worked because there was one query per camera).
18826
+ */
18827
+ var KeyEventsForDeviceSchema = object({
18828
+ deviceId: number(),
18829
+ events: array(KeyEventSchema).readonly()
18830
+ });
18704
18831
  object({
18705
18832
  trackId: string(),
18706
18833
  className: string(),
@@ -18772,6 +18899,50 @@ var EventStoreFootprintSchema = object({
18772
18899
  totalBytes: number().int(),
18773
18900
  devices: array(EventStoreDeviceFootprintSchema).readonly()
18774
18901
  });
18902
+ /** Event-media footprint for one {@link MediaFileKind}. */
18903
+ var EventMediaKindFootprintSchema = object({
18904
+ kind: MediaFileKindEnum,
18905
+ /** Media rows of this kind. */
18906
+ rows: number().int(),
18907
+ /** Bytes on disk held by those rows. */
18908
+ bytes: number().int()
18909
+ });
18910
+ /**
18911
+ * The media footprint broken down by KIND — the axis a deletion decision
18912
+ * actually turns on.
18913
+ *
18914
+ * A byte total says how much there is; it cannot say what is safe to remove.
18915
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
18916
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
18917
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
18918
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
18919
+ * nothing else, so sizing a deletion means summing per kind.
18920
+ *
18921
+ * ## Why `unaccounted*` exists
18922
+ *
18923
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
18924
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
18925
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
18926
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
18927
+ * retired code path, or by a version that knew a kind this one does not) would
18928
+ * otherwise vanish from the total silently, and an operator would delete
18929
+ * against a denominator smaller than the disk.
18930
+ *
18931
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
18932
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
18933
+ */
18934
+ var EventMediaKindBreakdownSchema = object({
18935
+ /** Every media row in scope, from one unfiltered aggregate. */
18936
+ totalRows: number().int(),
18937
+ /** Every media byte in scope, from that same aggregate. */
18938
+ totalBytes: number().int(),
18939
+ /** Per-kind footprint, ordered by bytes descending. */
18940
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
18941
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
18942
+ unaccountedRows: number().int(),
18943
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
18944
+ unaccountedBytes: number().int()
18945
+ });
18775
18946
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
18776
18947
  var EventPruneCountsSchema = object({
18777
18948
  motion: number().int(),
@@ -18926,7 +19097,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18926
19097
  until: number().optional(),
18927
19098
  kinds: array(string()).optional(),
18928
19099
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
18929
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19100
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
18930
19101
  deviceId: number(),
18931
19102
  since: number(),
18932
19103
  until: number(),
@@ -18975,6 +19146,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18975
19146
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
18976
19147
  kind: "query",
18977
19148
  auth: "admin"
19149
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19150
+ kind: "query",
19151
+ auth: "admin"
18978
19152
  }), method(object({
18979
19153
  olderThanMs: number(),
18980
19154
  reason: OpsLogReasonSchema.optional()
@@ -20939,6 +21113,20 @@ method(object({
20939
21113
  error: string().optional()
20940
21114
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
20941
21115
  providerId: string(),
21116
+ /**
21117
+ * The location this config is an UNSAVED edit of, when there is one.
21118
+ *
21119
+ * `listLocations` replaces every declared secret with the redaction
21120
+ * sentinel, so the edit modal's form state holds the sentinel for any
21121
+ * credential the operator did not retype — and posting that here
21122
+ * without a way to resolve it makes the provider try to authenticate
21123
+ * as `__camstack_redacted__` and report the operator's own working
21124
+ * password as wrong. Given this id, the orchestrator restores each
21125
+ * sentinel from the stored config (same rule as `upsertLocation`)
21126
+ * before dispatching. Omitted by the "Add location" wizard, where
21127
+ * every value was typed just now and nothing is stored yet.
21128
+ */
21129
+ locationId: string().optional(),
20942
21130
  config: record(string(), unknown())
20943
21131
  }), object({
20944
21132
  ok: boolean(),
@@ -26574,6 +26762,9 @@ method(object({
26574
26762
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26575
26763
  kind: "query",
26576
26764
  auth: "admin"
26765
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
26766
+ kind: "mutation",
26767
+ auth: "admin"
26577
26768
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26578
26769
  kind: "mutation",
26579
26770
  auth: "admin"
@@ -26965,7 +27156,26 @@ var SceneMonitorStatusSchema = object({
26965
27156
  monitors: array(SceneMonitorSchema),
26966
27157
  lastFetchedAt: number()
26967
27158
  });
26968
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
27159
+ /**
27160
+ * One camera's row in a `listScenesBatch` answer.
27161
+ *
27162
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27163
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27164
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27165
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27166
+ * configured. Fanned out per camera the difference was visible — one query
27167
+ * errored while the others resolved — and a batch that returned only the rows
27168
+ * it managed would have destroyed it, silently, by making an unreachable camera
27169
+ * indistinguishable from one that answered `monitors: []`.
27170
+ *
27171
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27172
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27173
+ */
27174
+ var SceneMonitorStatusForDeviceSchema = object({
27175
+ deviceId: number(),
27176
+ status: SceneMonitorStatusSchema.nullable()
27177
+ });
27178
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
26969
27179
  deviceId: number(),
26970
27180
  label: string(),
26971
27181
  roi: MaskRectShapeSchema,
@@ -28289,6 +28499,27 @@ var CameraOccupancySnapshotSchema = object({
28289
28499
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
28290
28500
  });
28291
28501
  /**
28502
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
28503
+ *
28504
+ * THREE outcomes, and the single-camera method could only express two of them
28505
+ * because `snapshot: null` was already spoken for:
28506
+ *
28507
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
28508
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
28509
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
28510
+ * - `read: 'unreadable'` — the owner could not answer for this
28511
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
28512
+ *
28513
+ * Collapsing the last two is the failure this field exists to prevent: a
28514
+ * hydration that threw would otherwise render as an empty Stationary section,
28515
+ * which is a definite claim about a camera nobody could read.
28516
+ */
28517
+ var CameraOccupancySnapshotForDeviceSchema = object({
28518
+ deviceId: number(),
28519
+ read: _enum(["read", "unreadable"]),
28520
+ snapshot: CameraOccupancySnapshotSchema.nullable()
28521
+ });
28522
+ /**
28292
28523
  * Time-series resolution. The history methods return one bucket per
28293
28524
  * step over the requested range. Smaller resolutions cost more
28294
28525
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -28312,7 +28543,7 @@ var HistoryPointSchema = object({
28312
28543
  /** Object count averaged over the bucket (rounded to nearest integer). */
28313
28544
  count: number().int().nonnegative()
28314
28545
  });
28315
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
28546
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
28316
28547
  deviceId: number(),
28317
28548
  zoneId: string(),
28318
28549
  className: string().optional()
@@ -31593,6 +31824,12 @@ Object.freeze({
31593
31824
  addonId: null,
31594
31825
  access: "view"
31595
31826
  },
31827
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31828
+ capName: "pipeline-analytics",
31829
+ capScope: "device",
31830
+ addonId: null,
31831
+ access: "view"
31832
+ },
31596
31833
  "pipelineAnalytics.getEventStoreFootprint": {
31597
31834
  capName: "pipeline-analytics",
31598
31835
  capScope: "device",
@@ -31611,6 +31848,12 @@ Object.freeze({
31611
31848
  addonId: null,
31612
31849
  access: "view"
31613
31850
  },
31851
+ "pipelineAnalytics.getKeyEventsBatch": {
31852
+ capName: "pipeline-analytics",
31853
+ capScope: "device",
31854
+ addonId: null,
31855
+ access: "view"
31856
+ },
31614
31857
  "pipelineAnalytics.getMotionEvents": {
31615
31858
  capName: "pipeline-analytics",
31616
31859
  capScope: "device",
@@ -32787,6 +33030,12 @@ Object.freeze({
32787
33030
  addonId: null,
32788
33031
  access: "view"
32789
33032
  },
33033
+ "recording.reconcileLedgerAgainstDisk": {
33034
+ capName: "recording",
33035
+ capScope: "system",
33036
+ addonId: null,
33037
+ access: "create"
33038
+ },
32790
33039
  "recording.refreshStorageLocationsForMigration": {
32791
33040
  capName: "recording",
32792
33041
  capScope: "system",
@@ -32913,6 +33162,12 @@ Object.freeze({
32913
33162
  addonId: null,
32914
33163
  access: "view"
32915
33164
  },
33165
+ "sceneMonitor.listScenesBatch": {
33166
+ capName: "scene-monitor",
33167
+ capScope: "device",
33168
+ addonId: null,
33169
+ access: "view"
33170
+ },
32916
33171
  "sceneMonitor.recheckNow": {
32917
33172
  capName: "scene-monitor",
32918
33173
  capScope: "device",
@@ -34269,6 +34524,12 @@ Object.freeze({
34269
34524
  addonId: null,
34270
34525
  access: "view"
34271
34526
  },
34527
+ "zoneAnalytics.getCurrentSnapshotBatch": {
34528
+ capName: "zone-analytics",
34529
+ capScope: "device",
34530
+ addonId: null,
34531
+ access: "view"
34532
+ },
34272
34533
  "zoneAnalytics.getUnzonedHistory": {
34273
34534
  capName: "zone-analytics",
34274
34535
  capScope: "device",
@@ -35258,6 +35519,11 @@ Object.freeze({
35258
35519
  form: "single",
35259
35520
  optional: false
35260
35521
  }],
35522
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35523
+ name: "deviceId",
35524
+ form: "single",
35525
+ optional: true
35526
+ }],
35261
35527
  "pipelineAnalytics.getGroup": [{
35262
35528
  name: "deviceId",
35263
35529
  form: "single",
@@ -35268,6 +35534,11 @@ Object.freeze({
35268
35534
  form: "single",
35269
35535
  optional: false
35270
35536
  }],
35537
+ "pipelineAnalytics.getKeyEventsBatch": [{
35538
+ name: "deviceIds",
35539
+ form: "array",
35540
+ optional: false
35541
+ }],
35271
35542
  "pipelineAnalytics.getMotionEvents": [{
35272
35543
  name: "deviceId",
35273
35544
  form: "single",
@@ -35708,6 +35979,11 @@ Object.freeze({
35708
35979
  form: "single",
35709
35980
  optional: false
35710
35981
  }],
35982
+ "recording.reconcileLedgerAgainstDisk": [{
35983
+ name: "deviceId",
35984
+ form: "single",
35985
+ optional: true
35986
+ }],
35711
35987
  "recording.relocateFootage": [{
35712
35988
  name: "deviceId",
35713
35989
  form: "single",
@@ -35773,6 +36049,11 @@ Object.freeze({
35773
36049
  form: "single",
35774
36050
  optional: false
35775
36051
  }],
36052
+ "sceneMonitor.listScenesBatch": [{
36053
+ name: "deviceIds",
36054
+ form: "array",
36055
+ optional: false
36056
+ }],
35776
36057
  "sceneMonitor.recheckNow": [{
35777
36058
  name: "deviceId",
35778
36059
  form: "single",
@@ -36034,6 +36315,11 @@ Object.freeze({
36034
36315
  form: "single",
36035
36316
  optional: false
36036
36317
  }],
36318
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
36319
+ name: "deviceIds",
36320
+ form: "array",
36321
+ optional: false
36322
+ }],
36037
36323
  "zoneAnalytics.getUnzonedHistory": [{
36038
36324
  name: "deviceId",
36039
36325
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-smtp-nodemailer",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "SMTP email provider addon for CamStack — wraps `nodemailer` and registers a `smtp-provider` cap collection entry. Used by magic-link login + notifier addons.",
5
5
  "keywords": [
6
6
  "camstack",