@smartspectra/node-sdk 3.3.0-rc.4 → 3.3.0-rc.7
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 +10 -1
- package/js/constants.js +6 -0
- package/js/ffi.js +73 -10
- package/js/index.d.ts +6 -0
- package/js/renderer/index.js +15 -14
- package/js/renderer/timestamp.js +56 -0
- package/js/smartspectra.js +5 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -57,9 +57,14 @@ thread-safe trampolines.
|
|
|
57
57
|
|
|
58
58
|
| Requirement | Version |
|
|
59
59
|
| --- | --- |
|
|
60
|
-
| Node.js | >=
|
|
60
|
+
| Node.js | >= 20 (Node 24 LTS recommended) |
|
|
61
61
|
| Electron (optional) | >= 28 |
|
|
62
62
|
|
|
63
|
+
You also need an API key from
|
|
64
|
+
[physiology.presagetech.com](https://physiology.presagetech.com/auth/login). An AI assistant
|
|
65
|
+
connected to the [SmartSpectra MCP Server](../docs/mcp-server.md) can fetch it from your account
|
|
66
|
+
for you.
|
|
67
|
+
|
|
63
68
|
The native runtime ships in per-platform npm packages pulled in automatically
|
|
64
69
|
as dependencies — no install script and no system libraries to install.
|
|
65
70
|
|
|
@@ -105,6 +110,10 @@ the missing package.
|
|
|
105
110
|
|
|
106
111
|
## Headless Node Quickstart
|
|
107
112
|
|
|
113
|
+
> The snippets below are ES modules (`import` + top-level `await`) — save them as `.mjs`, or
|
|
114
|
+
> set `"type": "module"` in your `package.json`. In a CommonJS project, use `require()` and
|
|
115
|
+
> wrap the `await` calls in an `async` function.
|
|
116
|
+
|
|
108
117
|
```ts
|
|
109
118
|
import {
|
|
110
119
|
SmartSpectraSDK, PixelFormat, FrameTransform, ProcessingStatus,
|
package/js/constants.js
CHANGED
|
@@ -48,6 +48,7 @@ const ValidationCode = Object.freeze({
|
|
|
48
48
|
kNoFaceFound: 1,
|
|
49
49
|
kMultipleFacesFound: 2,
|
|
50
50
|
kFaceNotCentered: 3,
|
|
51
|
+
/** @deprecated Use kFaceTooClose / kFaceTooFar instead. */
|
|
51
52
|
kFaceSizeOutOfRange: 4,
|
|
52
53
|
kTooDark: 5,
|
|
53
54
|
kTooBright: 6,
|
|
@@ -55,6 +56,11 @@ const ValidationCode = Object.freeze({
|
|
|
55
56
|
kCameraTuning: 10,
|
|
56
57
|
kFrameRateTooLow: 11,
|
|
57
58
|
kExcessiveMotion: 12,
|
|
59
|
+
kFaceTooClose: 13,
|
|
60
|
+
kFaceTooFar: 14,
|
|
61
|
+
kFaceTooHigh: 15,
|
|
62
|
+
kFaceTooLow: 16,
|
|
63
|
+
kFaceNotForward: 17,
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
const SmartSpectraErrorCode = Object.freeze({
|
package/js/ffi.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
16
19
|
const koffi = require('koffi');
|
|
17
20
|
const { resolveNativeLibrary } = require('./resolve-native');
|
|
18
21
|
|
|
@@ -444,21 +447,38 @@ class Session {
|
|
|
444
447
|
() => this._teardownNative(),
|
|
445
448
|
() => this._teardownNative());
|
|
446
449
|
} else {
|
|
447
|
-
this._teardownNative();
|
|
448
|
-
this._teardownPromise = Promise.resolve();
|
|
450
|
+
this._teardownPromise = this._teardownNative();
|
|
449
451
|
}
|
|
450
452
|
return this._teardownPromise;
|
|
451
453
|
}
|
|
452
454
|
|
|
455
|
+
// Returns a promise that resolves once native teardown (including any
|
|
456
|
+
// worker-thread joins inside session_destroy) has completed.
|
|
453
457
|
_teardownNative() {
|
|
454
|
-
if (this._sess === null) return;
|
|
455
|
-
// stop → wait → destroy → unregister. session_destroy joins
|
|
456
|
-
//
|
|
458
|
+
if (this._sess === null) return Promise.resolve();
|
|
459
|
+
// stop → wait → destroy → unregister. session_destroy joins every
|
|
460
|
+
// worker thread SpawnWorker ever started for this session — including
|
|
461
|
+
// one that's mid-flight relaying an onError/onStatus callback to JS
|
|
462
|
+
// (e.g. an async graph failure). That relay can only complete once
|
|
463
|
+
// the JS event loop is free to service it, so running session_destroy
|
|
464
|
+
// synchronously here would deadlock: this call IS the JS thread, and
|
|
465
|
+
// it would be blocked joining a thread that's blocked waiting on this
|
|
466
|
+
// same thread's event loop. Run it on a koffi worker thread instead —
|
|
467
|
+
// the same reason stopAsync() runs session_stop off-thread above —
|
|
468
|
+
// so the event loop stays free no matter what a worker is mid-relay
|
|
469
|
+
// of. _releaseThunks() only runs after the callback fires, matching
|
|
470
|
+
// the original synchronous ordering (unregister only after the joins
|
|
471
|
+
// that guarantee no SDK thread can still call our trampolines).
|
|
457
472
|
sessionStop(this._sess);
|
|
458
473
|
sessionWait(this._sess, 0);
|
|
459
|
-
|
|
474
|
+
const sess = this._sess;
|
|
460
475
|
this._sess = null;
|
|
461
|
-
|
|
476
|
+
return new Promise((resolve) => {
|
|
477
|
+
sessionDestroy.async(sess, () => {
|
|
478
|
+
this._releaseThunks();
|
|
479
|
+
resolve();
|
|
480
|
+
});
|
|
481
|
+
});
|
|
462
482
|
}
|
|
463
483
|
|
|
464
484
|
// ---------- internal ----------------------------------------------------
|
|
@@ -490,9 +510,11 @@ function makeFfiError(err) {
|
|
|
490
510
|
return e;
|
|
491
511
|
}
|
|
492
512
|
|
|
493
|
-
// Internal one-time SDK initialization. Called by the Electron
|
|
494
|
-
// glue in `@smartspectra/node-sdk/main
|
|
495
|
-
//
|
|
513
|
+
// Internal one-time SDK initialization. Called directly by the Electron
|
|
514
|
+
// main-process glue in `@smartspectra/node-sdk/main` (which namespaces its
|
|
515
|
+
// own cache directory under app.getPath('cache')). Not part of the package's
|
|
516
|
+
// public exports map — host apps don't reach for it; use ensurePreconfigured()
|
|
517
|
+
// below for the plain-Node default instead.
|
|
496
518
|
function preconfigure(sdkDirectory, deviceId) {
|
|
497
519
|
if (typeof sdkDirectory !== 'string' || sdkDirectory.length === 0) {
|
|
498
520
|
throw new TypeError('preconfigure: directory must be a non-empty string');
|
|
@@ -502,7 +524,48 @@ function preconfigure(sdkDirectory, deviceId) {
|
|
|
502
524
|
if (rc !== 0) throw makeFfiError(err);
|
|
503
525
|
}
|
|
504
526
|
|
|
527
|
+
// Per-OS writable cache directory, used as the default when nothing else
|
|
528
|
+
// (e.g. Electron's app.getPath('cache')) has already resolved one.
|
|
529
|
+
function defaultSdkDirectory() {
|
|
530
|
+
const base = process.platform === 'darwin'
|
|
531
|
+
? path.join(os.homedir(), 'Library', 'Caches')
|
|
532
|
+
: process.platform === 'win32'
|
|
533
|
+
? (process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'))
|
|
534
|
+
: (process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'));
|
|
535
|
+
return path.join(base, 'SmartSpectraSDK');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let autoPreconfigureAttempted = false;
|
|
539
|
+
|
|
540
|
+
// Idempotent, best-effort preconfigure for hosts that never call preconfigure()
|
|
541
|
+
// themselves. ARTERIAL_PRESSURE_TRACE, EDA_TRACE, and micromotion metrics need
|
|
542
|
+
// the device-key cache directory Preconfigure() sets up (smartspectra.h); without
|
|
543
|
+
// it their encrypted-model load fails downstream and Session.start*() throws —
|
|
544
|
+
// only Electron's main-process glue (js/main/index.js) called this previously,
|
|
545
|
+
// so a plain Node host requesting one of those metrics had no way to succeed.
|
|
546
|
+
// Runs once per process; failures are logged, not thrown, so metrics that don't
|
|
547
|
+
// need EMD still work. Called from SmartSpectraSDK._ensureSession() so every
|
|
548
|
+
// entry point — plain Node or Electron — gets it before a session can start;
|
|
549
|
+
// Preconfigure() itself only resolves the directory/device-identity once, so a
|
|
550
|
+
// prior, more specific call (e.g. Electron's app-namespaced one) always wins.
|
|
551
|
+
function ensurePreconfigured() {
|
|
552
|
+
if (autoPreconfigureAttempted) return;
|
|
553
|
+
autoPreconfigureAttempted = true;
|
|
554
|
+
const sdkDirectory = defaultSdkDirectory();
|
|
555
|
+
try {
|
|
556
|
+
// nosemgrep: detect-non-literal-fs-filename - safe: derived from OS-standard cache path env vars / os.homedir(), not untrusted input.
|
|
557
|
+
fs.mkdirSync(sdkDirectory, { recursive: true, mode: 0o700 });
|
|
558
|
+
preconfigure(sdkDirectory);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
console.error(
|
|
561
|
+
`SmartSpectra: automatic preconfigure failed (${err.message || err}). ` +
|
|
562
|
+
'Arterial pressure, EDA, and micromotion metrics will not be available ' +
|
|
563
|
+
`until preconfigure() succeeds for ${sdkDirectory}; other metrics still work.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
505
567
|
module.exports = {
|
|
506
568
|
Session,
|
|
507
569
|
preconfigure,
|
|
570
|
+
ensurePreconfigured,
|
|
508
571
|
};
|
package/js/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export declare const ValidationCode: {
|
|
|
46
46
|
readonly kNoFaceFound: 1;
|
|
47
47
|
readonly kMultipleFacesFound: 2;
|
|
48
48
|
readonly kFaceNotCentered: 3;
|
|
49
|
+
/** @deprecated Use kFaceTooClose / kFaceTooFar instead. */
|
|
49
50
|
readonly kFaceSizeOutOfRange: 4;
|
|
50
51
|
readonly kTooDark: 5;
|
|
51
52
|
readonly kTooBright: 6;
|
|
@@ -53,6 +54,11 @@ export declare const ValidationCode: {
|
|
|
53
54
|
readonly kCameraTuning: 10;
|
|
54
55
|
readonly kFrameRateTooLow: 11;
|
|
55
56
|
readonly kExcessiveMotion: 12;
|
|
57
|
+
readonly kFaceTooClose: 13;
|
|
58
|
+
readonly kFaceTooFar: 14;
|
|
59
|
+
readonly kFaceTooHigh: 15;
|
|
60
|
+
readonly kFaceTooLow: 16;
|
|
61
|
+
readonly kFaceNotForward: 17;
|
|
56
62
|
};
|
|
57
63
|
export type ValidationCodeValue = typeof ValidationCode[keyof typeof ValidationCode];
|
|
58
64
|
|
package/js/renderer/index.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { FrameTimestampAnchor } = require('./timestamp');
|
|
19
|
+
|
|
18
20
|
const SUPPORTED_EVENTS = new Set([
|
|
19
21
|
'processingStatus',
|
|
20
22
|
'validationStatus',
|
|
@@ -112,13 +114,14 @@ class SmartSpectraSDK {
|
|
|
112
114
|
this._reader = null;
|
|
113
115
|
this._frameLoopActive = false;
|
|
114
116
|
this._frameLoopPromise = null;
|
|
115
|
-
//
|
|
116
|
-
// frame loop) so it persists across
|
|
117
|
-
// process reuses one native session
|
|
118
|
-
// is NOT reset by stop/start, so a
|
|
119
|
-
// than the previous session's last
|
|
120
|
-
// kNonMonotonicTimestamp.
|
|
121
|
-
|
|
117
|
+
// Epoch-anchor + monotonic-ratchet for VideoFrame.timestamp. Lives on
|
|
118
|
+
// the instance (not the frame loop) so it persists across
|
|
119
|
+
// stop()/start() cycles: the main process reuses one native session
|
|
120
|
+
// whose per-session monotonic guard is NOT reset by stop/start, so a
|
|
121
|
+
// fresh camera clock starting lower than the previous session's last
|
|
122
|
+
// timestamp would otherwise trip kNonMonotonicTimestamp. See
|
|
123
|
+
// ./timestamp.js for why the epoch anchor is needed at all.
|
|
124
|
+
this._frameTimestamps = new FrameTimestampAnchor();
|
|
122
125
|
// Set true once the AE/AWB/focus lock has been attempted post-Running
|
|
123
126
|
// so we don't try to relock every frame after kRunning.
|
|
124
127
|
this._cameraLocked = false;
|
|
@@ -500,13 +503,11 @@ class SmartSpectraSDK {
|
|
|
500
503
|
ctx.drawImage(vf, 0, 0);
|
|
501
504
|
const imageData = ctx.getImageData(0, 0, vf.codedWidth, vf.codedHeight);
|
|
502
505
|
|
|
503
|
-
// VideoFrame.timestamp is already in microseconds
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
if (ts <= this._lastFrameTs) ts = this._lastFrameTs + 1n;
|
|
509
|
-
this._lastFrameTs = ts;
|
|
506
|
+
// VideoFrame.timestamp is already in microseconds, but in
|
|
507
|
+
// the frame's own media-clock domain — anchor it to the
|
|
508
|
+
// Unix epoch and ratchet it monotonic (see
|
|
509
|
+
// ./timestamp.js).
|
|
510
|
+
const ts = this._frameTimestamps.next(vf.timestamp);
|
|
510
511
|
|
|
511
512
|
const ab = imageData.data.buffer;
|
|
512
513
|
this._bridge.postMessage({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// js/renderer/timestamp.js
|
|
2
|
+
// Copyright (C) 2026 Presage Technologies, Inc.
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: LicenseRef-Proprietary
|
|
5
|
+
//
|
|
6
|
+
// Anchors a WebCodecs VideoFrame.timestamp (relative to an unspecified
|
|
7
|
+
// internal media clock, NOT the Unix epoch) to wall-clock epoch
|
|
8
|
+
// microseconds, then ratchets it to guarantee strict monotonicity across
|
|
9
|
+
// stream pauses, re-attaches, and stop()/start() restarts. Extracted from
|
|
10
|
+
// the renderer's frame loop into its own module so the anchor + ratchet
|
|
11
|
+
// math is unit-testable in plain Node — renderer/index.js itself needs
|
|
12
|
+
// OffscreenCanvas/MediaStreamTrackProcessor, which don't exist there.
|
|
13
|
+
//
|
|
14
|
+
// Android/iOS/Linux each apply an equivalent one-time boot/mach-to-epoch
|
|
15
|
+
// offset before a camera frame's timestamp reaches the graph
|
|
16
|
+
// (SmartSpectraVitalsProcessor.kt, avfoundation_camera_source.mm,
|
|
17
|
+
// v4l2_camera_source.cc); this is the Electron/WebCodecs binding's
|
|
18
|
+
// counterpart, so Metrics.timestamp is wall-clock-comparable across every
|
|
19
|
+
// binding instead of only self-consistent within one Electron session.
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
class FrameTimestampAnchor {
|
|
24
|
+
constructor() {
|
|
25
|
+
// Set from the first frame passed to next(); null means "not yet
|
|
26
|
+
// anchored". Persists for the anchor's lifetime (kept on the
|
|
27
|
+
// instance, not recomputed per stop()/start(), the same reasoning
|
|
28
|
+
// as _lastFrameTs below — VideoFrame.timestamp's clock domain is
|
|
29
|
+
// page-navigation-relative, not per-stream).
|
|
30
|
+
this._epochOffsetUs = null;
|
|
31
|
+
// Last timestamp handed out, for the monotonic ratchet.
|
|
32
|
+
this._lastFrameTs = 0n;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {number|bigint} rawTimestampUs VideoFrame.timestamp — already
|
|
37
|
+
* microseconds, but in its own media-clock domain.
|
|
38
|
+
* @param {number} [nowMs] Wall-clock epoch milliseconds at the moment of
|
|
39
|
+
* the first call. Defaults to Date.now(); overridable so callers (and
|
|
40
|
+
* tests) can pin a deterministic anchor.
|
|
41
|
+
* @returns {bigint} Epoch microseconds, strictly greater than every
|
|
42
|
+
* previous value returned by this instance.
|
|
43
|
+
*/
|
|
44
|
+
next(rawTimestampUs, nowMs = Date.now()) {
|
|
45
|
+
const raw = BigInt(rawTimestampUs || 0);
|
|
46
|
+
if (this._epochOffsetUs === null) {
|
|
47
|
+
this._epochOffsetUs = BigInt(nowMs) * 1000n - raw;
|
|
48
|
+
}
|
|
49
|
+
let ts = raw + this._epochOffsetUs;
|
|
50
|
+
if (ts <= this._lastFrameTs) ts = this._lastFrameTs + 1n;
|
|
51
|
+
this._lastFrameTs = ts;
|
|
52
|
+
return ts;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { FrameTimestampAnchor };
|
package/js/smartspectra.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
const { Session } = require('./ffi');
|
|
8
|
+
const { Session, ensurePreconfigured } = require('./ffi');
|
|
9
9
|
const {
|
|
10
10
|
ProcessingStatus,
|
|
11
11
|
SmartSpectraLogLevel,
|
|
@@ -250,6 +250,10 @@ class SmartSpectraSDK {
|
|
|
250
250
|
|
|
251
251
|
_ensureSession() {
|
|
252
252
|
if (this._session) return;
|
|
253
|
+
// ARTERIAL_PRESSURE_TRACE, EDA_TRACE, and micromotion metrics need the
|
|
254
|
+
// EMD cache directory Preconfigure() sets up before the graph starts;
|
|
255
|
+
// best-effort and idempotent, so plain metrics work even if it fails.
|
|
256
|
+
ensurePreconfigured();
|
|
253
257
|
// Listeners may be added/replaced after start(), so the lookup
|
|
254
258
|
// happens at emit time rather than capture time.
|
|
255
259
|
this._session = new Session(this._options, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartspectra/node-sdk",
|
|
3
|
-
"version": "3.3.0-rc.
|
|
3
|
+
"version": "3.3.0-rc.7",
|
|
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,7 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"prepare": "node scripts/gen-proto.cjs",
|
|
42
42
|
"build:proto": "node scripts/gen-proto.cjs",
|
|
43
|
-
"test": "node test/log-level-pin.js
|
|
43
|
+
"test": "node --test test/log-level-pin.js test/renderer-timestamp.js test/smoke.js",
|
|
44
44
|
"test:models-allowlist": "node test/models-allowlist.js"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
@@ -64,23 +64,23 @@
|
|
|
64
64
|
"registry": "https://registry.npmjs.org/"
|
|
65
65
|
},
|
|
66
66
|
"engines": {
|
|
67
|
-
"node": ">=
|
|
67
|
+
"node": ">=20.0.0"
|
|
68
68
|
},
|
|
69
69
|
"_nativeRuntime_doc": "Per-platform native packages (@smartspectra/node-sdk-<plat>-<arch>) are injected into dependencies at publish time (absent here so source installs don't fetch them). Why hard deps, not optionalDependencies: see js/resolve-native.js.",
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"koffi": "^2.10.0",
|
|
72
|
-
"protobufjs": "^7.5
|
|
73
|
-
"@smartspectra/node-sdk-linux-x64": "3.3.0-rc.
|
|
74
|
-
"@smartspectra/node-sdk-linux-arm64": "3.3.0-rc.
|
|
75
|
-
"@smartspectra/node-sdk-darwin-arm64": "3.3.0-rc.
|
|
76
|
-
"@smartspectra/node-sdk-win32-x64": "3.3.0-rc.
|
|
72
|
+
"protobufjs": "^7.6.5",
|
|
73
|
+
"@smartspectra/node-sdk-linux-x64": "3.3.0-rc.7",
|
|
74
|
+
"@smartspectra/node-sdk-linux-arm64": "3.3.0-rc.7",
|
|
75
|
+
"@smartspectra/node-sdk-darwin-arm64": "3.3.0-rc.7",
|
|
76
|
+
"@smartspectra/node-sdk-win32-x64": "3.3.0-rc.7"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@types/node": "^
|
|
79
|
+
"@types/node": "^24.0.0",
|
|
80
80
|
"protobufjs-cli": "^1.1.3"
|
|
81
81
|
},
|
|
82
|
-
"_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 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.
|
|
82
|
+
"_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
83
|
"overrides": {
|
|
84
|
-
"tmp": "0.2.
|
|
84
|
+
"tmp": "0.2.7"
|
|
85
85
|
}
|
|
86
86
|
}
|