@tchain/tcabci-read-js-client 2.8.0 → 2.8.1

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/authority.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /** Caller-owned result; the input is valid only during the synchronous call. */
2
+ export type WSAuthoritySigner = (payload: Uint8Array) => Uint8Array
3
+ export type Randomizer = (data: Uint8Array) => Uint8Array
4
+
5
+ export interface WSAuthorityOptions {
6
+ address: string
7
+ sign: WSAuthoritySigner
8
+ }
9
+
10
+ /** Fresh v3 header for one trusted WS handshake; never log or cache it. */
11
+ export declare function createWSAuthorityV3(
12
+ address: string,
13
+ sign: WSAuthoritySigner,
14
+ randomizer?: Randomizer
15
+ ): string
package/authority.js ADDED
@@ -0,0 +1,63 @@
1
+ import { SendThrow } from './util.js'
2
+
3
+ /**
4
+ * Create a fresh read-node WS authority v3 header, valid for ten seconds and one
5
+ * handshake. Use only with trusted endpoints; never log or cache the result.
6
+ * The signer is synchronous, receives an owned Uint8Array valid during its
7
+ * call, and returns caller-owned signature bytes. No private key is retained.
8
+ * @param {string} address Configured TransferChain public address.
9
+ * @param {(payload: Uint8Array) => Uint8Array} sign Synchronous signer.
10
+ * @param {(Uint8Array) => Uint8Array} randomizer
11
+ * @returns {string}
12
+ */
13
+ export function createWSAuthorityV3(address, sign, randomizer = null) {
14
+ if (
15
+ typeof address !== 'string' ||
16
+ address.length === 0 ||
17
+ address.length > 2048 ||
18
+ /[;\r\n]/.test(address) ||
19
+ typeof sign !== 'function'
20
+ )
21
+ throw new TypeError('Invalid websocket authority configuration')
22
+
23
+ const nonce = new Uint8Array(32),
24
+ hex = (bytes) =>
25
+ Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
26
+ let payload, signature
27
+
28
+ try {
29
+ if (!randomizer) globalThis.crypto.getRandomValues(nonce)
30
+ else randomizer(nonce)
31
+
32
+ const timestamp = (BigInt(Date.now()) * 1000000n).toString(),
33
+ nonceHex = hex(nonce)
34
+
35
+ payload = new TextEncoder().encode(
36
+ `read-node/ws-authority/v3\n${address}\n${timestamp}\n${nonceHex}`
37
+ )
38
+
39
+ const result = sign(payload)
40
+
41
+ if (result && typeof result.then === 'function') {
42
+ Promise.resolve(result).catch(() => {})
43
+ SendThrow(new TypeError('Authority signer must be synchronous'))
44
+ }
45
+
46
+ if (
47
+ !(result instanceof Uint8Array) ||
48
+ result.length === 0 ||
49
+ result.length > 8192
50
+ )
51
+ SendThrow(new TypeError('Invalid websocket authority signature'))
52
+
53
+ signature = new Uint8Array(result)
54
+
55
+ return `${address};v3;${timestamp};${nonceHex};${hex(signature)}`
56
+ } catch {
57
+ throw new Error('Websocket authority creation failed')
58
+ } finally {
59
+ nonce.fill(0)
60
+ payload?.fill(0)
61
+ signature?.fill(0)
62
+ }
63
+ }
package/client.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { createWSAuthorityV3 } from './authority.js'
2
+ export type { WSAuthoritySigner, WSAuthorityOptions } from './authority.js'
1
3
  import { Transaction, TXType } from './transaction.js'
2
4
  import { TWebSocket } from './websocket.js'
