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

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,10 +66,10 @@ 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'
69
- _version = `v2.8.0`
70
73
  /**
71
74
  * @type {?SuccessCallback}
72
75
  */
@@ -135,11 +138,15 @@ export default class TCaBCIClient {
135
138
  ).href
136
139
  }
137
140
 
141
+ this._isWeb = true
138
142
  this._httpClient = new HTTP(this._readNodeAddress, options)
139
143
  this._options = new Options(this._readNodeWSAddress)
140
144
  .setAllowInsecure(options.allowInsecure)
141
145
  .setCustomWS(this._wsLibrary)
142
146
 
147
+ if (typeof options.authority !== 'undefined')
148
+ this._options.setAuthority(options.authority)
149
+
143
150
  if (typeof options.maxMessageBytes !== 'undefined')
144
151
  this._options.setMaxMessageBytes(options.maxMessageBytes)
145
152
 
@@ -154,6 +161,11 @@ export default class TCaBCIClient {
154
161
  if (chainVersion) this._chainVersion = chainVersion
155
162
  }
156
163
 
164
+ SetIsWeb(isWeb) {
165
+ this._isWeb = isWeb
166
+ return isWeb
167
+ }
168
+
157
169
  SetDebug(debug) {
158
170
  this._options.setDebug(debug)
159
171
 
@@ -268,6 +280,7 @@ export default class TCaBCIClient {
268
280
  * @returns {Promise<TCaBCIClient>}
269
281
  */
270
282
  async Stop(code = 1000) {
283
+ this._listenRequested = false
271
284
  this._httpClient.abort()
272
285
  await this._disconnect(code)
273
286
  this._setConnected(false)
@@ -289,6 +302,22 @@ export default class TCaBCIClient {
289
302
  })
290
303
  }
291
304
 
305
+ /**
306
+ * Explicitly request privileged Listen with a configured authority handshake.
307
+ * The request is repeated after reconnect. Rejection stops automatic retries.
308
+ * @returns {TCaBCIClient}
309
+ */
310
+ Listen() {
311
+ if (!this.IsConnected()) throw new Error(NOT_CONNECTED)
312
+
313
+ this._ws.send(
314
+ new Message({ isWeb: this._isWeb, type: LISTENMessage }).ToJSON()
315
+ )
316
+ this._listenRequested = true
317
+
318
+ return this
319
+ }
320
+
292
321
  /**
293
322
  * Subscribe to at most 251 unique addresses, each at most 2048 characters.
294
323
  * The combined active address list is bounded. Proof maps are copied;
@@ -325,6 +354,7 @@ export default class TCaBCIClient {
325
354
  txTypes
326
355
  }).ToJSON()
327
356
  )
357
+ this._listenRequested = false
328
358
  this._setSubscribeAddresses(addrs)._setSubscribeSignedData(proof)
329
359
 
330
360
  return this
@@ -334,7 +364,7 @@ export default class TCaBCIClient {
334
364
  * @return {TCaBCIClient}
335
365
  */
336
366
  Unsubscribe() {
337
- if (!this.IsSubscribed()) {
367
+ if (!this.IsSubscribed() && !this._listenRequested) {
338
368
  throw new Error(NOT_SUBSCRIBED)
339
369
  }
340
370
 
@@ -347,6 +377,7 @@ export default class TCaBCIClient {
347
377
  }).ToJSON()
348
378
  )
349
379
  } finally {
380
+ this._listenRequested = false
350
381
  this._clearSubscription()
351
382
  }
352
383
 
@@ -820,6 +851,9 @@ export default class TCaBCIClient {
820
851
  if (!current()) return
821
852
 
822
853
  this._setConnected(true)
854
+
855
+ if (this._listenRequested) this.Listen()
856
+
823
857
  this._callSuccessCallback(e)
824
858
  })
