@whitewall/blip-sdk 0.0.176 → 0.0.177

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.
Files changed (49) hide show
  1. package/dist/cjs/sender/http/httpsender.js +20 -10
  2. package/dist/cjs/sender/http/httpsender.js.map +1 -1
  3. package/dist/cjs/sender/security.js +6 -0
  4. package/dist/cjs/sender/security.js.map +1 -1
  5. package/dist/cjs/sender/sender.js +4 -2
  6. package/dist/cjs/sender/sender.js.map +1 -1
  7. package/dist/cjs/sender/sessionnegotiator.js +11 -4
  8. package/dist/cjs/sender/sessionnegotiator.js.map +1 -1
  9. package/dist/cjs/sender/tcp/tcpsender.js +60 -15
  10. package/dist/cjs/sender/tcp/tcpsender.js.map +1 -1
  11. package/dist/cjs/sender/transporttls.js +16 -0
  12. package/dist/cjs/sender/transporttls.js.map +1 -0
  13. package/dist/cjs/sender/websocket/websocketsender.js +2 -0
  14. package/dist/cjs/sender/websocket/websocketsender.js.map +1 -1
  15. package/dist/esm/sender/http/httpsender.js +20 -10
  16. package/dist/esm/sender/http/httpsender.js.map +1 -1
  17. package/dist/esm/sender/security.js +5 -1
  18. package/dist/esm/sender/security.js.map +1 -1
  19. package/dist/esm/sender/sender.js +4 -2
  20. package/dist/esm/sender/sender.js.map +1 -1
  21. package/dist/esm/sender/sessionnegotiator.js +11 -4
  22. package/dist/esm/sender/sessionnegotiator.js.map +1 -1
  23. package/dist/esm/sender/tcp/tcpsender.js +60 -15
  24. package/dist/esm/sender/tcp/tcpsender.js.map +1 -1
  25. package/dist/esm/sender/transporttls.js +13 -0
  26. package/dist/esm/sender/transporttls.js.map +1 -0
  27. package/dist/esm/sender/websocket/websocketsender.js +2 -0
  28. package/dist/esm/sender/websocket/websocketsender.js.map +1 -1
  29. package/dist/types/sender/http/httpsender.d.ts +2 -1
  30. package/dist/types/sender/http/httpsender.d.ts.map +1 -1
  31. package/dist/types/sender/security.d.ts +12 -1
  32. package/dist/types/sender/security.d.ts.map +1 -1
  33. package/dist/types/sender/sender.d.ts +2 -2
  34. package/dist/types/sender/sender.d.ts.map +1 -1
  35. package/dist/types/sender/sessionnegotiator.d.ts +1 -0
  36. package/dist/types/sender/sessionnegotiator.d.ts.map +1 -1
  37. package/dist/types/sender/tcp/tcpsender.d.ts.map +1 -1
  38. package/dist/types/sender/transporttls.d.ts +13 -0
  39. package/dist/types/sender/transporttls.d.ts.map +1 -0
  40. package/dist/types/sender/websocket/websocketsender.d.ts +2 -1
  41. package/dist/types/sender/websocket/websocketsender.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/sender/http/httpsender.ts +22 -11
  44. package/src/sender/security.ts +21 -0
  45. package/src/sender/sender.ts +9 -7
  46. package/src/sender/sessionnegotiator.ts +19 -5
  47. package/src/sender/tcp/tcpsender.ts +72 -19
  48. package/src/sender/transporttls.ts +16 -0
  49. package/src/sender/websocket/websocketsender.ts +6 -1
@@ -1,4 +1,5 @@
1
1
  import type { Socket } from 'node:net'
