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