825
859
  ws.addMessageListener((message) => {
@@ -878,6 +912,7 @@ export default class TCaBCIClient {
878
912
  */
879
913
  async Dispose() {
880
914
  await this.Stop()
915
+ this._options.setAuthority(null)
881
916
  this._successCb = null
882
917
  this._errorCb = null
883
918
  this._closeCb = null
@@ -981,6 +1016,17 @@ export default class TCaBCIClient {
981
1016
 
982
1017
  if (this._listenCb) this._listenCb(null, msg.transaction, null)
983
1018
 
1019
+ break
1020
+ case MListen:
1021
+ if (_msg.State !== OK) {
1022
+ void this.Stop().catch(() => {})
1023
+ this._callErrorCallback(new Error('Listen rejected'))
1024
+
1025
+ return
1026
+ }
1027
+
1028
+ if (this._listenCb) this._listenCb(null, null, _msg)
1029
+
984
1030
  break
985
1031
  case Subscription:
986
1032
  if (_msg.State === OK) {
package/constants.d.ts CHANGED
@@ -1 +1,3 @@
1
- export declare const READ_NODE_ADDRESS: string, READ_NODE_WS_ADDRESS: string
1
+ export declare const CLIENT_VERSION: string,
2
+ READ_NODE_ADDRESS: string,
3
+ READ_NODE_WS_ADDRESS: string
package/constants.js CHANGED
@@ -1,2 +1,3 @@
1
- export const READ_NODE_ADDRESS = 'https://read-node-01.transferchain.io',
1
+ export const CLIENT_VERSION = '2.8.2',
2
+ READ_NODE_ADDRESS = 'https://read-node-01.transferchain.io',
2
3
  READ_NODE_WS_ADDRESS = 'wss://read-node-01.transferchain.io/ws'
package/http.js CHANGED
@@ -2,12 +2,12 @@ import { SendThrow } from './util.js'
2
2
  import { ERR_NETWORK, FetchError, INVALID_ARGUMENTS } from './errors.js'
3
3
  import { Breaker } from './breaker.js'
4
4
  import {
5
- CLIENT_VERSION,
6
5
  endpoint,
7
6
  positiveInteger,
8
7
  cloneAndFreeze,
9
8
  byteLength
10
9
  } from './validation.js'
10
+ import { CLIENT_VERSION } from './constants.js'
11
11
 
12
12
  export class HTTP {
13
13
  _baseURL
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.2",
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
package/validation.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { JSONValue } from './types.js'
2
2
 
3
- export declare const CLIENT_VERSION: string, MAX_ADDRESSES: number
3
+ export declare const MAX_ADDRESSES: number
4
4
 
5
5
  export declare function positiveInteger(value: number, name: string): number
6
6
 
package/validation.js CHANGED
@@ -1,5 +1,4 @@
1
- export const CLIENT_VERSION = '2.8.0',
2
- MAX_ADDRESSES = 251
1
+ export const MAX_ADDRESSES = 251
3
2
 
4
3
  export function positiveInteger(value, name) {
5
4
  if (!Number.isSafeInteger(value) || value <= 0)
@@ -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,4 +1,6 @@
1
- import { CLIENT_VERSION, endpoint, positiveInteger } from './validation.js'
1
+ import { createWSAuthorityV3 } from './authority.js'
2
+ import { endpoint, positiveInteger } from './validation.js'
3
+ import { CLIENT_VERSION } from './constants.js'
2
4
 
3
5
  export const HTTPSPort = 443,
4
6
  HTTPPort = 80,
@@ -13,6 +15,7 @@ export class Options {
13
15
  _longpoll = false
14
16
  _protocols = []
15
17
  _customWS
18
+ _authority = null
16
19
  _endpoints = ['ws', 'longpoll']
17
20
  _version = CLIENT_VERSION
18
21
  _allowInsecure = false
@@ -280,6 +283,37 @@ export class Options {
280
283
  return this._customWS
281
284
  }
282
285
 
286
+ /**
287
+ * Configure a synchronous v3 signer. Requires a Node ws-compatible constructor
288
+ * supporting handshake headers; browser WebSocket cannot send these headers.
289
+ * @param {import('./authority.js').WSAuthorityOptions|null} authority
290
+ * @returns {Options}
291
+ */
292
+ setAuthority(authority) {
293
+ if (authority === null) {
294
+ this._authority = null
295
+
296
+ return this
297
+ }
298
+
299
+ if (
300
+ !authority ||
301
+ typeof authority.address !== 'string' ||
302
+ authority.address.length === 0 ||
303
+ authority.address.length > 2048 ||
304
+ /[;\r\n]/.test(authority.address) ||
305
+ typeof authority.sign !== 'function' ||
306
+ typeof this._customWS?.prototype?.on !== 'function'
307
+ )
308
+ throw new TypeError(
309
+ 'Authority requires a Node ws-compatible constructor and signer'
310
+ )
311
+
312
+ this._authority = { address: authority.address, sign: authority.sign }
313
+
314
+ return this
315
+ }
316
+
283
317
  /**
284
318
  * @return {string}
285
319
  */
@@ -297,8 +331,40 @@ export class Options {
297
331
  * @return {{WebSocket: ?any, maxReconnectionDelay: number, minReconnectionDelay: number, reconnectionDelayGrowFactor: number, minUptime: number, connectionTimeout: number, maxRetries: number, maxEnqueuedMessages: number, startClosed: boolean, debug: boolean }}
298
332
  */
299
333
  make() {
334
+ let WS = this._customWS
335
+
336
+ if (this._authority || typeof WS?.prototype?.on === 'function') {
337
+ const Base = WS,
338
+ options = this,
339
+ requiresAuthority = Boolean(this._authority)
340
+
341
+ WS = class extends Base {
342
+ constructor(url, protocols) {
343
+ const authority = options._authority
344
+
345
+ if (requiresAuthority && !authority
346
+ throw new Error('Websocket authority configuration released')
347
+
348
+ super(url, protocols, {
349
+ followRedirects: false,
350
+ headers: {
351
+ Client: `tcabci-read-js-client/${CLIENT_VERSION}`,
352
+ ...(authority
353
+ ? {
354
+ 'X-Authority': createWSAuthorityV3(
355
+ authority.address,
356
+ authority.sign
357
+ )
358
+ }
359
+ : {})
360
+ }
361
+ })
362
+ }
363
+ }
364
+ }
365
+
300
366
  return {
301
- ...(this._customWS ? { WebSocket: this._customWS } : {}),
367
+ ...(WS ? { WebSocket: WS } : {}),
302
368
  maxReconnectionDelay: this._maxConnectionDelay,
303
369
  minReconnectionDelay: this._minReconnectionDelay,
304
370
  reconnectionDelayGrowFactor: this._reconnectionDelayGrowFactor,
@@ -310,7 +376,7 @@ export class Options {
310
376
  // ReconnectingWebSocket debug logs outgoing payloads. Never enable it.
311
377
  debug: false,
312
378
  headers: {
313
- Client: `tcabaci-read-js-client/${this._version}`
379
+ Client: `tcabci-read-js-client/${this._version}`
314
380
  }
315
381
  }
316
382
  }