dvgateway-sdk 1.0.0
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 +615 -0
- package/dist/audio/codec.d.ts +44 -0
- package/dist/audio/codec.d.ts.map +1 -0
- package/dist/audio/codec.js +136 -0
- package/dist/audio/codec.js.map +1 -0
- package/dist/audio/codec.test.d.ts +2 -0
- package/dist/audio/codec.test.d.ts.map +1 -0
- package/dist/audio/codec.test.js +155 -0
- package/dist/audio/codec.test.js.map +1 -0
- package/dist/auth/manager.d.ts +34 -0
- package/dist/auth/manager.d.ts.map +1 -0
- package/dist/auth/manager.js +122 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/manager.test.d.ts +2 -0
- package/dist/auth/manager.test.d.ts.map +1 -0
- package/dist/auth/manager.test.js +147 -0
- package/dist/auth/manager.test.js.map +1 -0
- package/dist/client.d.ts +154 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +218 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/minutes/manager.d.ts +43 -0
- package/dist/minutes/manager.d.ts.map +1 -0
- package/dist/minutes/manager.js +71 -0
- package/dist/minutes/manager.js.map +1 -0
- package/dist/observability/logger.d.ts +19 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +70 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +53 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +143 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/metrics.test.d.ts +2 -0
- package/dist/observability/metrics.test.d.ts.map +1 -0
- package/dist/observability/metrics.test.js +122 -0
- package/dist/observability/metrics.test.js.map +1 -0
- package/dist/pipeline/builder.d.ts +111 -0
- package/dist/pipeline/builder.d.ts.map +1 -0
- package/dist/pipeline/builder.js +323 -0
- package/dist/pipeline/builder.js.map +1 -0
- package/dist/session/manager.d.ts +21 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +52 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/streams/audio-stream.d.ts +29 -0
- package/dist/streams/audio-stream.d.ts.map +1 -0
- package/dist/streams/audio-stream.js +118 -0
- package/dist/streams/audio-stream.js.map +1 -0
- package/dist/streams/call-events.d.ts +32 -0
- package/dist/streams/call-events.d.ts.map +1 -0
- package/dist/streams/call-events.js +140 -0
- package/dist/streams/call-events.js.map +1 -0
- package/dist/streams/tts-stream.d.ts +46 -0
- package/dist/streams/tts-stream.d.ts.map +1 -0
- package/dist/streams/tts-stream.js +102 -0
- package/dist/streams/tts-stream.js.map +1 -0
- package/dist/transport/http-client.d.ts +36 -0
- package/dist/transport/http-client.d.ts.map +1 -0
- package/dist/transport/http-client.js +102 -0
- package/dist/transport/http-client.js.map +1 -0
- package/dist/transport/http-client.test.d.ts +2 -0
- package/dist/transport/http-client.test.d.ts.map +1 -0
- package/dist/transport/http-client.test.js +172 -0
- package/dist/transport/http-client.test.js.map +1 -0
- package/dist/transport/ws-pool.d.ts +34 -0
- package/dist/transport/ws-pool.d.ts.map +1 -0
- package/dist/transport/ws-pool.js +123 -0
- package/dist/transport/ws-pool.js.map +1 -0
- package/dist/types/index.d.ts +378 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Codec Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts between DVGateway's native slin16 format and Float32 PCM
|
|
5
|
+
* used by most AI STT/TTS APIs.
|
|
6
|
+
*
|
|
7
|
+
* DVGateway audio format:
|
|
8
|
+
* - Sample rate: 16000 Hz
|
|
9
|
+
* - Bit depth: 16-bit signed integer
|
|
10
|
+
* - Endianness: little-endian
|
|
11
|
+
* - Frame size: 640 bytes = 320 samples = 20ms
|
|
12
|
+
* - Channels: 1 (mono)
|
|
13
|
+
*/
|
|
14
|
+
/** Convert slin16 Buffer → normalized Float32Array [-1.0, 1.0] */
|
|
15
|
+
export function slin16ToFloat32(buf) {
|
|
16
|
+
const samples = buf.byteLength >> 1; // divide by 2 (each sample = 2 bytes)
|
|
17
|
+
const out = new Float32Array(samples);
|
|
18
|
+
for (let i = 0; i < samples; i++) {
|
|
19
|
+
// Read signed 16-bit little-endian integer
|
|
20
|
+
const s16 = buf.readInt16LE(i * 2);
|
|
21
|
+
// Normalize to [-1.0, 1.0]
|
|
22
|
+
out[i] = s16 / 32768.0;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** Convert normalized Float32Array [-1.0, 1.0] → slin16 Buffer */
|
|
27
|
+
export function float32ToSlin16(samples) {
|
|
28
|
+
const buf = Buffer.allocUnsafe(samples.length * 2);
|
|
29
|
+
for (let i = 0; i < samples.length; i++) {
|
|
30
|
+
// Clamp to [-1.0, 1.0] and scale to int16 range
|
|
31
|
+
const clamped = Math.max(-1.0, Math.min(1.0, samples[i] ?? 0));
|
|
32
|
+
const s16 = Math.round(clamped * 32767);
|
|
33
|
+
buf.writeInt16LE(s16, i * 2);
|
|
34
|
+
}
|
|
35
|
+
return buf;
|
|
36
|
+
}
|
|
37
|
+
/** Convert μ-law (ulaw) Buffer → slin16 Buffer */
|
|
38
|
+
export function ulawToSlin16(ulaw) {
|
|
39
|
+
const out = Buffer.allocUnsafe(ulaw.byteLength * 2);
|
|
40
|
+
for (let i = 0; i < ulaw.byteLength; i++) {
|
|
41
|
+
const s16 = ULAW_TO_LINEAR[ulaw[i]];
|
|
42
|
+
out.writeInt16LE(s16, i * 2);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** Convert slin16 Buffer → μ-law (ulaw) Buffer */
|
|
47
|
+
export function slin16ToUlaw(slin16) {
|
|
48
|
+
const samples = slin16.byteLength >> 1;
|
|
49
|
+
const out = Buffer.allocUnsafe(samples);
|
|
50
|
+
for (let i = 0; i < samples; i++) {
|
|
51
|
+
const s16 = slin16.readInt16LE(i * 2);
|
|
52
|
+
out[i] = linearToUlaw(s16);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resample Float32Array from one sample rate to another.
|
|
58
|
+
* Uses linear interpolation — suitable for real-time processing.
|
|
59
|
+
*
|
|
60
|
+
* @param samples - Input samples
|
|
61
|
+
* @param fromRate - Source sample rate (e.g. 16000)
|
|
62
|
+
* @param toRate - Target sample rate (e.g. 24000 for ElevenLabs)
|
|
63
|
+
*/
|
|
64
|
+
export function resample(samples, fromRate, toRate) {
|
|
65
|
+
if (fromRate === toRate)
|
|
66
|
+
return samples;
|
|
67
|
+
const ratio = fromRate / toRate;
|
|
68
|
+
const outLength = Math.round(samples.length / ratio);
|
|
69
|
+
const out = new Float32Array(outLength);
|
|
70
|
+
for (let i = 0; i < outLength; i++) {
|
|
71
|
+
const srcPos = i * ratio;
|
|
72
|
+
const srcIdx = Math.floor(srcPos);
|
|
73
|
+
const frac = srcPos - srcIdx;
|
|
74
|
+
const s0 = samples[srcIdx] ?? 0;
|
|
75
|
+
const s1 = samples[Math.min(srcIdx + 1, samples.length - 1)] ?? 0;
|
|
76
|
+
out[i] = s0 + frac * (s1 - s0); // linear interpolation
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Calculate RMS (Root Mean Square) amplitude of audio samples.
|
|
82
|
+
* Returns a value in [0.0, 1.0].
|
|
83
|
+
*/
|
|
84
|
+
export function calculateRms(samples) {
|
|
85
|
+
if (samples.length === 0)
|
|
86
|
+
return 0;
|
|
87
|
+
let sum = 0;
|
|
88
|
+
for (let i = 0; i < samples.length; i++) {
|
|
89
|
+
const s = samples[i] ?? 0;
|
|
90
|
+
sum += s * s;
|
|
91
|
+
}
|
|
92
|
+
return Math.sqrt(sum / samples.length);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Simple Voice Activity Detection based on RMS threshold.
|
|
96
|
+
* Returns true if the frame likely contains speech.
|
|
97
|
+
*
|
|
98
|
+
* @param samples - Float32 samples
|
|
99
|
+
* @param threshold - RMS threshold (default: 0.01, i.e. -40 dBFS)
|
|
100
|
+
*/
|
|
101
|
+
export function detectVoiceActivity(samples, threshold = 0.01) {
|
|
102
|
+
return calculateRms(samples) > threshold;
|
|
103
|
+
}
|
|
104
|
+
// ─── μ-law lookup tables ──────────────────────────────────────────────────
|
|
105
|
+
/** μ-law to linear 16-bit PCM lookup table (256 entries) */
|
|
106
|
+
const ULAW_TO_LINEAR = (() => {
|
|
107
|
+
const table = new Int16Array(256);
|
|
108
|
+
for (let i = 0; i < 256; i++) {
|
|
109
|
+
const u = ~i;
|
|
110
|
+
const sign = u & 0x80;
|
|
111
|
+
const exponent = (u >> 4) & 0x07;
|
|
112
|
+
const mantissa = u & 0x0f;
|
|
113
|
+
let sample = ((mantissa << 3) + 0x84) << exponent;
|
|
114
|
+
sample -= 0x84;
|
|
115
|
+
table[i] = sign !== 0 ? -sample : sample;
|
|
116
|
+
}
|
|
117
|
+
return table;
|
|
118
|
+
})();
|
|
119
|
+
function linearToUlaw(sample) {
|
|
120
|
+
const BIAS = 0x84;
|
|
121
|
+
const CLIP = 32635;
|
|
122
|
+
const sign = sample < 0 ? 0x80 : 0;
|
|
123
|
+
if (sample < 0)
|
|
124
|
+
sample = -sample;
|
|
125
|
+
if (sample > CLIP)
|
|
126
|
+
sample = CLIP;
|
|
127
|
+
sample += BIAS;
|
|
128
|
+
let exponent = 7;
|
|
129
|
+
for (let expMask = 0x4000; (sample & expMask) === 0 && exponent > 0; exponent--) {
|
|
130
|
+
expMask >>= 1;
|
|
131
|
+
}
|
|
132
|
+
const mantissa = (sample >> (exponent + 3)) & 0x0f;
|
|
133
|
+
const result = ~(sign | (exponent << 4) | mantissa);
|
|
134
|
+
return result & 0xff;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.js","sourceRoot":"","sources":["../../src/audio/codec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.test.d.ts","sourceRoot":"","sources":["../../src/audio/codec.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { slin16ToFloat32, float32ToSlin16, ulawToSlin16, slin16ToUlaw, resample, calculateRms, detectVoiceActivity, } from './codec.js';
|
|
2
|
+
describe('slin16ToFloat32', () => {
|
|
3
|
+
it('should convert silence (zeros) correctly', () => {
|
|
4
|
+
const buf = Buffer.alloc(640); // 320 samples of silence
|
|
5
|
+
const result = slin16ToFloat32(buf);
|
|
6
|
+
expect(result.length).toBe(320);
|
|
7
|
+
expect(result.every((s) => s === 0)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it('should convert max positive value to ~1.0', () => {
|
|
10
|
+
const buf = Buffer.alloc(2);
|
|
11
|
+
buf.writeInt16LE(32767, 0); // max int16
|
|
12
|
+
const result = slin16ToFloat32(buf);
|
|
13
|
+
expect(result[0]).toBeCloseTo(1.0, 3);
|
|
14
|
+
});
|
|
15
|
+
it('should convert max negative value to ~-1.0', () => {
|
|
16
|
+
const buf = Buffer.alloc(2);
|
|
17
|
+
buf.writeInt16LE(-32768, 0); // min int16
|
|
18
|
+
const result = slin16ToFloat32(buf);
|
|
19
|
+
expect(result[0]).toBeCloseTo(-1.0, 3);
|
|
20
|
+
});
|
|
21
|
+
it('should handle a standard 20ms frame (640 bytes)', () => {
|
|
22
|
+
const buf = Buffer.alloc(640);
|
|
23
|
+
// Write a known pattern
|
|
24
|
+
for (let i = 0; i < 320; i++) {
|
|
25
|
+
buf.writeInt16LE(i * 100, i * 2);
|
|
26
|
+
}
|
|
27
|
+
const result = slin16ToFloat32(buf);
|
|
28
|
+
expect(result.length).toBe(320);
|
|
29
|
+
expect(result[0]).toBe(0); // first sample = 0
|
|
30
|
+
expect(result[1]).toBeCloseTo(100 / 32768, 4);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('float32ToSlin16', () => {
|
|
34
|
+
it('should convert silence correctly', () => {
|
|
35
|
+
const samples = new Float32Array(320);
|
|
36
|
+
const buf = float32ToSlin16(samples);
|
|
37
|
+
expect(buf.byteLength).toBe(640);
|
|
38
|
+
for (let i = 0; i < 320; i++) {
|
|
39
|
+
expect(buf.readInt16LE(i * 2)).toBe(0);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
it('should clamp values beyond [-1.0, 1.0]', () => {
|
|
43
|
+
const samples = new Float32Array([1.5, -1.5, 0.5]);
|
|
44
|
+
const buf = float32ToSlin16(samples);
|
|
45
|
+
expect(buf.readInt16LE(0)).toBe(32767); // clamped to 1.0
|
|
46
|
+
expect(buf.readInt16LE(2)).toBe(-32767); // clamped to -1.0
|
|
47
|
+
expect(buf.readInt16LE(4)).toBe(Math.round(0.5 * 32767));
|
|
48
|
+
});
|
|
49
|
+
it('should be inverse of slin16ToFloat32 (roundtrip)', () => {
|
|
50
|
+
const original = Buffer.alloc(20);
|
|
51
|
+
for (let i = 0; i < 10; i++) {
|
|
52
|
+
original.writeInt16LE(i * 1000 - 5000, i * 2);
|
|
53
|
+
}
|
|
54
|
+
const float32 = slin16ToFloat32(original);
|
|
55
|
+
const roundtrip = float32ToSlin16(float32);
|
|
56
|
+
for (let i = 0; i < 10; i++) {
|
|
57
|
+
// Allow ±1 rounding error
|
|
58
|
+
expect(Math.abs(roundtrip.readInt16LE(i * 2) - original.readInt16LE(i * 2))).toBeLessThanOrEqual(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('ulawToSlin16 / slin16ToUlaw', () => {
|
|
63
|
+
it('should roundtrip ulaw → slin16 → ulaw with reasonable accuracy', () => {
|
|
64
|
+
// Create a buffer with all 256 possible ulaw values
|
|
65
|
+
const ulawBuf = Buffer.alloc(256);
|
|
66
|
+
for (let i = 0; i < 256; i++) {
|
|
67
|
+
ulawBuf[i] = i;
|
|
68
|
+
}
|
|
69
|
+
const slin16Buf = ulawToSlin16(ulawBuf);
|
|
70
|
+
expect(slin16Buf.byteLength).toBe(512); // 256 samples × 2 bytes
|
|
71
|
+
const reEncoded = slin16ToUlaw(slin16Buf);
|
|
72
|
+
expect(reEncoded.byteLength).toBe(256);
|
|
73
|
+
// Roundtrip: ulaw → linear → ulaw should be exact (or very close)
|
|
74
|
+
let exactMatches = 0;
|
|
75
|
+
for (let i = 0; i < 256; i++) {
|
|
76
|
+
if (reEncoded[i] === ulawBuf[i])
|
|
77
|
+
exactMatches++;
|
|
78
|
+
}
|
|
79
|
+
// At least 90% should be exact matches
|
|
80
|
+
expect(exactMatches).toBeGreaterThan(230);
|
|
81
|
+
});
|
|
82
|
+
it('should convert ulaw silence (0xFF) to near-zero linear', () => {
|
|
83
|
+
const ulawBuf = Buffer.from([0xFF]);
|
|
84
|
+
const slin16Buf = ulawToSlin16(ulawBuf);
|
|
85
|
+
const sample = slin16Buf.readInt16LE(0);
|
|
86
|
+
expect(Math.abs(sample)).toBeLessThan(200); // near-silence
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('resample', () => {
|
|
90
|
+
it('should return same array when rates are equal', () => {
|
|
91
|
+
const samples = new Float32Array([0.1, 0.2, 0.3]);
|
|
92
|
+
const result = resample(samples, 16000, 16000);
|
|
93
|
+
expect(result).toBe(samples); // exact same reference
|
|
94
|
+
});
|
|
95
|
+
it('should upsample 16kHz → 24kHz', () => {
|
|
96
|
+
const samples = new Float32Array(320); // 20ms at 16kHz
|
|
97
|
+
for (let i = 0; i < 320; i++) {
|
|
98
|
+
samples[i] = Math.sin(2 * Math.PI * 440 * i / 16000); // 440Hz tone
|
|
99
|
+
}
|
|
100
|
+
const result = resample(samples, 16000, 24000);
|
|
101
|
+
// 320 samples at 16kHz → ~480 samples at 24kHz
|
|
102
|
+
expect(result.length).toBe(480);
|
|
103
|
+
});
|
|
104
|
+
it('should downsample 48kHz → 16kHz', () => {
|
|
105
|
+
const samples = new Float32Array(960); // 20ms at 48kHz
|
|
106
|
+
const result = resample(samples, 48000, 16000);
|
|
107
|
+
expect(result.length).toBe(320);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('calculateRms', () => {
|
|
111
|
+
it('should return 0 for empty array', () => {
|
|
112
|
+
expect(calculateRms(new Float32Array(0))).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
it('should return 0 for silence', () => {
|
|
115
|
+
expect(calculateRms(new Float32Array(320))).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
it('should return ~0.707 for full-scale square wave', () => {
|
|
118
|
+
const samples = new Float32Array(100);
|
|
119
|
+
for (let i = 0; i < 100; i++) {
|
|
120
|
+
samples[i] = i % 2 === 0 ? 1.0 : -1.0;
|
|
121
|
+
}
|
|
122
|
+
expect(calculateRms(samples)).toBeCloseTo(1.0, 3);
|
|
123
|
+
});
|
|
124
|
+
it('should return a value between 0 and 1 for a sine wave', () => {
|
|
125
|
+
const samples = new Float32Array(1000);
|
|
126
|
+
for (let i = 0; i < 1000; i++) {
|
|
127
|
+
samples[i] = Math.sin(2 * Math.PI * i / 1000);
|
|
128
|
+
}
|
|
129
|
+
const rms = calculateRms(samples);
|
|
130
|
+
expect(rms).toBeGreaterThan(0);
|
|
131
|
+
expect(rms).toBeLessThan(1);
|
|
132
|
+
expect(rms).toBeCloseTo(Math.SQRT1_2, 1); // sine wave RMS = 1/√2 ≈ 0.707
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('detectVoiceActivity', () => {
|
|
136
|
+
it('should return false for silence', () => {
|
|
137
|
+
expect(detectVoiceActivity(new Float32Array(320))).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
it('should return true for loud audio', () => {
|
|
140
|
+
const samples = new Float32Array(320);
|
|
141
|
+
for (let i = 0; i < 320; i++) {
|
|
142
|
+
samples[i] = 0.5 * Math.sin(2 * Math.PI * 440 * i / 16000);
|
|
143
|
+
}
|
|
144
|
+
expect(detectVoiceActivity(samples)).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
it('should respect custom threshold', () => {
|
|
147
|
+
const samples = new Float32Array(320);
|
|
148
|
+
for (let i = 0; i < 320; i++) {
|
|
149
|
+
samples[i] = 0.005; // very quiet
|
|
150
|
+
}
|
|
151
|
+
expect(detectVoiceActivity(samples, 0.01)).toBe(false); // default threshold: not voice
|
|
152
|
+
expect(detectVoiceActivity(samples, 0.001)).toBe(true); // lower threshold: detected
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=codec.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.test.js","sourceRoot":"","sources":["../../src/audio/codec.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles API Key → JWT exchange with automatic silent refresh.
|
|
5
|
+
* Users never need to manage tokens directly.
|
|
6
|
+
*
|
|
7
|
+
* Security: Uses RS256 JWTs with short TTL (15 min).
|
|
8
|
+
* Tokens are refreshed 60 seconds before expiry.
|
|
9
|
+
*/
|
|
10
|
+
import type { Auth, Logger } from '../types/index.js';
|
|
11
|
+
export declare class AuthManager {
|
|
12
|
+
private readonly auth;
|
|
13
|
+
private readonly baseUrl;
|
|
14
|
+
private readonly logger;
|
|
15
|
+
private cachedToken;
|
|
16
|
+
private tokenExpiresAt;
|
|
17
|
+
private refreshPromise;
|
|
18
|
+
/** Token is refreshed 60 seconds before actual expiry */
|
|
19
|
+
private static readonly REFRESH_BUFFER_MS;
|
|
20
|
+
constructor(auth: Auth, baseUrl: string, logger: Logger);
|
|
21
|
+
/**
|
|
22
|
+
* Returns a valid bearer token, refreshing if necessary.
|
|
23
|
+
* Thread-safe: concurrent calls share a single refresh promise.
|
|
24
|
+
*/
|
|
25
|
+
getToken(): Promise<string>;
|
|
26
|
+
/** Returns a bearer token header value */
|
|
27
|
+
getBearerHeader(): Promise<string>;
|
|
28
|
+
private fetchToken;
|
|
29
|
+
/** Build authenticated WebSocket URL (token embedded as query param) */
|
|
30
|
+
buildWsUrl(path: string, params?: Record<string, string>): Promise<string>;
|
|
31
|
+
/** Build authenticated HTTP URL */
|
|
32
|
+
buildHttpUrl(path: string, params?: Record<string, string>): string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/auth/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAqBtD,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAgC;IAEtD,yDAAyD;IACzD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAU;gBAEvC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAkBvD;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAkBjC,0CAA0C;IACpC,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;YAI1B,UAAU;IAkDxB,wEAAwE;IAClE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAUpF,mCAAmC;IACnC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,MAAM;CAMxE"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles API Key → JWT exchange with automatic silent refresh.
|
|
5
|
+
* Users never need to manage tokens directly.
|
|
6
|
+
*
|
|
7
|
+
* Security: Uses RS256 JWTs with short TTL (15 min).
|
|
8
|
+
* Tokens are refreshed 60 seconds before expiry.
|
|
9
|
+
*/
|
|
10
|
+
function parseJwtPayload(token) {
|
|
11
|
+
const parts = token.split('.');
|
|
12
|
+
if (parts.length !== 3) {
|
|
13
|
+
throw new Error('Invalid JWT format');
|
|
14
|
+
}
|
|
15
|
+
const payload = parts[1];
|
|
16
|
+
// Base64URL decode
|
|
17
|
+
const padded = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
18
|
+
const json = Buffer.from(padded, 'base64').toString('utf8');
|
|
19
|
+
return JSON.parse(json);
|
|
20
|
+
}
|
|
21
|
+
export class AuthManager {
|
|
22
|
+
auth;
|
|
23
|
+
baseUrl;
|
|
24
|
+
logger;
|
|
25
|
+
cachedToken = null;
|
|
26
|
+
tokenExpiresAt = 0;
|
|
27
|
+
refreshPromise = null;
|
|
28
|
+
/** Token is refreshed 60 seconds before actual expiry */
|
|
29
|
+
static REFRESH_BUFFER_MS = 60_000;
|
|
30
|
+
constructor(auth, baseUrl, logger) {
|
|
31
|
+
this.auth = auth;
|
|
32
|
+
this.baseUrl = baseUrl;
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
// Pre-load JWT if provided directly
|
|
35
|
+
if (auth.type === 'jwt') {
|
|
36
|
+
this.cachedToken = auth.token;
|
|
37
|
+
try {
|
|
38
|
+
const payload = parseJwtPayload(auth.token);
|
|
39
|
+
this.tokenExpiresAt = payload.exp * 1000;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// If we can't parse it, treat as expired so it gets refreshed
|
|
43
|
+
this.tokenExpiresAt = 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns a valid bearer token, refreshing if necessary.
|
|
49
|
+
* Thread-safe: concurrent calls share a single refresh promise.
|
|
50
|
+
*/
|
|
51
|
+
async getToken() {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const refreshThreshold = this.tokenExpiresAt - AuthManager.REFRESH_BUFFER_MS;
|
|
54
|
+
if (this.cachedToken && now < refreshThreshold) {
|
|
55
|
+
return this.cachedToken;
|
|
56
|
+
}
|
|
57
|
+
// Coalesce concurrent refresh requests into a single HTTP call
|
|
58
|
+
if (!this.refreshPromise) {
|
|
59
|
+
this.refreshPromise = this.fetchToken().finally(() => {
|
|
60
|
+
this.refreshPromise = null;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return this.refreshPromise;
|
|
64
|
+
}
|
|
65
|
+
/** Returns a bearer token header value */
|
|
66
|
+
async getBearerHeader() {
|
|
67
|
+
return `Bearer ${await this.getToken()}`;
|
|
68
|
+
}
|
|
69
|
+
async fetchToken() {
|
|
70
|
+
if (this.auth.type === 'jwt') {
|
|
71
|
+
// Re-validate JWT — caller supplied a static token
|
|
72
|
+
const payload = parseJwtPayload(this.auth.token);
|
|
73
|
+
if (payload.exp * 1000 < Date.now()) {
|
|
74
|
+
throw new Error('DVGateway: provided JWT has expired. ' +
|
|
75
|
+
'Use apiKey auth for automatic token refresh.');
|
|
76
|
+
}
|
|
77
|
+
this.cachedToken = this.auth.token;
|
|
78
|
+
this.tokenExpiresAt = payload.exp * 1000;
|
|
79
|
+
return this.cachedToken;
|
|
80
|
+
}
|
|
81
|
+
// API Key → JWT exchange
|
|
82
|
+
const url = `${this.baseUrl}/api/v1/auth/token`;
|
|
83
|
+
this.logger.debug({ url }, 'Fetching new JWT from gateway');
|
|
84
|
+
const response = await fetch(url, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
'X-API-Key': this.auth.apiKey,
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({ apiKey: this.auth.apiKey }),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const body = await response.text().catch(() => '');
|
|
94
|
+
throw new Error(`DVGateway auth failed (HTTP ${response.status}): ${body}`);
|
|
95
|
+
}
|
|
96
|
+
const data = (await response.json());
|
|
97
|
+
const token = data.token;
|
|
98
|
+
const payload = parseJwtPayload(token);
|
|
99
|
+
this.cachedToken = token;
|
|
100
|
+
this.tokenExpiresAt = payload.exp * 1000;
|
|
101
|
+
this.logger.debug({ expiresAt: new Date(this.tokenExpiresAt).toISOString() }, 'JWT refreshed successfully');
|
|
102
|
+
return token;
|
|
103
|
+
}
|
|
104
|
+
/** Build authenticated WebSocket URL (token embedded as query param) */
|
|
105
|
+
async buildWsUrl(path, params = {}) {
|
|
106
|
+
const token = await this.getToken();
|
|
107
|
+
const base = this.baseUrl
|
|
108
|
+
.replace(/^https?:\/\//, (m) => (m.startsWith('https') ? 'wss://' : 'ws://'))
|
|
109
|
+
.replace(/\/$/, '');
|
|
110
|
+
const query = new URLSearchParams({ ...params, token });
|
|
111
|
+
return `${base}${path}?${query.toString()}`;
|
|
112
|
+
}
|
|
113
|
+
/** Build authenticated HTTP URL */
|
|
114
|
+
buildHttpUrl(path, params = {}) {
|
|
115
|
+
const base = this.baseUrl.replace(/\/$/, '');
|
|
116
|
+
if (Object.keys(params).length === 0)
|
|
117
|
+
return `${base}${path}`;
|
|
118
|
+
const query = new URLSearchParams(params);
|
|
119
|
+
return `${base}${path}?${query.toString()}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/auth/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.test.d.ts","sourceRoot":"","sources":["../../src/auth/manager.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { AuthManager } from './manager.js';
|
|
2
|
+
// Minimal logger for tests
|
|
3
|
+
const mockLogger = {
|
|
4
|
+
debug: jest.fn(),
|
|
5
|
+
info: jest.fn(),
|
|
6
|
+
warn: jest.fn(),
|
|
7
|
+
error: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
// Helper: create a valid JWT with given payload
|
|
10
|
+
function createJwt(payload) {
|
|
11
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
12
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
13
|
+
const sig = 'fakesig';
|
|
14
|
+
return `${header}.${body}.${sig}`;
|
|
15
|
+
}
|
|
16
|
+
describe('AuthManager', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
// Reset fetch mocks
|
|
20
|
+
globalThis.fetch = jest.fn();
|
|
21
|
+
});
|
|
22
|
+
describe('JWT auth', () => {
|
|
23
|
+
it('should return the provided token when not expired', async () => {
|
|
24
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
|
|
25
|
+
const token = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'user1' });
|
|
26
|
+
const auth = new AuthManager({ type: 'jwt', token }, 'https://gw.test', mockLogger);
|
|
27
|
+
const result = await auth.getToken();
|
|
28
|
+
expect(result).toBe(token);
|
|
29
|
+
});
|
|
30
|
+
it('should throw when JWT is expired', async () => {
|
|
31
|
+
const pastExp = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago
|
|
32
|
+
const token = createJwt({ exp: pastExp, iat: pastExp - 3600, sub: 'user1' });
|
|
33
|
+
const auth = new AuthManager({ type: 'jwt', token }, 'https://gw.test', mockLogger);
|
|
34
|
+
await expect(auth.getToken()).rejects.toThrow('JWT has expired');
|
|
35
|
+
});
|
|
36
|
+
it('should treat malformed JWT as expired and throw on getToken', async () => {
|
|
37
|
+
// Constructor catches parse errors and sets tokenExpiresAt = 0
|
|
38
|
+
const auth = new AuthManager({ type: 'jwt', token: 'invalid' }, 'https://gw.test', mockLogger);
|
|
39
|
+
// getToken re-validates → throws "Invalid JWT format"
|
|
40
|
+
await expect(auth.getToken()).rejects.toThrow('Invalid JWT format');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('API key auth', () => {
|
|
44
|
+
it('should exchange API key for JWT token', async () => {
|
|
45
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
46
|
+
const responseToken = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'api' });
|
|
47
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
48
|
+
ok: true,
|
|
49
|
+
json: async () => ({ token: responseToken }),
|
|
50
|
+
});
|
|
51
|
+
const auth = new AuthManager({ type: 'apiKey', apiKey: 'test-key' }, 'https://gw.test', mockLogger);
|
|
52
|
+
const result = await auth.getToken();
|
|
53
|
+
expect(result).toBe(responseToken);
|
|
54
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('https://gw.test/api/v1/auth/token', expect.objectContaining({
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: expect.objectContaining({
|
|
57
|
+
'X-API-Key': 'test-key',
|
|
58
|
+
}),
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
it('should throw on auth failure', async () => {
|
|
62
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
63
|
+
ok: false,
|
|
64
|
+
status: 401,
|
|
65
|
+
text: async () => 'Unauthorized',
|
|
66
|
+
});
|
|
67
|
+
const auth = new AuthManager({ type: 'apiKey', apiKey: 'bad-key' }, 'https://gw.test', mockLogger);
|
|
68
|
+
await expect(auth.getToken()).rejects.toThrow('auth failed (HTTP 401)');
|
|
69
|
+
});
|
|
70
|
+
it('should cache token and reuse on subsequent calls', async () => {
|
|
71
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
72
|
+
const responseToken = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'api' });
|
|
73
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
74
|
+
ok: true,
|
|
75
|
+
json: async () => ({ token: responseToken }),
|
|
76
|
+
});
|
|
77
|
+
const auth = new AuthManager({ type: 'apiKey', apiKey: 'test-key' }, 'https://gw.test', mockLogger);
|
|
78
|
+
const r1 = await auth.getToken();
|
|
79
|
+
const r2 = await auth.getToken();
|
|
80
|
+
expect(r1).toBe(r2);
|
|
81
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1); // Only one HTTP call
|
|
82
|
+
});
|
|
83
|
+
it('should coalesce concurrent refresh requests', async () => {
|
|
84
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
85
|
+
const responseToken = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'api' });
|
|
86
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
87
|
+
ok: true,
|
|
88
|
+
json: async () => ({ token: responseToken }),
|
|
89
|
+
});
|
|
90
|
+
const auth = new AuthManager({ type: 'apiKey', apiKey: 'test-key' }, 'https://gw.test', mockLogger);
|
|
91
|
+
// Fire multiple concurrent calls
|
|
92
|
+
const [r1, r2, r3] = await Promise.all([
|
|
93
|
+
auth.getToken(),
|
|
94
|
+
auth.getToken(),
|
|
95
|
+
auth.getToken(),
|
|
96
|
+
]);
|
|
97
|
+
expect(r1).toBe(responseToken);
|
|
98
|
+
expect(r2).toBe(responseToken);
|
|
99
|
+
expect(r3).toBe(responseToken);
|
|
100
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('getBearerHeader', () => {
|
|
104
|
+
it('should return Bearer prefix', async () => {
|
|
105
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
106
|
+
const token = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'u1' });
|
|
107
|
+
const auth = new AuthManager({ type: 'jwt', token }, 'https://gw.test', mockLogger);
|
|
108
|
+
const header = await auth.getBearerHeader();
|
|
109
|
+
expect(header).toBe(`Bearer ${token}`);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('buildWsUrl', () => {
|
|
113
|
+
it('should build wss:// URL with token and params', async () => {
|
|
114
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
115
|
+
const token = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'u1' });
|
|
116
|
+
const auth = new AuthManager({ type: 'jwt', token }, 'https://gw.test', mockLogger);
|
|
117
|
+
const url = await auth.buildWsUrl('/api/v1/ws/stream', { linkedid: 'abc123' });
|
|
118
|
+
expect(url).toContain('wss://gw.test/api/v1/ws/stream');
|
|
119
|
+
expect(url).toContain('linkedid=abc123');
|
|
120
|
+
expect(url).toContain(`token=`);
|
|
121
|
+
});
|
|
122
|
+
it('should convert http:// to ws://', async () => {
|
|
123
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
124
|
+
const token = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'u1' });
|
|
125
|
+
const auth = new AuthManager({ type: 'jwt', token }, 'http://gw.test', mockLogger);
|
|
126
|
+
const url = await auth.buildWsUrl('/test', {});
|
|
127
|
+
expect(url).toContain('ws://gw.test/test');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('buildHttpUrl', () => {
|
|
131
|
+
it('should build full URL with path', () => {
|
|
132
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
133
|
+
const token = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'u1' });
|
|
134
|
+
const auth = new AuthManager({ type: 'jwt', token }, 'https://gw.test/', mockLogger);
|
|
135
|
+
const url = auth.buildHttpUrl('/api/v1/sessions');
|
|
136
|
+
expect(url).toBe('https://gw.test/api/v1/sessions');
|
|
137
|
+
});
|
|
138
|
+
it('should append query params', () => {
|
|
139
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
140
|
+
const token = createJwt({ exp: futureExp, iat: Date.now() / 1000, sub: 'u1' });
|
|
141
|
+
const auth = new AuthManager({ type: 'jwt', token }, 'https://gw.test', mockLogger);
|
|
142
|
+
const url = auth.buildHttpUrl('/api', { foo: 'bar' });
|
|
143
|
+
expect(url).toBe('https://gw.test/api?foo=bar');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=manager.test.js.map
|