@invezgo/sdk 2.1.0 → 2.2.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/dist/index.js CHANGED
@@ -422,7 +422,7 @@ class InvezgoResources {
422
422
  }
423
423
  }
424
424
 
425
- const encoder = new TextEncoder();
425
+ const encoder$1 = new TextEncoder();
426
426
  class LruCache {
427
427
  maxEntries;
428
428
  maxBytes;
@@ -453,7 +453,7 @@ class LruCache {
453
453
  return entry.body;
454
454
  }
455
455
  set(key, body, ttlMs) {
456
- const size = encoder.encode(body).length;
456
+ const size = encoder$1.encode(body).length;
457
457
  this.delete(key);
458
458
  if (ttlMs <= 0 || size > this.maxBytes || this.maxEntries <= 0)
459
459
  return;
@@ -543,7 +543,7 @@ function errorClassFor(status) {
543
543
  return InvezgoError;
544
544
  }
545
545
 
546
- const VERSION = '2.1.0';
546
+ const VERSION = '2.2.0';
547
547
  const DEFAULT_BASE_URL = 'https://api.invezgo.com';
548
548
  const PERSONAL_SEGMENTS = new Set(['watchlists', 'journals', 'portfolios', 'trades', 'alerts', 'screener']);
549
549
  const QUOTA_MESSAGE = /quota|kuota|monthly|bulanan/i;
@@ -769,16 +769,280 @@ function buildError(res, text) {
769
769
  });
770
770
  }
771
771
 
