@camstack/addon-smtp-nodemailer 1.2.26 → 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.
@@ -5838,6 +5838,13 @@ var BaseAddon = class {
5838
5838
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5839
5839
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5840
5840
  _registeredCapNames = [];
5841
+ /**
5842
+ * True only after `readAddonStore` actually answered. Constructor
5843
+ * defaults look like stored config when the store is down — a forked
5844
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5845
+ * mode, 2026-08-25) is not "the operator chose this".
5846
+ */
5847
+ settingsStoreReady = false;
5841
5848
  /** Default config values. Provided via constructor. */
5842
5849
  defaults;
5843
5850
  constructor(defaults) {
@@ -6238,7 +6245,9 @@ var BaseAddon = class {
6238
6245
  ];
6239
6246
  let lastErr;
6240
6247
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6241
- return await settings.readAddonStore() ?? {};
6248
+ const stored = await settings.readAddonStore() ?? {};
6249
+ this.settingsStoreReady = true;
6250
+ return stored;
6242
6251
  } catch (err) {
6243
6252
  lastErr = err;
6244
6253
  const msg = err instanceof Error ? err.message : String(err);
@@ -6246,6 +6255,7 @@ var BaseAddon = class {
6246
6255
  if (attempt === delaysMs.length) break;
6247
6256
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6248
6257
  }
6258
+ this.settingsStoreReady = false;
6249
6259
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6250
6260
  return {};
6251
6261
  }
@@ -8133,6 +8143,12 @@ var ModelVariantGroupSchema = object({
8133
8143
  */
8134
8144
  resolution: number().int().positive().optional()
8135
8145
  });
8146
+ var ModelProviderIdSchema = _enum([
8147
+ "camstack",
8148
+ "frigate",
8149
+ "scrypted",
8150
+ "custom"
8151
+ ]);
8136
8152
  var ModelCatalogEntrySchema = object({
8137
8153
  id: string(),
8138
8154
  name: string(),
@@ -8230,6 +8246,12 @@ var ModelCatalogEntrySchema = object({
8230
8246
  */
8231
8247
  group: ModelVariantGroupSchema.optional(),
8232
8248
  /**
8249
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8250
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8251
+ * persisted before this field existed (`inferModelProvider` fills those).
8252
+ */
8253
+ provider: ModelProviderIdSchema.optional(),
8254
+ /**
8233
8255
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8234
8256
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8235
8257
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11757,6 +11779,27 @@ var LinkedDeviceSchema = object({
11757
11779
  features: array(string()),
11758
11780
  producesTrackedEvents: boolean().optional()
11759
11781
  });
11782
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11783
+ * The batch answer needs the tag; the single-device answer already has it
11784
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11785
+ var LinkedDevicesForDeviceSchema = object({
11786
+ deviceId: number(),
11787
+ mode: LinkedDevicesModeSchema,
11788
+ devices: array(LinkedDeviceSchema)
11789
+ });
11790
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11791
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11792
+ * object literal is exactly how the three drift apart. */
11793
+ var DeviceBindingsForDeviceSchema = object({
11794
+ deviceId: number(),
11795
+ entries: array(object({
11796
+ capName: string(),
11797
+ kind: _enum(["native", "wrapped"]),
11798
+ providerAddonId: string(),
11799
+ providerNodeId: string(),
11800
+ nativeAddonId: string()
11801
+ }))
11802
+ });
11760
11803
  var SavedDeviceRowSchema = object({
11761
11804
  /** Numeric id reserved at allocateDeviceId time. */
11762
11805
  id: number(),
@@ -11982,11 +12025,25 @@ method(object({
11982
12025
  projection: _enum(["full", "slim"]).optional(),
11983
12026
  /** Return only camera devices. Filtering server-side instead of
11984
12027
  * shipping 293 rows to find 12. */
11985
- isCamera: boolean().optional()
12028
+ isCamera: boolean().optional(),
12029
+ /**
12030
+ * Return only these device ids. For the caller that already KNOWS the
12031
+ * handful it wants and needs a field the id-bearing answer does not
12032
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12033
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12034
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12035
+ * refetches on the reconcile interval, on a phone.
12036
+ *
12037
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12038
+ * keys rather than rejecting them (verified against the live hub
12039
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12040
+ * it answers today and the caller filters as it already does.
12041
+ */
12042
+ deviceIds: array(number()).optional()
11986
12043
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
11987
12044
  mode: LinkedDevicesModeSchema,
11988
12045
  devices: array(LinkedDeviceSchema)
11989
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12046
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
11990
12047
  deviceId: number(),
11991
12048
  values: record(string(), unknown())
11992
12049
  }), object({ success: literal(true) }), {
@@ -12013,25 +12070,7 @@ method(object({
12013
12070
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12014
12071
  kind: "mutation",
12015
12072
  auth: "admin"
12016
- }), method(object({ deviceId: number() }), object({
12017
- deviceId: number(),
12018
- entries: array(object({
12019
- capName: string(),
12020
- kind: _enum(["native", "wrapped"]),
12021
- providerAddonId: string(),
12022
- providerNodeId: string(),
12023
- nativeAddonId: string()
12024
- }))
12025
- })), method(object({}), array(object({
12026
- deviceId: number(),
12027
- entries: array(object({
12028
- capName: string(),
12029
- kind: _enum(["native", "wrapped"]),
12030
- providerAddonId: string(),
12031
- providerNodeId: string(),
12032
- nativeAddonId: string()
12033
- }))
12034
- }))), method(object({
12073
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12035
12074
  deviceId: number(),
12036
12075
  capName: string(),
12037
12076
  wrapperAddonId: string(),
@@ -14403,12 +14442,15 @@ var NcOccupancyConditionSchema = object({
14403
14442
  * there is no second switch that can disagree with the first and every rule
14404
14443
  * authored before the decision migrates for free (`audioModeOf`):
14405
14444
  *
14406
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14407
- * classifier labels with one of them. No window, no percentage:
14408
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14409
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14410
- * the analyzer's (`classificationMinScore`, per device) a label only
14411
- * reaches this condition if the classifier was already confident enough.
14445
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14446
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14447
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14448
+ * frames is the wrong question for a classifier that labels 1–3 frames
14449
+ * per episode. The count window is the brake that drops a single-frame
14450
+ * false positive; the rule's own `throttle` cooldown is the other. The
14451
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14452
+ * per device) — a label only reaches this condition if the classifier was
14453
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14412
14454
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14413
14455
  * the condition: at least `hitPercent`% of the samples over
14414
14456
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14435,14 +14477,22 @@ var NcOccupancyConditionSchema = object({
14435
14477
  * an operator who typed `dog` mean the same thing.
14436
14478
  */
14437
14479
  var NcAudioConditionSchema = object({
14438
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14480
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14439
14481
  labels: array(string().min(1)).min(1).optional(),
14440
14482
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14441
14483
  dbThreshold: number().min(-96).max(0).optional(),
14442
14484
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14443
14485
  hitPercent: number().int().min(1).max(100).default(60),
14444
14486
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14445
- samplingSeconds: number().int().min(1).max(300).default(10)
14487
+ samplingSeconds: number().int().min(1).max(300).default(10),
14488
+ /**
14489
+ * LABEL MODE: how many labelled frames must land inside
14490
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14491
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14492
+ */
14493
+ confirmHits: number().int().min(1).max(20).optional(),
14494
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14495
+ confirmWindowSec: number().int().min(1).max(60).optional()
14446
14496
  });
14447
14497
  /**
14448
14498
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16816,6 +16866,46 @@ var RecentTracksPageSchema = object({
16816
16866
  /** Cursor for the next page, or null when this page is the last. */
16817
16867
  nextCursor: string().nullable()
16818
16868
  });
16869
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16870
+ var LIST_GROUPS_MAX_LIMIT = 100;
16871
+ var AnalyticsGroupRecordSchema = object({
16872
+ id: string(),
16873
+ deviceId: number().int(),
16874
+ openedAt: number().int(),
16875
+ closedAt: number().int(),
16876
+ timestamp: number().int(),
16877
+ memberCount: number().int(),
16878
+ memberTrackIds: array(string()).readonly(),
16879
+ className: string(),
16880
+ classes: array(string()).readonly(),
16881
+ /** Relative event-media path, or null when the group has no picture yet. */
16882
+ mediaUrl: string().nullable(),
16883
+ singleton: boolean()
16884
+ });
16885
+ var AnalyticsGroupMemberSchema = object({
16886
+ trackId: string(),
16887
+ deviceId: number().int(),
16888
+ className: string(),
16889
+ firstSeen: number().int(),
16890
+ lastSeen: number().int(),
16891
+ mediaUrl: string().nullable()
16892
+ });
16893
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
16894
+ var ListGroupsQueryInput = object({
16895
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
16896
+ deviceIds: array(number()),
16897
+ /** Window lower bound on `closedAt` (inclusive). */
16898
+ since: number().optional(),
16899
+ /** Window upper bound on `openedAt` (inclusive). */
16900
+ until: number().optional(),
16901
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
16902
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
16903
+ cursor: string().optional()
16904
+ });
16905
+ var ListGroupsPageSchema = object({
16906
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
16907
+ nextCursor: string().nullable()
16908
+ });
16819
16909
  var KeyEventQueryInput = object({
16820
16910
  deviceId: number(),
16821
16911
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16891,7 +16981,9 @@ var TrackCascadeCountsSchema = object({
16891
16981
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16892
16982
  plates: number().int(),
16893
16983
  /** Per-track CLIP search vectors removed (best-effort). */
16894
- embeddings: number().int()
16984
+ embeddings: number().int(),
16985
+ /** Group membership + group rows removed with their last member (best-effort). */
16986
+ groups: number().int()
16895
16987
  });
16896
16988
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16897
16989
  var DiskReconcileCountsSchema = object({
@@ -17037,7 +17129,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17037
17129
  * stationary registry). Default false: the timeline lists passages,
17038
17130
  * not parking records (operator decision, 2026-08-15). */
17039
17131
  includeStationary: boolean().optional()
17040
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17132
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17133
+ deviceId: number(),
17134
+ groupId: string().min(1)
17135
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17041
17136
  kind: "mutation",
17042
17137
  auth: "admin"
17043
17138
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17347,7 +17442,8 @@ var PipelineModelOptionSchema = object({
17347
17442
  sizeMB: number()
17348
17443
  })),
17349
17444
  group: ModelVariantGroupSchema.optional(),
17350
- legacy: boolean().optional()
17445
+ legacy: boolean().optional(),
17446
+ provider: ModelProviderIdSchema.optional()
17351
17447
  });
17352
17448
  var ConfigFieldBridge = custom();
17353
17449
  var PipelineAddonSchemaSchema = object({
@@ -23944,7 +24040,12 @@ var PlateInfoSchema = object({
23944
24040
  plateBbox: BoundingBoxSchema.optional(),
23945
24041
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
23946
24042
  keyFrameMediaKey: string().optional(),
23947
- base64: string().optional()
24043
+ base64: string().optional(),
24044
+ /**
24045
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24046
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24047
+ */
24048
+ cropUrl: string().optional()
23948
24049
  });
23949
24050
  var MediaFileLiteSchema = object({
23950
24051
  key: string(),
@@ -27584,6 +27685,12 @@ Object.freeze({
27584
27685
  addonId: null,
27585
27686
  access: "view"
27586
27687
  },
27688
+ "deviceManager.getBindingsBatch": {
27689
+ capName: "device-manager",
27690
+ capScope: "system",
27691
+ addonId: null,
27692
+ access: "view"
27693
+ },
27587
27694
  "deviceManager.getChildren": {
27588
27695
  capName: "device-manager",
27589
27696
  capScope: "system",
@@ -27644,6 +27751,12 @@ Object.freeze({
27644
27751
  addonId: null,
27645
27752
  access: "view"
27646
27753
  },
27754
+ "deviceManager.getLinkedDevicesBatch": {
27755
+ capName: "device-manager",
27756
+ capScope: "system",
27757
+ addonId: null,
27758
+ access: "view"
27759
+ },
27647
27760
  "deviceManager.getRoleDisplayDefaults": {
27648
27761
  capName: "device-manager",
27649
27762
  capScope: "system",
@@ -29414,6 +29527,12 @@ Object.freeze({
29414
29527
  addonId: null,
29415
29528
  access: "view"
29416
29529
  },
29530
+ "pipelineAnalytics.getGroup": {
29531
+ capName: "pipeline-analytics",
29532
+ capScope: "device",
29533
+ addonId: null,
29534
+ access: "view"
29535
+ },
29417
29536
  "pipelineAnalytics.getKeyEvents": {
29418
29537
  capName: "pipeline-analytics",
29419
29538
  capScope: "device",
@@ -29498,6 +29617,12 @@ Object.freeze({
29498
29617
  addonId: null,
29499
29618
  access: "view"
29500
29619
  },
29620
+ "pipelineAnalytics.listGroups": {
29621
+ capName: "pipeline-analytics",
29622
+ capScope: "device",
29623
+ addonId: null,
29624
+ access: "view"
29625
+ },
29501
29626
  "pipelineAnalytics.listOpsLog": {
29502
29627
  capName: "pipeline-analytics",
29503
29628
  capScope: "device",
@@ -32279,6 +32404,11 @@ Object.freeze({
32279
32404
  form: "single",
32280
32405
  optional: false
32281
32406
  }],
32407
+ "deviceManager.getBindingsBatch": [{
32408
+ name: "deviceIds",
32409
+ form: "array",
32410
+ optional: false
32411
+ }],
32282
32412
  "deviceManager.getChildren": [{
32283
32413
  name: "parentDeviceId",
32284
32414
  form: "single",
@@ -32324,6 +32454,11 @@ Object.freeze({
32324
32454
  form: "single",
32325
32455
  optional: false
32326
32456
  }],
32457
+ "deviceManager.getLinkedDevicesBatch": [{
32458
+ name: "deviceIds",
32459
+ form: "array",
32460
+ optional: false
32461
+ }],
32327
32462
  "deviceManager.getSettingsSchema": [{
32328
32463
  name: "deviceId",
32329
32464
  form: "single",
@@ -32344,6 +32479,11 @@ Object.freeze({
32344
32479
  form: "single",
32345
32480
  optional: false
32346
32481
  }],
32482
+ "deviceManager.listAll": [{
32483
+ name: "deviceIds",
32484
+ form: "array",
32485
+ optional: true
32486
+ }],
32347
32487
  "deviceManager.loadConfig": [{
32348
32488
  name: "deviceId",
32349
32489
  form: "single",
@@ -32917,6 +33057,11 @@ Object.freeze({
32917
33057
  form: "single",
32918
33058
  optional: false
32919
33059
  }],
33060
+ "pipelineAnalytics.getGroup": [{
33061
+ name: "deviceId",
33062
+ form: "single",
33063
+ optional: false
33064
+ }],
32920
33065
  "pipelineAnalytics.getKeyEvents": [{
32921
33066
  name: "deviceId",
32922
33067
  form: "single",
@@ -32972,6 +33117,11 @@ Object.freeze({
32972
33117
  form: "array",
32973
33118
  optional: false
32974
33119
  }],
33120
+ "pipelineAnalytics.listGroups": [{
33121
+ name: "deviceIds",
33122
+ form: "array",
33123
+ optional: false
33124
+ }],
32975
33125
  "pipelineAnalytics.listOpsLog": [{
32976
33126
  name: "deviceId",
32977
33127
  form: "single",
@@ -5836,6 +5836,13 @@ var BaseAddon = class {
5836
5836
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5837
5837
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5838
5838
  _registeredCapNames = [];
5839
+ /**
5840
+ * True only after `readAddonStore` actually answered. Constructor
5841
+ * defaults look like stored config when the store is down — a forked
5842
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5843
+ * mode, 2026-08-25) is not "the operator chose this".
5844
+ */
5845
+ settingsStoreReady = false;
5839
5846
  /** Default config values. Provided via constructor. */
5840
5847
  defaults;
5841
5848
  constructor(defaults) {
@@ -6236,7 +6243,9 @@ var BaseAddon = class {
6236
6243
  ];
6237
6244
  let lastErr;
6238
6245
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6239
- return await settings.readAddonStore() ?? {};
6246
+ const stored = await settings.readAddonStore() ?? {};
6247
+ this.settingsStoreReady = true;
6248
+ return stored;
6240
6249
  } catch (err) {
6241
6250
  lastErr = err;
6242
6251
  const msg = err instanceof Error ? err.message : String(err);
@@ -6244,6 +6253,7 @@ var BaseAddon = class {
6244
6253
  if (attempt === delaysMs.length) break;
6245
6254
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6246
6255
  }
6256
+ this.settingsStoreReady = false;
6247
6257
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6248
6258
  return {};
6249
6259
  }
@@ -8131,6 +8141,12 @@ var ModelVariantGroupSchema = object({
8131
8141
  */
8132
8142
  resolution: number().int().positive().optional()
8133
8143
  });
8144
+ var ModelProviderIdSchema = _enum([
8145
+ "camstack",
8146
+ "frigate",
8147
+ "scrypted",
8148
+ "custom"
8149
+ ]);
8134
8150
  var ModelCatalogEntrySchema = object({
8135
8151
  id: string(),
8136
8152
  name: string(),
@@ -8228,6 +8244,12 @@ var ModelCatalogEntrySchema = object({
8228
8244
  */
8229
8245
  group: ModelVariantGroupSchema.optional(),
8230
8246
  /**
8247
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8248
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8249
+ * persisted before this field existed (`inferModelProvider` fills those).
8250
+ */
8251
+ provider: ModelProviderIdSchema.optional(),
8252
+ /**
8231
8253
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8232
8254
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8233
8255
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11755,6 +11777,27 @@ var LinkedDeviceSchema = object({
11755
11777
  features: array(string()),
11756
11778
  producesTrackedEvents: boolean().optional()
11757
11779
  });
11780
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11781
+ * The batch answer needs the tag; the single-device answer already has it
11782
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11783
+ var LinkedDevicesForDeviceSchema = object({
11784
+ deviceId: number(),
11785
+ mode: LinkedDevicesModeSchema,
11786
+ devices: array(LinkedDeviceSchema)
11787
+ });
11788
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11789
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11790
+ * object literal is exactly how the three drift apart. */
11791
+ var DeviceBindingsForDeviceSchema = object({
11792
+ deviceId: number(),
11793
+ entries: array(object({
11794
+ capName: string(),
11795
+ kind: _enum(["native", "wrapped"]),
11796
+ providerAddonId: string(),
11797
+ providerNodeId: string(),
11798
+ nativeAddonId: string()
11799
+ }))
11800
+ });
11758
11801
  var SavedDeviceRowSchema = object({
11759
11802
  /** Numeric id reserved at allocateDeviceId time. */
11760
11803
  id: number(),
@@ -11980,11 +12023,25 @@ method(object({
11980
12023
  projection: _enum(["full", "slim"]).optional(),
11981
12024
  /** Return only camera devices. Filtering server-side instead of
11982
12025
  * shipping 293 rows to find 12. */
11983
- isCamera: boolean().optional()
12026
+ isCamera: boolean().optional(),
12027
+ /**
12028
+ * Return only these device ids. For the caller that already KNOWS the
12029
+ * handful it wants and needs a field the id-bearing answer does not
12030
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12031
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12032
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12033
+ * refetches on the reconcile interval, on a phone.
12034
+ *
12035
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12036
+ * keys rather than rejecting them (verified against the live hub
12037
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12038
+ * it answers today and the caller filters as it already does.
12039
+ */
12040
+ deviceIds: array(number()).optional()
11984
12041
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
11985
12042
  mode: LinkedDevicesModeSchema,
11986
12043
  devices: array(LinkedDeviceSchema)
11987
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12044
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
11988
12045
  deviceId: number(),
11989
12046
  values: record(string(), unknown())
11990
12047
  }), object({ success: literal(true) }), {
@@ -12011,25 +12068,7 @@ method(object({
12011
12068
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12012
12069
  kind: "mutation",
12013
12070
  auth: "admin"
12014
- }), method(object({ deviceId: number() }), object({
12015
- deviceId: number(),
12016
- entries: array(object({
12017
- capName: string(),
12018
- kind: _enum(["native", "wrapped"]),
12019
- providerAddonId: string(),
12020
- providerNodeId: string(),
12021
- nativeAddonId: string()
12022
- }))
12023
- })), method(object({}), array(object({
12024
- deviceId: number(),
12025
- entries: array(object({
12026
- capName: string(),
12027
- kind: _enum(["native", "wrapped"]),
12028
- providerAddonId: string(),
12029
- providerNodeId: string(),
12030
- nativeAddonId: string()
12031
- }))
12032
- }))), method(object({
12071
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12033
12072
  deviceId: number(),
12034
12073
  capName: string(),
12035
12074
  wrapperAddonId: string(),
@@ -14401,12 +14440,15 @@ var NcOccupancyConditionSchema = object({
14401
14440
  * there is no second switch that can disagree with the first and every rule
14402
14441
  * authored before the decision migrates for free (`audioModeOf`):
14403
14442
  *
14404
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14405
- * classifier labels with one of them. No window, no percentage:
14406
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14407
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14408
- * the analyzer's (`classificationMinScore`, per device) a label only
14409
- * reaches this condition if the classifier was already confident enough.
14443
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14444
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14445
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14446
+ * frames is the wrong question for a classifier that labels 1–3 frames
14447
+ * per episode. The count window is the brake that drops a single-frame
14448
+ * false positive; the rule's own `throttle` cooldown is the other. The
14449
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14450
+ * per device) — a label only reaches this condition if the classifier was
14451
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14410
14452
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14411
14453
  * the condition: at least `hitPercent`% of the samples over
14412
14454
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14433,14 +14475,22 @@ var NcOccupancyConditionSchema = object({
14433
14475
  * an operator who typed `dog` mean the same thing.
14434
14476
  */
14435
14477
  var NcAudioConditionSchema = object({
14436
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14478
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14437
14479
  labels: array(string().min(1)).min(1).optional(),
14438
14480
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14439
14481
  dbThreshold: number().min(-96).max(0).optional(),
14440
14482
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14441
14483
  hitPercent: number().int().min(1).max(100).default(60),
14442
14484
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14443
- samplingSeconds: number().int().min(1).max(300).default(10)
14485
+ samplingSeconds: number().int().min(1).max(300).default(10),
14486
+ /**
14487
+ * LABEL MODE: how many labelled frames must land inside
14488
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14489
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14490
+ */
14491
+ confirmHits: number().int().min(1).max(20).optional(),
14492
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14493
+ confirmWindowSec: number().int().min(1).max(60).optional()
14444
14494
  });
14445
14495
  /**
14446
14496
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16814,6 +16864,46 @@ var RecentTracksPageSchema = object({
16814
16864
  /** Cursor for the next page, or null when this page is the last. */
16815
16865
  nextCursor: string().nullable()
16816
16866
  });
16867
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16868
+ var LIST_GROUPS_MAX_LIMIT = 100;
16869
+ var AnalyticsGroupRecordSchema = object({
16870
+ id: string(),
16871
+ deviceId: number().int(),
16872
+ openedAt: number().int(),
16873
+ closedAt: number().int(),
16874
+ timestamp: number().int(),
16875
+ memberCount: number().int(),
16876
+ memberTrackIds: array(string()).readonly(),
16877
+ className: string(),
16878
+ classes: array(string()).readonly(),
16879
+ /** Relative event-media path, or null when the group has no picture yet. */
16880
+ mediaUrl: string().nullable(),
16881
+ singleton: boolean()
16882
+ });
16883
+ var AnalyticsGroupMemberSchema = object({
16884
+ trackId: string(),
16885
+ deviceId: number().int(),
16886
+ className: string(),
16887
+ firstSeen: number().int(),
16888
+ lastSeen: number().int(),
16889
+ mediaUrl: string().nullable()
16890
+ });
16891
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
16892
+ var ListGroupsQueryInput = object({
16893
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
16894
+ deviceIds: array(number()),
16895
+ /** Window lower bound on `closedAt` (inclusive). */
16896
+ since: number().optional(),
16897
+ /** Window upper bound on `openedAt` (inclusive). */
16898
+ until: number().optional(),
16899
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
16900
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
16901
+ cursor: string().optional()
16902
+ });
16903
+ var ListGroupsPageSchema = object({
16904
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
16905
+ nextCursor: string().nullable()
16906
+ });
16817
16907
  var KeyEventQueryInput = object({
16818
16908
  deviceId: number(),
16819
16909
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16889,7 +16979,9 @@ var TrackCascadeCountsSchema = object({
16889
16979
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16890
16980
  plates: number().int(),
16891
16981
  /** Per-track CLIP search vectors removed (best-effort). */
16892
- embeddings: number().int()
16982
+ embeddings: number().int(),
16983
+ /** Group membership + group rows removed with their last member (best-effort). */
16984
+ groups: number().int()
16893
16985
  });
16894
16986
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16895
16987
  var DiskReconcileCountsSchema = object({
@@ -17035,7 +17127,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17035
17127
  * stationary registry). Default false: the timeline lists passages,
17036
17128
  * not parking records (operator decision, 2026-08-15). */
17037
17129
  includeStationary: boolean().optional()
17038
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17130
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17131
+ deviceId: number(),
17132
+ groupId: string().min(1)
17133
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17039
17134
  kind: "mutation",
17040
17135
  auth: "admin"
17041
17136
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17345,7 +17440,8 @@ var PipelineModelOptionSchema = object({
17345
17440
  sizeMB: number()
17346
17441
  })),
17347
17442
  group: ModelVariantGroupSchema.optional(),
17348
- legacy: boolean().optional()
17443
+ legacy: boolean().optional(),
17444
+ provider: ModelProviderIdSchema.optional()
17349
17445
  });
17350
17446
  var ConfigFieldBridge = custom();
17351
17447
  var PipelineAddonSchemaSchema = object({
@@ -23942,7 +24038,12 @@ var PlateInfoSchema = object({
23942
24038
  plateBbox: BoundingBoxSchema.optional(),
23943
24039
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
23944
24040
  keyFrameMediaKey: string().optional(),
23945
- base64: string().optional()
24041
+ base64: string().optional(),
24042
+ /**
24043
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24044
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24045
+ */
24046
+ cropUrl: string().optional()
23946
24047
  });
23947
24048
  var MediaFileLiteSchema = object({
23948
24049
  key: string(),
@@ -27582,6 +27683,12 @@ Object.freeze({
27582
27683
  addonId: null,
27583
27684
  access: "view"
27584
27685
  },
27686
+ "deviceManager.getBindingsBatch": {
27687
+ capName: "device-manager",
27688
+ capScope: "system",
27689
+ addonId: null,
27690
+ access: "view"
27691
+ },
27585
27692
  "deviceManager.getChildren": {
27586
27693
  capName: "device-manager",
27587
27694
  capScope: "system",
@@ -27642,6 +27749,12 @@ Object.freeze({
27642
27749
  addonId: null,
27643
27750
  access: "view"
27644
27751
  },
27752
+ "deviceManager.getLinkedDevicesBatch": {
27753
+ capName: "device-manager",
27754
+ capScope: "system",
27755
+ addonId: null,
27756
+ access: "view"
27757
+ },
27645
27758
  "deviceManager.getRoleDisplayDefaults": {
27646
27759
  capName: "device-manager",
27647
27760
  capScope: "system",
@@ -29412,6 +29525,12 @@ Object.freeze({
29412
29525
  addonId: null,
29413
29526
  access: "view"
29414
29527
  },
29528
+ "pipelineAnalytics.getGroup": {
29529
+ capName: "pipeline-analytics",
29530
+ capScope: "device",
29531
+ addonId: null,
29532
+ access: "view"
29533
+ },
29415
29534
  "pipelineAnalytics.getKeyEvents": {
29416
29535
  capName: "pipeline-analytics",
29417
29536
  capScope: "device",
@@ -29496,6 +29615,12 @@ Object.freeze({
29496
29615
  addonId: null,
29497
29616
  access: "view"
29498
29617
  },
29618
+ "pipelineAnalytics.listGroups": {
29619
+ capName: "pipeline-analytics",
29620
+ capScope: "device",
29621
+ addonId: null,
29622
+ access: "view"
29623
+ },
29499
29624
  "pipelineAnalytics.listOpsLog": {
29500
29625
  capName: "pipeline-analytics",
29501
29626
  capScope: "device",
@@ -32277,6 +32402,11 @@ Object.freeze({
32277
32402
  form: "single",
32278
32403
  optional: false
32279
32404
  }],
32405
+ "deviceManager.getBindingsBatch": [{
32406
+ name: "deviceIds",
32407
+ form: "array",
32408
+ optional: false
32409
+ }],
32280
32410
  "deviceManager.getChildren": [{
32281
32411
  name: "parentDeviceId",
32282
32412
  form: "single",
@@ -32322,6 +32452,11 @@ Object.freeze({
32322
32452
  form: "single",
32323
32453
  optional: false
32324
32454
  }],
32455
+ "deviceManager.getLinkedDevicesBatch": [{
32456
+ name: "deviceIds",
32457
+ form: "array",
32458
+ optional: false
32459
+ }],
32325
32460
  "deviceManager.getSettingsSchema": [{
32326
32461
  name: "deviceId",
32327
32462
  form: "single",
@@ -32342,6 +32477,11 @@ Object.freeze({
32342
32477
  form: "single",
32343
32478
  optional: false
32344
32479
  }],
32480
+ "deviceManager.listAll": [{
32481
+ name: "deviceIds",
32482
+ form: "array",
32483
+ optional: true
32484
+ }],
32345
32485
  "deviceManager.loadConfig": [{
32346
32486
  name: "deviceId",
32347
32487
  form: "single",
@@ -32915,6 +33055,11 @@ Object.freeze({
32915
33055
  form: "single",
32916
33056
  optional: false
32917
33057
  }],
33058
+ "pipelineAnalytics.getGroup": [{
33059
+ name: "deviceId",
33060
+ form: "single",
33061
+ optional: false
33062
+ }],
32918
33063
  "pipelineAnalytics.getKeyEvents": [{
32919
33064
  name: "deviceId",
32920
33065
  form: "single",
@@ -32970,6 +33115,11 @@ Object.freeze({
32970
33115
  form: "array",
32971
33116
  optional: false
32972
33117
  }],
33118
+ "pipelineAnalytics.listGroups": [{
33119
+ name: "deviceIds",
33120
+ form: "array",
33121
+ optional: false
33122
+ }],
32973
33123
  "pipelineAnalytics.listOpsLog": [{
32974
33124
  name: "deviceId",
32975
33125
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-smtp-nodemailer",
3
- "version": "1.2.26",
3
+ "version": "1.2.27",
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",