2
+ import type { ConnectionOptions } from 'node:tls'
2
3
  import type {
3
4
  Command,
4
5
  CommandMethods,
@@ -9,11 +10,13 @@ import type {
9
10
  Notification,
10
11
  UnknownCommandResponse,
11
12
  } from '../../types/index.ts'
13
+ import { logger } from '../../utils/logger.ts'
12
14
  import { BlipError } from '../bliperror.ts'
13
15
  import { RetryableError } from '../retryableerror.ts'
14
16
  import { ConnectionSender, type ConnectionSenderConstructor, OpenConnectionSender } from '../sender.ts'
15
17
  import { SessionNegotiator } from '../sessionnegotiator.ts'
16
18
  import { EnvelopeThrottler } from '../throttler.ts'
19
+ import { buildTlsConnectOptions } from '../transporttls.ts'
17
20
  import { tryParseJSON } from './json-parser.ts'
18
21
 
19
22
  /**
@@ -30,16 +33,26 @@ export class TCPSender extends OpenConnectionSender {
30
33
  super(options)
31
34
 
32
35
  const prefix = options.tenantId ? `${options.tenantId}.` : ''
36
+ const host = `${prefix}tcp.${this.domain}`
37
+ const auth = options.authentication
38
+ const tlsOptions: ConnectionOptions | null =
39
+ auth.scheme === 'transport' ? { ...buildTlsConnectOptions(auth), servername: host } : null
40
+
33
41
  this.connectionHandle = new TCPHandle<Envelope>(
34
- `${prefix}tcp.${this.domain}`,
42
+ host,
35
43
  443,
36
- (socket) => {
44
+ () => {
37
45
  this.sessionNegotiator = new SessionNegotiator(this, (session) => {
38
- socket.write(JSON.stringify(session))
46
+ this.connectionHandle
47
+ .get()
48
+ .then((s) => s.write(JSON.stringify(session)))
49
+ .catch((err) => logger.warn('TCPSender', 'Failed to write session frame', err))
39
50
  })
51
+ const upgradeToTls = tlsOptions ? () => this.connectionHandle.upgradeToTls(tlsOptions) : undefined
40
52
  return this.sessionNegotiator.negotiate({
41
53
  node: options.node,
42
- authentication: options.authentication,
54
+ authentication: auth,
55
+ upgradeToTls,
43
56
  })
44
57
  },
45
58
  () => this.envelopeResolver.rejectPendingEnvelopes('Connection was closed'),
@@ -124,15 +137,18 @@ class TCPHandle<T> {
124
137
  private currentSocketPromise: Promise<Socket> | null = null
125
138
  private closing = false
126
139
  private connectionAttempts = 0
140
+ private buffer = Buffer.alloc(0)
141
+ private readonly onMessage: (message: T) => void
127
142
 
128
143
  constructor(
129
144
  host: string,
130
145
  port: number,
131
- onConnected: (socket: Socket) => Promise<void>,
146
+ onConnected: () => Promise<void>,
132
147
  onClose: () => void,
133
148
  onMessage: (message: T) => void,
134
149
  ) {
135
- this.currentSocketPromise = this.connect(host, port, onConnected, onClose, onMessage)
150
+ this.onMessage = onMessage
151
+ this.currentSocketPromise = this.connect(host, port, onConnected, onClose)
136
152
  }
137
153
 
138
154
  public get() {
@@ -151,17 +167,55 @@ class TCPHandle<T> {
151
167
  }
152
168
  }
153
169
 
170
+ public async upgradeToTls(options: ConnectionOptions): Promise<void> {
171
+ if (!this.currentSocketPromise) {
172
+ throw new Error('Cannot upgrade: no active socket.')
173
+ }
174
+ const plain = await this.currentSocketPromise
175
+ plain.removeAllListeners('data')
176
+ this.buffer = Buffer.alloc(0)
177
+
178
+ const { connect: tlsConnect } = await import('node:tls')
179
+ const secured = tlsConnect({ ...options, socket: plain })
180
+
181
+ await new Promise<void>((resolve, reject) => {
182
+ const onError = (err: Error) => {
183
+ secured.off('secureConnect', onConnect)
184
+ reject(err)
185
+ }
186
+ const onConnect = () => {
187
+ secured.off('error', onError)
188
+ resolve()
189
+ }
190
+ secured.once('secureConnect', onConnect)
191
+ secured.once('error', onError)
192
+ })
193
+
194
+ this.attachDataListener(secured)
195
+ this.currentSocketPromise = Promise.resolve(secured)
196
+ }
197
+
198
+ private attachDataListener(socket: NodeJS.ReadableStream) {
199
+ socket.on('data', (chunk: Buffer<ArrayBuffer>) => {
200
+ const result = tryParseJSON<T>(this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk]))
201
+ this.buffer = result.remainingBuffer
202
+
203
+ for (const parsed of result.parsedObjects) {
204
+ this.onMessage(parsed)
205
+ }
206
+ })
207
+ }
208
+
154
209
  private async connect(
155
210
  host: string,
156
211
  port: number,
157
- onConnected: (socket: Socket) => Promise<void>,
212
+ onConnected: () => Promise<void>,
158
213
  onClose: () => void,
159
- onMessage: (message: T) => void,
160
214
  ): Promise<Socket> {
161
215
  const { connect } = await import('node:net')
162
216
 
163
217
  const socket = connect({ host, port }).setKeepAlive(true)
164
- let buffer = Buffer.alloc(0)
218
+ this.buffer = Buffer.alloc(0)
165
219
 
166
220
  await new Promise<void>((resolve) => {
167
221
  socket.once('connect', () => {
@@ -175,29 +229,28 @@ class TCPHandle<T> {
175
229
  })
176
230
 
177
231
  socket.once('close', () => {
232
+ socket.removeAllListeners('data')
178
233
  if (!this.closing) {
179
234
  onClose()
180
235
 
181
236
  this.connectionAttempts++
182
237
  if (this.connectionAttempts < 3) {
183
- this.currentSocketPromise = this.connect(host, port, onConnected, onClose, onMessage)
238
+ this.currentSocketPromise = this.connect(host, port, onConnected, onClose)
184
239
  } else {
185
240
  throw new Error('Failed to connect/reconnect to TCP socket')
186
241
  }
187
242
  }
188
243
  })
189
244
 
190
- socket.on('data', (chunk: Buffer<ArrayBuffer>) => {
191
- const result = tryParseJSON<T>(buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]))
192
- buffer = result.remainingBuffer
193
-
194
- for (const parsed of result.parsedObjects) {
195
- onMessage(parsed)
196
- }
197
- })
245
+ this.attachDataListener(socket)
198
246
  })
199
247
 
200
- await onConnected(socket)
248
+ // sendSession callbacks read currentSocketPromise via .get(); resolve it
249
+ // before onConnected runs so negotiate() can write the 'new' session frame
250
+ // (otherwise we deadlock waiting on the outer connect() promise).
251
+ this.currentSocketPromise = Promise.resolve(socket)
252
+
253
+ await onConnected()
201
254
  this.connectionAttempts = 0
202
255
 
203
256
  return socket
@@ -0,0 +1,16 @@
1
+ import type { TransportAuthentication } from './security.ts'
2
+
3
+ export function buildTlsConnectOptions(auth: TransportAuthentication) {
4
+ const toBuf = (v: Uint8Array | string) => (typeof v === 'string' ? v : Buffer.from(v))
5
+ const toBufOrArray = (v: TransportAuthentication['ca']) =>
6
+ Array.isArray(v) ? v.map(toBuf) : v !== undefined ? toBuf(v) : undefined
7
+
8
+ return {
9
+ pfx: auth.pfx ? [{ buf: Buffer.from(auth.pfx), passphrase: auth.passphrase }] : undefined,
10
+ cert: auth.cert !== undefined ? toBuf(auth.cert) : undefined,
11
+ key: auth.key !== undefined ? toBuf(auth.key) : undefined,
12
+ ca: toBufOrArray(auth.ca),
13
+ passphrase: auth.passphrase,
14
+ rejectUnauthorized: auth.rejectUnauthorized ?? true,
15
+ }
16
+ }
@@ -11,6 +11,7 @@ import type {
11
11
  import { logger } from '../../utils/logger.ts'
12
12
  import { BlipError } from '../bliperror.ts'
13
13
  import { RetryableError } from '../retryableerror.ts'
14
+ import { assertNonTransportAuth, type NonTransportAuthentication } from '../security.ts'
14
15
  import { ConnectionSender, type ConnectionSenderConstructor, OpenConnectionSender } from '../sender.ts'
15
16
  import { SessionNegotiator } from '../sessionnegotiator.ts'
16
17
  import { EnvelopeThrottler } from '../throttler.ts'
@@ -19,10 +20,14 @@ export class WebSocketSender extends OpenConnectionSender {
19
20
  private readonly throttler = new EnvelopeThrottler()
20
21
  private readonly connectionHandle: WebSocketHandle<Envelope>
21
22
 
22
- constructor(options: ConstructorParameters<ConnectionSenderConstructor>[0]) {
23
+ constructor(
24
+ options: ConstructorParameters<ConnectionSenderConstructor<WebSocketSender, NonTransportAuthentication>>[0],
25
+ ) {
23
26
  super(options)
27
+ assertNonTransportAuth(options.authentication, 'WebSocketSender')
24
28
 
25
29
  const prefix = options.tenantId ? `${options.tenantId}.` : ''
30
+
26
31
  this.connectionHandle = new WebSocketHandle<Envelope>(
27
32
  `wss://${prefix}ws.${this.domain}`,
28
33
  (webSocket) => {