3
5
  import {
@@ -71,6 +73,7 @@ export default class TCaBCIClient {
71
73
  options?: ClientOptions
72
74
  )
73
75
 
76
+ SetIsWeb(isWeb: boolean): this
74
77
  SetDebug(debug: boolean): this
75
78
 
76
79
  SetSuccessCallback(cb: SuccessCallback | null): this
@@ -109,6 +112,9 @@ export default class TCaBCIClient {
109
112
  txTypes?: readonly TXType[] | null
110
113
  ): this
111
114
 
115
+ /** Explicit Listen; requires a valid authority and repeats after reconnect. */
116
+ Listen(): this
117
+
112
118
  Unsubscribe(): this
113
119
 
114
120
  LastBlock(
package/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { createWSAuthorityV3 } from './authority.js'
1
2
  import {
2
3
  addresses as validateAddresses,
3
4
  signedData as validateSignedData,
@@ -21,6 +22,8 @@ import Message, {
21
22
  Transaction as MTransaction,
22
23
  SUBSCRIBEMessage,
23
24
  UNSUBSCRIBEMessage,
25
+ LISTENMessage,
26
+ Listen as MListen,
24
27
  Subscription,
25
28
  OK
26
29
  } from './message.js'
@@ -63,6 +66,7 @@ export default class TCaBCIClient {
63
66
  _subscribed = false
64
67
  _subscribedAddresses = []
65
68
  _SubscribedSignedData = {}
69
+ _listenRequested = false
66
70
  _connected = false
67
71
  _chainName = 'transferchain'
68
72
  _chainVersion = 'v1'
@@ -135,11 +139,15 @@ export default class TCaBCIClient {
135
139
  ).href
136
140
  }
137
141
 
142
+ this._isWeb = true
138
143
  this._httpClient = new HTTP(this._readNodeAddress, options)
139
144
  this._options = new Options(this._readNodeWSAddress)
140
145
  .setAllowInsecure(options.allowInsecure)
141
146
  .setCustomWS(this._wsLibrary)
142
147
 
148
+ if (typeof options.authority !== 'undefined')
149
+ this._options.setAuthority(options.authority)
150
+
143
151
  if (typeof options.maxMessageBytes !== 'undefined')
144
152
  this._options.setMaxMessageBytes(options.maxMessageBytes)
145
153
 
@@ -154,6 +162,11 @@ export default class TCaBCIClient {
154
162
  if (chainVersion) this._chainVersion = chainVersion
155
163
  }
156
164
 
165
+ SetIsWeb(isWeb) {
166
+ this._isWeb = isWeb
167
+ return isWeb
168
+ }
169
+
157
170
  SetDebug(debug) {
158
171
  this._options.setDebug(debug)
159
172
 
@@ -268,6 +281,7 @@ export default class TCaBCIClient {
268
281
  * @returns {Promise<TCaBCIClient>}
269
282
  */
270
283
  async Stop(code = 1000) {
284
+ this._listenRequested = false
271
285
  this._httpClient.abort()
272
286
  await this._disconnect(code)
273
287
  this._setConnected(false)
@@ -289,6 +303,22 @@ export default class TCaBCIClient {
289
303
  })
290
304
  }
291
305
 
306
+ /**
307
+ * Explicitly request privileged Listen with a configured authority handshake.
308
+ * The request is repeated after reconnect. Rejection stops automatic retries.
309
+ * @returns {TCaBCIClient}
310
+ */
311
+ Listen() {
312
+ if (!this.IsConnected()) throw new Error(NOT_CONNECTED)
313
+
314
+ this._ws.send(
315
+ new Message({ isWeb: this._isWeb, type: LISTENMessage }).ToJSON()
316
+ )
317
+ this._listenRequested = true
318
+
319
+ return this
320
+ }
321
+
292
322
  /**
293
323
  * Subscribe to at most 251 unique addresses, each at most 2048 characters.
294
324
  * The combined active address list is bounded. Proof maps are copied;
@@ -325,6 +355,7 @@ export default class TCaBCIClient {
325
355
  txTypes
326
356
  }).ToJSON()
327
357
  )
358
+ this._listenRequested = false
328
359
  this._setSubscribeAddresses(addrs)._setSubscribeSignedData(proof)
329
360
 
330
361
  return this
@@ -334,7 +365,7 @@ export default class TCaBCIClient {
334
365
  * @return {TCaBCIClient}
335
366
  */
336
367
  Unsubscribe() {
337
- if (!this.IsSubscribed()) {
368
+ if (!this.IsSubscribed() && !this._listenRequested) {
338
369
  throw new Error(NOT_SUBSCRIBED)
339
370
  }
340
371
 
@@ -347,6 +378,7 @@ export default class TCaBCIClient {
347
378
  }).ToJSON()
348
379
  )
349
380
  } finally {
381
+ this._listenRequested = false
350
382
  this._clearSubscription()
351
383
  }
352
384
 
@@ -820,6 +852,9 @@ export default class TCaBCIClient {
820
852
  if (!current()) return
821
853
 
822
854
  this._setConnected(true)
855
+
856
+ if (this._listenRequested) this.Listen()
857
+
823
858
  this._callSuccessCallback(e)
824
859
  })
825
860
  ws.addMessageListener((message) => {
@@ -878,6 +913,7 @@ export default class TCaBCIClient {
878
913
  */
879
914
  async Dispose() {
880
915
  await this.Stop()
916
+ this._options.setAuthority(null)
881
917
  this._successCb = null
882
918
  this._errorCb = null
883
919
  this._closeCb = null
@@ -981,6 +1017,17 @@ export default class TCaBCIClient {
981
1017
 
982
1018
  if (this._listenCb) this._listenCb(null, msg.transaction, null)
983
1019
 
1020
+ break
1021
+ case MListen:
1022
+ if (_msg.State !== OK) {
1023
+ void this.Stop().catch(() => {})
1024
+ this._callErrorCallback(new Error('Listen rejected'))
1025
+
1026
+ return
1027
+ }
1028
+
1029
+ if (this._listenCb) this._listenCb(null, null, _msg)
1030
+
984
1031
  break
985
1032
  case Subscription:
986
1033
  if (_msg.State === OK) {
package/message.d.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import { TXType } from './transaction.js'
2
2
  import { JSONValue, ParseResult } from './types.js'
3
3
 
4
- export type MessageType = 'subscribe' | 'unsubscribe' | 0 | 1 | 2 | 3 | 4
4
+ export type MessageType =
5
+ | 'subscribe'
6
+ | 'unsubscribe'
7
+ | 'listen'
8
+ | 0
9
+ | 1
10
+ | 2
11
+ | 3
12
+ | 4
5
13
 
6
14
  export type MType = 0 | 1 | 2 | 3 | 4
7
15
 
@@ -9,6 +17,7 @@ export type MState = 1 | 2
9
17
 
10
18
  export declare const SUBSCRIBEMessage: 'subscribe',
11
19
  UNSUBSCRIBEMessage: 'unsubscribe',
20
+ LISTENMessage: 'listen',
12
21
  Block: 0,
13
22
  Transaction: 1,
14
23
  Subscription: 2,
package/message.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  export const SUBSCRIBEMessage = 'subscribe',
11
11
  UNSUBSCRIBEMessage = 'unsubscribe',
12
+ LISTENMessage = 'listen',
12
13
  Block = 0,
13
14
  Transaction = 1,
14
15
  Subscription = 2,
@@ -163,6 +164,7 @@ export default class Message {
163
164
  ![
164
165
  SUBSCRIBEMessage,
165
166
  UNSUBSCRIBEMessage,
167
+ LISTENMessage,
166
168
  Block,
167
169
  Transaction,
168
170
  Subscription,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tchain/tcabci-read-js-client",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "TransferChain Fastest Read Network WebSocket Client",
5
5
  "homepage": "https://github.com/TransferChain/tcabci-read-js-client#readme",
6
6
  "bugs": {
@@ -26,11 +26,24 @@
26
26
  "websocket.js",
27
27
  "websocketOptions.js",
28
28
  "bytea.js",
29
- "validation.js"
29
+ "validation.js",
30
+ "authority.js"
30
31
  ],
31
32
  "type": "module",
32
33
  "main": "client.js",
33
34
  "types": "client.d.ts",
35
+ "scripts": {
36
+ "format": "oxfmt ./",
37
+ "lint": "oxfmt ./ && oxlint --fix ./",
38
+ "lint:check": "oxfmt --check ./ && oxlint ./",
39
+ "test": "pnpm run test:unit && pnpm run test:security && pnpm run test:types",
40
+ "test:all": "pnpm test",
41
+ "test:coverage": "node --experimental-test-coverage --test test/security.test.js",
42
+ "test:unit": "mocha --timeout 5000 test/errors_test.js test/websocketOptions_test.js test/websocket_test.js",
43
+ "test:security": "node --test test/security.test.js",
44
+ "test:types": "tsc --noEmit --strict --module NodeNext --moduleResolution NodeNext --target ES2022 test/consumer.ts",
45
+ "test:integration": "mocha --timeout 7000 test/client_test.js"
46
+ },
34
47
  "dependencies": {
35
48
  "cockatiel": "^4.0.0",
36
49
  "reconnecting-websocket": "^4.4.0"
@@ -48,16 +61,5 @@
48
61
  "engines": {
49
62
  "node": ">=24.0.0"
50
63
  },
51
- "scripts": {
52
- "format": "oxfmt ./",
53
- "lint": "oxfmt ./ && oxlint --fix ./",
54
- "lint:check": "oxfmt --check ./ && oxlint ./",
55
- "test": "pnpm run test:unit && pnpm run test:security && pnpm run test:types",
56
- "test:all": "pnpm test",
57
- "test:coverage": "node --experimental-test-coverage --test test/security.test.js",
58
- "test:unit": "mocha --timeout 5000 test/errors_test.js test/websocketOptions_test.js test/websocket_test.js",
59
- "test:security": "node --test test/security.test.js",
60
- "test:types": "tsc --noEmit --strict --module NodeNext --moduleResolution NodeNext --target ES2022 test/consumer.ts",
61
- "test:integration": "mocha --timeout 7000 test/client_test.js"
62
- }
63
- }
64
+ "packageManager": "pnpm@12.5.1"
65
+ }
package/types.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { WSAuthorityOptions } from './authority.js'
2
+
1
3
  /** Immutable JSON snapshot; byte buffers are not JSON values. */
2
4
  export type JSONValue =
3
5
  | null
@@ -19,6 +21,7 @@ export interface RequestOptions {
19
21
  }
20
22
 
21
23
  export interface ClientOptions {
24
+ authority?: WSAuthorityOptions
22
25
  allowInsecure?: boolean
23
26
  timeoutMs?: number
24
27
  maxResponseBytes?: number
@@ -1,3 +1,4 @@
1
+ import type { WSAuthorityOptions } from './authority.js'
1
2
  import { WebSocketConstructor } from './types.js'
2
3
 
3
4
  export declare type Timeout = number
@@ -71,6 +72,8 @@ export declare class Options {
71
72
 
72
73
  get customWS(): WebSocketConstructor | undefined
73
74
 
75
+ setAuthority(authority: WSAuthorityOptions | null): this
76
+
74
77
  get url(): string
75
78
 
76
79
  make(): {
@@ -1,3 +1,4 @@
1
+ import { createWSAuthorityV3 } from './authority.js'
1
2
  import { CLIENT_VERSION, endpoint, positiveInteger } from './validation.js'
2
3
 
3
4
  export const HTTPSPort = 443,
@@ -13,6 +14,7 @@ export class Options {
13
14
  _longpoll = false
14
15
  _protocols = []
15
16
  _customWS
17
+ _authority = null
16
18
  _endpoints = ['ws', 'longpoll']
17
19
  _version = CLIENT_VERSION
18
20
  _allowInsecure = false
@@ -280,6 +282,37 @@ export class Options {
280
282
  return this._customWS
281
283
  }
282
284
 
285
+ /**
286
+ * Configure a synchronous v3 signer. Requires a Node ws-compatible constructor
287
+ * supporting handshake headers; browser WebSocket cannot send these headers.
288
+ * @param {import('./authority.js').WSAuthorityOptions|null} authority
289
+ * @returns {Options}
290
+ */
291
+ setAuthority(authority) {
292
+ if (authority === null) {
293
+ this._authority = null
294
+
295
+ return this
296
+ }
297
+
298
+ if (
299
+ !authority ||
300
+ typeof authority.address !== 'string' ||
301
+ authority.address.length === 0 ||
302
+ authority.address.length > 2048 ||
303
+ /[;\r\n]/.test(authority.address) ||
304
+ typeof authority.sign !== 'function' ||
305
+ typeof this._customWS?.prototype?.on !== 'function'
306
+ )
307
+ throw new TypeError(
308
+ 'Authority requires a Node ws-compatible constructor and signer'
309
+ )
310
+
311
+ this._authority = { address: authority.address, sign: authority.sign }
312
+
313
+ return this
314
+ }
315
+
283
316
  /**
284
317
  * @return {string}
285
318
  */
@@ -297,8 +330,35 @@ export class Options {
297
330
  * @return {{WebSocket: ?any, maxReconnectionDelay: number, minReconnectionDelay: number, reconnectionDelayGrowFactor: number, minUptime: number, connectionTimeout: number, maxRetries: number, maxEnqueuedMessages: number, startClosed: boolean, debug: boolean }}
298
331
  */
299
332
  make() {
333
+ let WS = this._customWS
334
+
335
+ if (this._authority) {
336
+ const Base = WS,
337
+ options = this
338
+
339
+ WS = class extends Base {
340
+ constructor(url, protocols) {
341
+ const authority = options._authority
342
+
343
+ if (!authority)
344
+ throw new Error('Websocket authority configuration released')
345
+
346
+ super(url, protocols, {
347
+ followRedirects: false,
348
+ headers: {
349
+ Client: `tcabci-read-js-client/${CLIENT_VERSION}`,
350
+ 'X-Authority': createWSAuthorityV3(
351
+ authority.address,
352
+ authority.sign
353
+ )
354
+ }
355
+ })
356
+ }
357
+ }
358
+ }
359
+
300
360
  return {
301
- ...(this._customWS ? { WebSocket: this._customWS } : {}),
361
+ ...(WS ? { WebSocket: WS } : {}),
302
362
  maxReconnectionDelay: this._maxConnectionDelay,
303
363
  minReconnectionDelay: this._minReconnectionDelay,
304
364
  reconnectionDelayGrowFactor: this._reconnectionDelayGrowFactor,