@smartspectra/node-sdk 3.4.0-rc.5 → 3.4.0-rc.6

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.
package/README.md CHANGED
@@ -106,7 +106,7 @@ the missing package.
106
106
  - Electron desktop app: use `@smartspectra/node-sdk/main`,
107
107
  `@smartspectra/node-sdk/preload`, and `@smartspectra/node-sdk/renderer`.
108
108
  - Headless Node process with a local camera: use `@smartspectra/node-sdk`
109
- directly with `useCamera()`.
109
+ directly with `useCamera(CameraSelection.default)`.
110
110
  - Headless or server-side Node process with host-provided frames: use
111
111
  `useCustomInput()` / `sendFrame()`.
112
112
  - Runnable reference app: [electron-quickstart](https://github.com/Presage-Security/SmartSpectra/tree/main/nodejs/samples/electron-quickstart)
@@ -118,7 +118,7 @@ the missing package.
118
118
  > wrap the `await` calls in an `async` function.
119
119
 
120
120
  ```ts
121
- import { SmartSpectraSDK, breathingMetrics, cardioMetrics, decodeMetrics } from '@smartspectra/node-sdk';
121
+ import { SmartSpectraSDK, CameraSelection, breathingMetrics, cardioMetrics, decodeMetrics } from '@smartspectra/node-sdk';
122
122
 
123
123
  const sdk = new SmartSpectraSDK({
124
124
  apiKey: 'YOUR_API_KEY',
@@ -134,7 +134,7 @@ sdk.on('metrics', (buf, ts) => {
134
134
  sdk.on('error', (code, message, retryable) =>
135
135
  console.error('SmartSpectra error', code, message, 'retryable=', retryable));
136
136
 
137
- sdk.useCamera();
137
+ sdk.useCamera(CameraSelection.default);
138
138
  sdk.start();
139
139
 
140
140
  console.log('Measuring from the default camera. Press Ctrl+C to stop.');
@@ -147,10 +147,28 @@ process.on('SIGINT', async () => {
147
147
  });
148
148
  ```
149
149
 
150
- `useCamera()` opens the default camera and sends frames after `start()`. Pass a
151
- device index or capture dimensions to select another camera; see the [API
152
- reference](docs/api-reference.md#cameraoptions). Use the custom-input path
153
- below only when your app already owns frame capture.
150
+ `useCamera(CameraSelection.default)` selects the default camera while stopped; capture begins at
151
+ `start()`. You can discover cameras without creating an SDK instance:
152
+
153
+ ```ts
154
+ import { CameraSelection, SmartSpectraSDK } from '@smartspectra/node-sdk';
155
+
156
+ const cameras = SmartSpectraSDK.availableCameras();
157
+ if (cameras.length > 0) {
158
+ sdk.useCamera(CameraSelection.byId(cameras[0].id), { width: 1280, height: 720 });
159
+ }
160
+ ```
161
+
162
+ `CameraSelection.default`, `.front`, `.back`, and `.byId(id)` share the camera
163
+ selection contract across SDKs. Explicit requests never fall back. Stop/start/reset
164
+ retain the chosen input. Only macOS currently supports discovery and explicit
165
+ selectors in native Node.js; Linux/Windows report `kConfigurationFailed` for those
166
+ operations and continue to support default capture. See the
167
+ [migration guide](docs/migration-guide.md) and [API reference](docs/api-reference.md#cameraoptions).
168
+
169
+ Electron renderer capture uses browser-owned streams and `useMediaStream()`;
170
+ browser device IDs cannot be used for native camera selection. Use the
171
+ custom-input path below when your app already owns frame capture.
154
172
 
155
173
  ## Custom Input Quickstart
156
174
 
@@ -0,0 +1,57 @@
1
+ // Copyright (C) 2026 Presage Technologies, Inc.
2
+ // SPDX-License-Identifier: LicenseRef-Proprietary
3
+ 'use strict';
4
+
5
+ const { SmartSpectraErrorCode } = require('./constants');
6
+
7
+ const CameraSelection = Object.freeze({
8
+ default: Object.freeze({ kind: 'default' }),
9
+ front: Object.freeze({ kind: 'front' }),
10
+ back: Object.freeze({ kind: 'back' }),
11
+ byId: (id) => Object.freeze({ kind: 'byId', id }),
12
+ });
13
+
14
+ function configurationError(message) {
15
+ return Object.assign(new Error(message), {
16
+ code: SmartSpectraErrorCode.kConfigurationFailed, retryable: false,
17
+ });
18
+ }
19
+
20
+ function normalizeCameraSelection(selection) {
21
+ if (!selection || typeof selection !== 'object') {
22
+ throw configurationError('useCamera: expected a CameraSelection');
23
+ }
24
+ switch (selection.kind) {
25
+ case 'default': case 'front': case 'back':
26
+ if (selection.id !== undefined) throw configurationError('Camera ID requires byId selection');
27
+ return Object.freeze({ kind: selection.kind });
28
+ case 'byId':
29
+ if (typeof selection.id !== 'string' || !selection.id || selection.id.includes('\0')) {
30
+ throw configurationError('Camera ID must be a non-empty string without NUL characters');
31
+ }
32
+ return CameraSelection.byId(selection.id);
33
+ default: throw configurationError('Unknown camera selection kind');
34
+ }
35
+ }
36
+
37
+ function normalizeCameraOptions(options) {
38
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
39
+ throw configurationError('useCamera: expected capture options');
40
+ }
41
+ if ('deviceIndex' in options) throw configurationError('Use CameraSelection.byId(id) instead of deviceIndex');
42
+ const copy = {};
43
+ for (const field of ['width', 'height', 'fps', 'frameTransform']) {
44
+ const value = options[field] ?? 0;
45
+ const max = field === 'frameTransform' ? 5 : 2147483647;
46
+ if (!Number.isInteger(value) || value < 0 || value > max) {
47
+ throw configurationError(`Invalid camera ${field}`);
48
+ }
49
+ copy[field] = value;
50
+ }
51
+ if (Boolean(copy.width) !== Boolean(copy.height)) {
52
+ throw configurationError('Camera width and height must be specified together');
53
+ }
54
+ return Object.freeze(copy);
55
+ }
56
+
57
+ module.exports = { CameraSelection, normalizeCameraSelection, normalizeCameraOptions };
package/js/ffi.js CHANGED
@@ -27,6 +27,8 @@ const lib = koffi.load(resolveNativeLibrary());
27
27
 
28
28
  // Opaque session token. C side: `struct smartspectra_session_s*`.
29
29
  koffi.opaque('smartspectra_session_t');
30
+ koffi.opaque('smartspectra_camera_list_t');
31
+ koffi.struct('smartspectra_camera_info_t', { id: 'const char *', name: 'const char *', facing: 'int', lens_type: 'int' });
30
32
 
31
33
  // smartspectra_error_t — keep field order identical to the C struct; koffi
32
34
  // computes offsets by declaration order.
@@ -102,13 +104,6 @@ koffi.struct('smartspectra_file_options_t', {
102
104
  max_duration_ms: 'int',
103
105
  frame_transform: 'int',
104
106
  });
105
- koffi.struct('smartspectra_camera_options_t', {
106
- device_index: 'int',
107
- width: 'int',
108
- height: 'int',
109
- fps: 'int',
110
- frame_transform: 'int',
111
- });
112
107
 
113
108
  // Non-blocking file source: configure + start, returns immediately. Caller
114
109
  // blocks via sessionWait when it wants end-of-file.
@@ -117,10 +112,48 @@ const sessionStartFile = lib.func(
117
112
  'const char *video_path, const char *timestamps_path, ' +
118
113
  'const smartspectra_file_options_t *opts, _Out_ smartspectra_error_t *out_err)');
119
114
 
120
- const sessionStartCamera = lib.func(
115
+ koffi.struct('smartspectra_camera_options_t', {
116
+ device_index: 'int', width: 'int', height: 'int', fps: 'int', frame_transform: 'int',
117
+ });
118
+ const sessionStartLegacyCamera = lib.func(
121
119
  'int smartspectra_session_start_camera(smartspectra_session_t *sess, ' +
122
120
  'const smartspectra_camera_options_t *opts, _Out_ smartspectra_error_t *out_err)');
123
121
 
122
+ koffi.struct('smartspectra_camera_capture_options_t', {
123
+ width: 'int', height: 'int', fps: 'int', frame_transform: 'int',
124
+ });
125
+ const sessionStartCamera = lib.func(
126
+ 'int smartspectra_session_start_selected_camera(smartspectra_session_t *sess, ' +
127
+ 'int selection_kind, const char *camera_id, const smartspectra_camera_capture_options_t *opts, ' +
128
+ '_Out_ smartspectra_error_t *out_err)');
129
+ const discoverCameras = lib.func('int smartspectra_available_cameras(' +
130
+ '_Out_ smartspectra_camera_list_t **out_list, _Out_ smartspectra_error_t *out_err)');
131
+ const cameraListSize = lib.func('size_t smartspectra_camera_list_size(const smartspectra_camera_list_t *list)');
132
+ const cameraListGet = lib.func('const smartspectra_camera_info_t *smartspectra_camera_list_get(' +
133
+ 'const smartspectra_camera_list_t *list, size_t index)');
134
+ const cameraListFree = lib.func('void smartspectra_camera_list_free(smartspectra_camera_list_t *list)');
135
+
136
+ function availableCameras() {
137
+ const out = [null];
138
+ const error = {};
139
+ if (discoverCameras(out, error) !== 0) throw makeFfiError(error);
140
+ const list = out[0];
141
+ try {
142
+ const cameras = [];
143
+ const count = Number(cameraListSize(list));
144
+ for (let index = 0; index < count; index++) {
145
+ const camera = koffi.decode(cameraListGet(list, index), 'smartspectra_camera_info_t');
146
+ cameras.push({ id: camera.id, name: camera.name,
147
+ facing: camera.facing === 1 ? 'front' : camera.facing === 2 ? 'back' : 'unknown',
148
+ lensType: camera.lens_type === 1 ? 'wideAngle' : camera.lens_type === 2 ? 'ultraWide' :
149
+ camera.lens_type === 3 ? 'telephoto' : 'unknown' });
150
+ }
151
+ return cameras;
152
+ } finally {
153
+ cameraListFree(list);
154
+ }
155
+ }
156
+
124
157
  const sessionStartCustom = lib.func(
125
158
  'int smartspectra_session_start_custom(smartspectra_session_t *sess, ' +
126
159
  'int frame_transform, _Out_ smartspectra_error_t *out_err)');
@@ -343,8 +376,7 @@ class Session {
343
376
  this._takeKeepAlive();
344
377
  }
345
378
 
346
- // options (optional): { deviceIndex, width, height, fps, frameTransform }
347
- startCamera(options = null) {
379
+ startLegacyCamera(options = null) {
348
380
  this._assertAlive();
349
381
  const err = {};
350
382
  const opts = options ? {
@@ -354,7 +386,23 @@ class Session {
354
386
  fps: options.fps | 0,
355
387
  frame_transform: options.frameTransform | 0,
356
388
  } : null;
357
- const rc = sessionStartCamera(this._sess, opts, err);
389
+ const rc = sessionStartLegacyCamera(this._sess, opts, err);
390
+ if (rc !== 0) throw makeFfiError(err);
391
+ this._takeKeepAlive();
392
+ }
393
+
394
+ // Selection and capture settings are separate, matching the C++ API.
395
+ startCamera(selection, options = null) {
396
+ this._assertAlive();
397
+ const err = {};
398
+ const opts = options ? {
399
+ width: options.width | 0,
400
+ height: options.height | 0,
401
+ fps: options.fps | 0,
402
+ frame_transform: options.frameTransform | 0,
403
+ } : null;
404
+ const kind = { default: 0, front: 1, back: 2, byId: 3 }[selection.kind];
405
+ const rc = sessionStartCamera(this._sess, kind, selection.id ?? null, opts, err);
358
406
  if (rc !== 0) throw makeFfiError(err);
359
407
  this._takeKeepAlive();
360
408
  }
@@ -416,8 +464,18 @@ class Session {
416
464
  return sessionGetStatus(this._sess);
417
465
  }
418
466
 
419
- reset() {
467
+ assertSourceSelectionAllowed() {
468
+ const status = this.getStatus();
469
+ if (this._destroyed || this._stopAsyncPromise || (status !== 0 && status !== 1)) {
470
+ throw Object.assign(new Error('Select an input while stopped'), { code: 1, retryable: false });
471
+ }
472
+ }
473
+
474
+ reset(requireStopSettled = false) {
420
475
  this._assertAlive();
476
+ if (requireStopSettled && this._stopAsyncPromise) {
477
+ throw Object.assign(new Error('Await stopAsync before reset'), { code: 1, retryable: false });
478
+ }
421
479
  const err = {};
422
480
  const rc = sessionReset(this._sess, err);
423
481
  if (rc !== 0) throw makeFfiError(err);
@@ -587,6 +645,7 @@ function ensurePreconfigured() {
587
645
 
588
646
  module.exports = {
589
647
  Session,
648
+ availableCameras,
590
649
  preconfigure,
591
650
  ensurePreconfigured,
592
651
  };
package/js/index.d.ts CHANGED
@@ -127,9 +127,36 @@ export interface VideoFileOptions {
127
127
  frameTransform?: FrameTransformValue;
128
128
  }
129
129
 
130
- /** Camera capture options for useCamera(). */
130
+ /** Camera request resolved at capture startup. Explicit requests never fall back. */
131
+ export type CameraSelection =
132
+ | Readonly<{ kind: 'default' }>
133
+ | Readonly<{ kind: 'front' }>
134
+ | Readonly<{ kind: 'back' }>
135
+ | Readonly<{ kind: 'byId'; id: string }>;
136
+
137
+ export declare const CameraSelection: {
138
+ readonly default: Readonly<{ kind: 'default' }>;
139
+ readonly front: Readonly<{ kind: 'front' }>;
140
+ readonly back: Readonly<{ kind: 'back' }>;
141
+ readonly byId: (id: string) => Readonly<{ kind: 'byId'; id: string }>;
142
+ };
143
+
144
+ export type CameraFacing = 'front' | 'back' | 'unknown';
145
+
146
+ /** Best-effort classification. Missing metadata and multi-lens cameras are unknown; independent of digital zoom. */
147
+ export type CameraLensType = 'unknown' | 'wideAngle' | 'ultraWide' | 'telephoto';
148
+
149
+ /** Discovery snapshot. IDs are opaque and platform-local; rediscover before reuse. */
150
+ export interface CameraInfo {
151
+ readonly id: string;
152
+ readonly name: string | null;
153
+ readonly facing: CameraFacing;
154
+ readonly lensType: CameraLensType;
155
+ }
156
+
157
+ /** Capture settings, separate from camera selection. */
131
158
  export interface CameraOptions {
132
- /** Camera device index; omit/0 = default device. */
159
+ /** @deprecated Use useCamera(CameraSelection.byId(id), options) instead. */
133
160
  deviceIndex?: number;
134
161
  /** Capture width in px; omit/0 = SDK default. */
135
162
  width?: number;
@@ -163,15 +190,21 @@ export declare class SmartSpectraSDK {
163
190
  /** SDK package version. */
164
191
  static readonly version: string;
165
192
 
193
+ /** No SDK instance, authentication, or capture required. Throws when discovery is unsupported. */
194
+ static availableCameras(): CameraInfo[];
195
+
166
196
  constructor(options?: SmartSpectraOptions);
167
197
 
168
- /** Initialize and begin a custom-input session. */
198
+ /** Initialize and begin the input configured with useCamera, useCustomInput, or useFile. */
169
199
  start(): void;
170
200
  /** Request the session to stop. Idempotent. */
171
201
  stop(): void;
172
202
  /** Async variant of stop() — the native stop blocks until the pipeline drains; prefer this in event-loop-sensitive contexts. */
173
203
  stopAsync(): Promise<void>;
174
- /** Rebuild the processing pipeline after kError; source must be reconfigured before next start(). */
204
+ /**
205
+ * Rebuild after kError. Clears the source unless useCamera(selection, options) opted into retention.
206
+ * After typed selection, throws kInvalidState while stopAsync() is pending; await it before resetting.
207
+ */
175
208
  reset(): void;
176
209
  /**
177
210
  * Block until the session reaches a terminal state (idle or error), or the
@@ -211,6 +244,15 @@ export declare class SmartSpectraSDK {
211
244
  * pumps frames internally on `start()` — no `sendFrame()` needed. Returns
212
245
  * `this` for chaining. Captures in THIS process — for Electron, prefer
213
246
  * the renderer SDK's `useMediaStream()`.
247
+ * Select while stopped; explicit requests never fall back. Selection is
248
+ * resolved at startup and retained across stop/start/reset. Invalid or busy
249
+ * requests preserve the previous source. Pass CameraSelection.default for SDK selection.
250
+ */
251
+ useCamera(selection: CameraSelection, options?: Omit<CameraOptions, 'deviceIndex'>): this;
252
+
253
+ /**
254
+ * Select a camera by legacy index (default 0). Reset clears the source.
255
+ * @deprecated Pass an explicit CameraSelection and separate capture options instead.
214
256
  */
215
257
  useCamera(options?: CameraOptions): this;
216
258
 
package/js/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  const {
9
9
  SmartSpectraSDK,
10
+ CameraSelection,
10
11
  SmartSpectraLogLevel,
11
12
  ProcessingStatus,
12
13
  PixelFormat,
@@ -45,6 +46,7 @@ function decodeMetrics(buf) {
45
46
 
46
47
  module.exports = {
47
48
  SmartSpectraSDK,
49
+ CameraSelection,
48
50
  SmartSpectraLogLevel,
49
51
  ProcessingStatus,
50
52
  PixelFormat,
@@ -5,7 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { Session, ensurePreconfigured } = require('./ffi');
8
+ const { Session, ensurePreconfigured, availableCameras } = require('./ffi');
9
+ const { CameraSelection, normalizeCameraSelection, normalizeCameraOptions } = require('./camera-selection');
9
10
  const {
10
11
  ProcessingStatus,
11
12
  SmartSpectraLogLevel,
@@ -41,6 +42,9 @@ class SmartSpectraSDK {
41
42
  /** SDK package version (matches `package.json#version`). */
42
43
  static get version() { return PACKAGE_VERSION; }
43
44
 
45
+ /** Discover cameras without a session, authentication, or capture. */
46
+ static availableCameras() { return availableCameras(); }
47
+
44
48
  /**
45
49
  * @param {Object} [options]
46
50
  * @param {string} [options.apiKey]
@@ -64,7 +68,9 @@ class SmartSpectraSDK {
64
68
  // consumed by start(). Shape: { kind: 'custom'|'camera'|'file', ... }.
65
69
  // The last use* call wins.
66
70
  this._source = null;
71
+ this._explicitCameraSelection = false;
67
72
  this._session = null; // lazy: created on start()
73
+ this._destroyPromise = null;
68
74
  }
69
75
 
70
76
  // ---------- public API --------------------------------------------------
@@ -92,6 +98,7 @@ class SmartSpectraSDK {
92
98
  * Spatial transform applied to every pushed frame.
93
99
  */
94
100
  useCustomInput(frameTransform = DEFAULT_FRAME_TRANSFORM) {
101
+ if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
95
102
  this._source = { kind: 'custom', frameTransform: frameTransform | 0 };
96
103
  return this;
97
104
  }
@@ -101,14 +108,28 @@ class SmartSpectraSDK {
101
108
  * pumps frames internally on start() — no sendFrame() needed. Returns
102
109
  * `this` for chaining.
103
110
  *
104
- * @param {Object} [options] { deviceIndex?, width?, height?, fps?, frameTransform? }
111
+ * Selection is stopped-only and resolved at startup. Explicit requests never
112
+ * fall back. Stop/start/reset retain the selection. Capture options are separate.
113
+ * The deprecated useCamera(options) overload retains index-based capture
114
+ * and the legacy source lifecycle; omit options for camera index zero.
105
115
  *
106
116
  * Captures from a camera in THIS process (headless / Node use). In an
107
117
  * Electron app the camera is normally owned by the renderer via
108
118
  * getUserMedia — use the renderer SDK's useMediaStream() there instead.
109
119
  */
110
- useCamera(options = null) {
111
- this._source = { kind: 'camera', options };
120
+ useCamera(selection = null, options = {}) {
121
+ const isSelection = selection !== null && typeof selection === 'object' && 'kind' in selection;
122
+ if (!isSelection && arguments.length < 2) {
123
+ if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
124
+ this._source = { kind: 'camera', options: selection };
125
+ this._explicitCameraSelection = false;
126
+ return this;
127
+ }
128
+ this._requireSourceSelectionAllowed();
129
+ const request = normalizeCameraSelection(selection);
130
+ const capture = normalizeCameraOptions(options);
131
+ this._source = { kind: 'camera', selection: request, options: capture };
132
+ this._explicitCameraSelection = true;
112
133
  return this;
113
134
  }
114
135
 
@@ -123,6 +144,7 @@ class SmartSpectraSDK {
123
144
  * { timestampsPath?, interframeDelayMs?, startOffsetMs?, maxDurationMs?, frameTransform? }
124
145
  */
125
146
  useFile(videoPath, options = null) {
147
+ if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
126
148
  if (typeof videoPath !== 'string' || !videoPath) {
127
149
  throw new TypeError('useFile: videoPath must be a non-empty string');
128
150
  }
@@ -134,9 +156,10 @@ class SmartSpectraSDK {
134
156
  * Initialize the session and start the configured input source. Returns
135
157
  * once running — for custom input the graph is armed for sendFrame(); for
136
158
  * camera/file the SDK pumps frames itself. Configure a source with
137
- * useCustomInput() / useCamera() / useFile() first.
159
+ * useCustomInput(), useCamera(), or useFile() first.
138
160
  */
139
161
  start() {
162
+ if (this._explicitCameraSelection) this._requireSourceSelectionAllowed();
140
163
  const source = this._source;
141
164
  if (!source) {
142
165
  throw new Error(
@@ -149,7 +172,8 @@ class SmartSpectraSDK {
149
172
  this._session.startCustom(source.frameTransform);
150
173
  break;
151
174
  case 'camera':
152
- this._session.startCamera(source.options);
175
+ if (source.selection) this._session.startCamera(source.selection, source.options);
176
+ else this._session.startLegacyCamera(source.options);
153
177
  break;
154
178
  case 'file':
155
179
  this._session.startFile(
@@ -177,14 +201,14 @@ class SmartSpectraSDK {
177
201
  }
178
202
 
179
203
  /**
180
- * Recover from a kError state. Rebuilds the internal graph. The input
181
- * source is discarded — call useCustomInput() / useCamera() / useFile()
182
- * AND start() again to drive new input.
204
+ * Recover from a kError state. Clears the input source for legacy callers.
205
+ * After useCamera(selection, options), retains the selected input.
206
+ * Typed selection requires any pending stopAsync() to settle before reset.
183
207
  */
184
208
  reset() {
185
209
  if (!this._session) return;
186
- this._session.reset();
187
- this._source = null;
210
+ this._session.reset(this._explicitCameraSelection);
211
+ if (!this._explicitCameraSelection) this._source = null;
188
212
  }
189
213
 
190
214
  /**
@@ -239,15 +263,26 @@ class SmartSpectraSDK {
239
263
  * replacement session, since SDK state is process-global.
240
264
  */
241
265
  destroy() {
266
+ if (this._destroyPromise) return this._destroyPromise;
242
267
  if (!this._session) return Promise.resolve();
243
268
  const teardown = this._session.destroy();
244
269
  this._session = null;
245
270
  this._source = null;
246
- return teardown;
271
+ this._destroyPromise = Promise.resolve(teardown).finally(() => { this._destroyPromise = null; });
272
+ return this._destroyPromise;
247
273
  }
248
274
 
249
275
  // ---------- internal ----------------------------------------------------
250
276
 
277
+ _requireSourceSelectionAllowed() {
278
+ if (this._destroyPromise) {
279
+ throw Object.assign(new Error('SmartSpectra is shutting down'), {
280
+ code: SmartSpectraErrorCode.kInvalidState, retryable: false,
281
+ });
282
+ }
283
+ this._session?.assertSourceSelectionAllowed();
284
+ }
285
+
251
286
  _ensureSession() {
252
287
  if (this._session) return;
253
288
  // ARTERIAL_PRESSURE_TRACE, EDA_TRACE, and micromotion metrics need the
@@ -289,6 +324,7 @@ class SmartSpectraSDK {
289
324
 
290
325
  module.exports = {
291
326
  SmartSpectraSDK,
327
+ CameraSelection,
292
328
  SmartSpectraLogLevel,
293
329
  ProcessingStatus,
294
330
  PixelFormat,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartspectra/node-sdk",
3
- "version": "3.4.0-rc.5",
3
+ "version": "3.4.0-rc.6",
4
4
  "description": "Node.js (Electron) FFI binding for SmartSpectra vitals measurement",
5
5
  "author": "Presage Technologies, Inc.",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -40,7 +40,8 @@
40
40
  "scripts": {
41
41
  "prepare": "node scripts/gen-proto.cjs",
42
42
  "build:proto": "node scripts/gen-proto.cjs",
43
- "test": "node --test test/log-level-pin.js test/renderer-timestamp.js test/smoke.js",
43
+ "test": "npm run test:types && node --test test/log-level-pin.js test/renderer-timestamp.js test/camera-selection.js test/reset-lifecycle.js test/smoke.js",
44
+ "test:types": "tsc --noEmit --strict --skipLibCheck --target es2022 --module nodenext test/camera-selection-types.ts",
44
45
  "test:models-allowlist": "node test/models-allowlist.js"
45
46
  },
46
47
  "keywords": [
@@ -70,14 +71,15 @@
70
71
  "dependencies": {
71
72
  "koffi": "^2.10.0",
72
73
  "protobufjs": "^7.6.5",
73
- "@smartspectra/node-sdk-linux-x64": "3.4.0-rc.5",
74
- "@smartspectra/node-sdk-linux-arm64": "3.4.0-rc.5",
75
- "@smartspectra/node-sdk-darwin-arm64": "3.4.0-rc.5",
76
- "@smartspectra/node-sdk-win32-x64": "3.4.0-rc.5"
74
+ "@smartspectra/node-sdk-linux-x64": "3.4.0-rc.6",
75
+ "@smartspectra/node-sdk-linux-arm64": "3.4.0-rc.6",
76
+ "@smartspectra/node-sdk-darwin-arm64": "3.4.0-rc.6",
77
+ "@smartspectra/node-sdk-win32-x64": "3.4.0-rc.6"
77
78
  },
78
79
  "devDependencies": {
79
80
  "@types/node": "^24.0.0",
80
- "protobufjs-cli": "^1.1.3"
81
+ "protobufjs-cli": "^1.1.3",
82
+ "typescript": "^6.0.2"
81
83
  },
82
84
  "_overrides_doc": "tmp is pulled in transitively (protobufjs-cli → tmp) as a build-only devDependency; it is never a runtime dependency and is not shipped to consumers (the published tarball ships js/ + prebuilds only, no node_modules). Pinned to an exact version (not a >= range) to clear GHSA-ph9p-34f9-6g65 (path traversal, affects tmp <0.2.6) and GHSA-7c78-jf6q-g5cm (type-confusion bypass of the 0.2.6 _assertPath guard, affects tmp >=0.2.6) AND to keep installs deterministic — an open range would let a future install pull a brand-new, not-yet-vetted release. Bump deliberately. Drop this once protobufjs-cli ships a release whose tmp range is already >=0.2.7.",
83
85
  "overrides": {