@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/LICENSE +22 -22
- package/README.md +68 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/generated/resources.d.ts +62 -24
- package/dist/generated/resources.d.ts.map +1 -1
- package/dist/generated/types.d.ts +43 -27
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +273 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +268 -4
- package/dist/index.mjs.map +1 -1
- package/dist/realtime.d.ts +137 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/webhook.d.ts +90 -0
- package/dist/webhook.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -418,7 +418,7 @@ class InvezgoResources {
|
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
const encoder = new TextEncoder();
|
|
421
|
+
const encoder$1 = new TextEncoder();
|
|
422
422
|
class LruCache {
|
|
423
423
|
maxEntries;
|
|
424
424
|
maxBytes;
|
|
@@ -449,7 +449,7 @@ class LruCache {
|
|
|
449
449
|
return entry.body;
|
|
450
450
|
}
|
|
451
451
|
set(key, body, ttlMs) {
|
|
452
|
-
const size = encoder.encode(body).length;
|
|
452
|
+
const size = encoder$1.encode(body).length;
|
|
453
453
|
this.delete(key);
|
|
454
454
|
if (ttlMs <= 0 || size > this.maxBytes || this.maxEntries <= 0)
|
|
455
455
|
return;
|
|
@@ -539,7 +539,7 @@ function errorClassFor(status) {
|
|
|
539
539
|
return InvezgoError;
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
-
const VERSION = '2.
|
|
542
|
+
const VERSION = '2.2.0';
|
|
543
543
|
const DEFAULT_BASE_URL = 'https://api.invezgo.com';
|
|
544
544
|
const PERSONAL_SEGMENTS = new Set(['watchlists', 'journals', 'portfolios', 'trades', 'alerts', 'screener']);
|
|
545
545
|
const QUOTA_MESSAGE = /quota|kuota|monthly|bulanan/i;
|
|
@@ -765,17 +765,281 @@ function buildError(res, text) {
|
|
|
765
765
|
});
|
|
766
766
|
}
|
|
767
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
|
+
|
|
768
968
|
class Invezgo extends InvezgoResources {
|
|
769
969
|
#transport;
|
|
970
|
+
#config;
|
|
770
971
|
constructor(config) {
|
|
771
972
|
const transport = new Transport(config);
|
|
772
973
|
super(transport);
|
|
773
974
|
this.#transport = transport;
|
|
975
|
+
this.#config = config;
|
|
774
976
|
}
|
|
775
977
|
clearCache() {
|
|
776
978
|
this.#transport.clearCache();
|
|
777
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);
|
|
778
1042
|
}
|
|
779
1043
|
|
|
780
|
-
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 };
|
|
781
1045
|
//# sourceMappingURL=index.mjs.map
|