772
+ const DEFAULT_REALTIME_URL = 'wss://api.invezgo.com/ws';
773
+ const handshakeStatus = new Map();
774
+ let undiciHooked = false;
775
+ function hookUndici() {
776
+ if (undiciHooked)
777
+ return;
778
+ undiciHooked = true;
779
+ const dc = globalThis.process?.getBuiltinModule?.('node:diagnostics_channel');
780
+ dc?.subscribe('undici:request:headers', (m) => {
781
+ const key = `${m?.request?.origin}${m?.request?.path}`;
782
+ if (handshakeStatus.has(key))
783
+ handshakeStatus.set(key, m.response?.statusCode ?? 0);
784
+ });
785
+ }
786
+ function defaultWebSocket() {
787
+ const node = Number(globalThis.process?.versions?.node?.split('.')[0]);
788
+ return node >= 22 ? globalThis.WebSocket : undefined;
789
+ }
790
+ class InvezgoRealtime {
791
+ #url;
792
+ #apiKey;
793
+ #reconnect;
794
+ #WS;
795
+ #native;
796
+ #listeners = new Map();
797
+ #ws;
798
+ #ready = false;
799
+ #closed = false;
800
+ #attempt = 0;
801
+ #timer;
802
+ #trades = null;
803
+ #minLot;
804
+ #minValue;
805
+ #orderbook = new Set();
806
+ constructor(options) {
807
+ if (!options || typeof options.apiKey !== 'string' || !options.apiKey.trim()) {
808
+ throw new TypeError('Invezgo: `apiKey` is required');
809
+ }
810
+ const WS = options.WebSocket ?? defaultWebSocket();
811
+ if (typeof WS !== 'function') {
812
+ throw new TypeError('Invezgo: this runtime has no WebSocket that accepts headers; pass `WebSocket: require("ws")` (Node < 22)');
813
+ }
814
+ this.#WS = WS;
815
+ this.#native = !options.WebSocket;
816
+ this.#apiKey = options.apiKey;
817
+ this.#url = options.url ?? DEFAULT_REALTIME_URL;
818
+ this.#reconnect = options.reconnect ?? true;
819
+ if (this.#native)
820
+ hookUndici();
821
+ this.#connect();
822
+ }
823
+ on(event, fn) {
824
+ if (!this.#listeners.has(event))
825
+ this.#listeners.set(event, new Set());
826
+ this.#listeners.get(event).add(fn);
827
+ return this;
828
+ }
829
+ off(event, fn) {
830
+ this.#listeners.get(event)?.delete(fn);
831
+ return this;
832
+ }
833
+ subscribeTrades(codes, filter = {}) {
834
+ const upper = codes?.map((c) => c.toUpperCase());
835
+ if (upper) {
836
+ const next = new Set(this.#trades instanceof Set ? this.#trades : []);
837
+ upper.forEach((c) => next.add(c));
838
+ if (next.size > 200)
839
+ throw new RangeError('Invezgo: at most 200 trade codes per connection');
840
+ if (this.#trades !== '*')
841
+ this.#trades = next;
842
+ }
843
+ else {
844
+ this.#trades = '*';
845
+ }
846
+ if (filter.minLot !== undefined)
847
+ this.#minLot = filter.minLot;
848
+ if (filter.minValue !== undefined)
849
+ this.#minValue = filter.minValue;
850
+ this.#send({ op: 'subscribe', channel: 'trades', codes: upper, min_lot: filter.minLot, min_value: filter.minValue });
851
+ }
852
+ subscribeOrderbook(codes) {
853
+ if (!Array.isArray(codes) || codes.length === 0)
854
+ throw new TypeError('Invezgo: orderbook `codes` must not be empty');
855
+ const upper = codes.map((c) => c.toUpperCase());
856
+ if (new Set([...this.#orderbook, ...upper]).size > 20)
857
+ throw new RangeError('Invezgo: at most 20 orderbook codes per connection');
858
+ upper.forEach((c) => this.#orderbook.add(c));
859
+ this.#send({ op: 'subscribe', channel: 'orderbook', codes: upper });
860
+ }
861
+ unsubscribe(channel, codes) {
862
+ const upper = codes?.map((c) => c.toUpperCase());
863
+ if (channel === 'trades') {
864
+ if (!upper)
865
+ [this.#trades, this.#minLot, this.#minValue] = [null, undefined, undefined];
866
+ else if (this.#trades instanceof Set)
867
+ upper.forEach((c) => this.#trades.delete(c));
868
+ }
869
+ else if (!upper)
870
+ this.#orderbook.clear();
871
+ else
872
+ upper.forEach((c) => this.#orderbook.delete(c));
873
+ this.#send({ op: 'unsubscribe', channel, codes: upper });
874
+ }
875
+ ping() {
876
+ if (!this.#ready)
877
+ throw new ConnectionError({ status: 0, message: 'Invezgo: realtime connection is not open' });
878
+ this.#send({ op: 'ping' });
879
+ }
880
+ close() {
881
+ this.#closed = true;
882
+ clearTimeout(this.#timer);
883
+ this.#ws?.close(1000);
884
+ }
885
+ #emit(event, payload) {
886
+ this.#listeners.get(event)?.forEach((fn) => fn(payload));
887
+ }
888
+ #send(message) {
889
+ if (this.#ready)
890
+ this.#ws.send(JSON.stringify(message));
891
+ }
892
+ #replay() {
893
+ if (this.#trades) {
894
+ this.#send({
895
+ op: 'subscribe', channel: 'trades', codes: this.#trades === '*' ? undefined : [...this.#trades],
896
+ min_lot: this.#minLot, min_value: this.#minValue,
897
+ });
898
+ }
899
+ if (this.#orderbook.size)
900
+ this.#send({ op: 'subscribe', channel: 'orderbook', codes: [...this.#orderbook] });
901
+ }
902
+ #connect() {
903
+ const statusKey = this.#url.replace(/^ws/, 'http');
904
+ if (this.#native)
905
+ handshakeStatus.set(statusKey, 0);
906
+ let opened = false;
907
+ let failure = '';
908
+ const ws = new this.#WS(this.#url, { headers: { Authorization: `Bearer ${this.#apiKey}`, 'User-Agent': `invezgo-js-sdk/${VERSION}` } });
909
+ this.#ws = ws;
910
+ ws.onopen = () => {
911
+ opened = true;
912
+ handshakeStatus.delete(statusKey);
913
+ };
914
+ ws.onerror = (ev) => {
915
+ failure = String(ev?.message ?? '');
916
+ };
917
+ ws.onmessage = (ev) => {
918
+ let msg;
919
+ try {
920
+ msg = JSON.parse(String(ev.data));
921
+ }
922
+ catch {
923
+ return;
924
+ }
925
+ if (msg.type === 'connected') {
926
+ this.#ready = true;
927
+ this.#attempt = 0;
928
+ this.#replay();
929
+ }
930
+ this.#emit('message', msg);
931
+ if (msg.type === 'error') {
932
+ this.#emit('error', new ValidationError({ status: 400, message: String(msg.message), body: msg }));
933
+ }
934
+ else if (typeof msg.type === 'string') {
935
+ this.#emit(msg.type, msg);
936
+ }
937
+ };
938
+ ws.onclose = (ev) => {
939
+ this.#ready = false;
940
+ let fatal = this.#closed || ev.code === 4400;
941
+ if (!opened) {
942
+ const status = Number(/response: (\d{3})/.exec(failure)?.[1] ?? handshakeStatus.get(statusKey) ?? 0);
943
+ handshakeStatus.delete(statusKey);
944
+ if (!this.#closed) {
945
+ this.#emit('error', handshakeError(status, failure));
946
+ fatal ||= status === 401 || status === 403;
947
+ }
948
+ }
949
+ this.#emit('close', { code: ev.code, reason: String(ev.reason ?? '') });
950
+ if (fatal || !this.#reconnect)
951
+ return;
952
+ const delay = Math.min(2 ** this.#attempt, 30);
953
+ this.#attempt++;
954
+ this.#emit('reconnect', { attempt: this.#attempt, delay });
955
+ this.#timer = setTimeout(() => this.#connect(), delay * 1000);
956
+ };
957
+ }
958
+ }
959
+ function handshakeError(status, detail) {
960
+ const init = { status, message: `Invezgo: realtime handshake failed${status ? ` (HTTP ${status})` : ''}${detail ? `: ${detail}` : ''}`, path: '/ws' };
961
+ if (status === 401)
962
+ return new AuthenticationError(init);
963
+ if (status === 403)
964
+ return new PaymentRequiredError(init);
965
+ if (status === 429)
966
+ return new RateLimitError({ ...init, retryAfter: 0, quotaExceeded: false });
967
+ if (status >= 500)
968
+ return new ServerError(init);
969
+ return new ConnectionError(init);
970
+ }
971
+
772
972
  class Invezgo extends InvezgoResources {
773
973
  #transport;
974
+ #config;
774
975
  constructor(config) {
775
976
  const transport = new Transport(config);
776
977
  super(transport);
777
978
  this.#transport = transport;
979
+ this.#config = config;
778
980
  }
779
981
  clearCache() {
780
982
  this.#transport.clearCache();
781
983
  }
984
+ realtime(options = {}) {
985
+ const url = `${(this.#config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '').replace(/^http/, 'ws')}/ws`;
986
+ return new InvezgoRealtime({ ...options, apiKey: this.#config.apiKey, url });
987
+ }
988
+ }
989
+
990
+ class WebhookVerificationError extends Error {
991
+ reason;
992
+ constructor(reason, message, cause) {
993
+ super(message, cause === undefined ? undefined : { cause });
994
+ this.name = 'WebhookVerificationError';
995
+ this.reason = reason;
996
+ }
997
+ }
998
+ function isWebhookEvent(event, name) {
999
+ return event.event === name;
1000
+ }
1001
+ const encoder = new TextEncoder();
1002
+ function header(headers, name, short) {
1003
+ if (typeof headers.get === 'function')
1004
+ return headers.get(name) ?? '';
1005
+ const wanted = [name.toLowerCase(), short];
1006
+ for (const [key, value] of Object.entries(headers)) {
1007
+ if (wanted.includes(key.toLowerCase()))
1008
+ return (Array.isArray(value) ? value[0] : value) ?? '';
1009
+ }
1010
+ return '';
1011
+ }
1012
+ function parseWebhook(rawBody) {
1013
+ let event;
1014
+ try {
1015
+ event = JSON.parse(typeof rawBody === 'string' ? rawBody : new TextDecoder().decode(rawBody));
1016
+ }
1017
+ catch (cause) {
1018
+ throw new WebhookVerificationError('payload', 'Invezgo webhook: body is not valid JSON', cause);
1019
+ }
1020
+ if (!event || typeof event !== 'object' || Array.isArray(event)) {
1021
+ throw new WebhookVerificationError('payload', 'Invezgo webhook: body is not a JSON object');
1022
+ }
1023
+ event.event = `${event.domain}.${event.type}`;
1024
+ return event;
1025
+ }
1026
+ async function verifyWebhook(rawBody, headers, secret, options = {}) {
1027
+ if (typeof secret !== 'string' || !secret)
1028
+ throw new TypeError('Invezgo webhook: `secret` is required');
1029
+ const timestamp = header(headers, 'X-Invezgo-Timestamp', 'timestamp');
1030
+ const signature = header(headers, 'X-Invezgo-Signature', 'signature');
1031
+ const now = options.now ?? Date.now() / 1000;
1032
+ if (!/^\d+$/.test(timestamp) || Math.abs(now - Number(timestamp)) > (options.tolerance ?? 300)) {
1033
+ throw new WebhookVerificationError('timestamp', 'Invezgo webhook: timestamp is missing, invalid or outside the tolerance');
1034
+ }
1035
+ const hex = /^sha256=([0-9a-f]{64})$/.exec(signature)?.[1];
1036
+ const body = typeof rawBody === 'string' ? encoder.encode(rawBody) : rawBody;
1037
+ const prefix = encoder.encode(`${timestamp}.`);
1038
+ const message = new Uint8Array(prefix.length + body.length);
1039
+ message.set(prefix);
1040
+ message.set(body, prefix.length);
1041
+ const key = await globalThis.crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
1042
+ const valid = !!hex && (await globalThis.crypto.subtle.verify('HMAC', key, Uint8Array.from(hex.match(/../g), (h) => parseInt(h, 16)), message));
1043
+ if (!valid)
1044
+ throw new WebhookVerificationError('signature', 'Invezgo webhook: signature mismatch');
1045
+ return parseWebhook(body);
782
1046
  }
783
1047
 
784
1048
  exports.AlertsResource = AlertsResource;
@@ -787,9 +1051,11 @@ exports.AuthenticationError = AuthenticationError;
787
1051
  exports.BatchResource = BatchResource;
788
1052
  exports.ConnectionError = ConnectionError;
789
1053
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
1054
+ exports.DEFAULT_REALTIME_URL = DEFAULT_REALTIME_URL;
790
1055
  exports.Invezgo = Invezgo;
791
1056
  exports.InvezgoClient = Invezgo;
792
1057
  exports.InvezgoError = InvezgoError;
1058
+ exports.InvezgoRealtime = InvezgoRealtime;
793
1059
  exports.InvezgoResources = InvezgoResources;
794
1060
  exports.JournalsResource = JournalsResource;
795
1061
  exports.NotFoundError = NotFoundError;
@@ -806,5 +1072,9 @@ exports.UsageResource = UsageResource;
806
1072
  exports.VERSION = VERSION;
807
1073
  exports.ValidationError = ValidationError;
808
1074
  exports.WatchlistsResource = WatchlistsResource;
1075
+ exports.WebhookVerificationError = WebhookVerificationError;
809
1076
  exports.default = Invezgo;
1077
+ exports.isWebhookEvent = isWebhookEvent;
1078
+ exports.parseWebhook = parseWebhook;
1079
+ exports.verifyWebhook = verifyWebhook;
810
1080
  //# sourceMappingURL=index.js.map