@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/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 +145 -16
- package/dist/generated/resources.d.ts.map +1 -1
- package/dist/generated/types.d.ts +547 -107
- 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 +279 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +274 -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.js
CHANGED
|
@@ -97,6 +97,9 @@ class AnalysisResource {
|
|
|
97
97
|
getShareholderClassifyTable(code, options) {
|
|
98
98
|
return this.transport.request({ method: 'GET', route: '/analysis/shareholder/classify-table/{code}', cache: 'default', pathParams: { code } }, options);
|
|
99
99
|
}
|
|
100
|
+
getFreeFloat(code, params, options) {
|
|
101
|
+
return this.transport.request({ method: 'GET', route: '/analysis/free-float/{code}', cache: 'default', pathParams: { code }, query: params }, options);
|
|
102
|
+
}
|
|
100
103
|
getSummaryStock(code, params, options) {
|
|
101
104
|
return this.transport.request({ method: 'GET', route: '/analysis/summary/stock/{code}', cache: 'default', pathParams: { code }, query: params }, options);
|
|
102
105
|
}
|
|
@@ -178,6 +181,9 @@ class AnalysisResource {
|
|
|
178
181
|
getKeystatChart(code, params, options) {
|
|
179
182
|
return this.transport.request({ method: 'GET', route: '/analysis/keystat-chart/{code}', cache: 'default', pathParams: { code }, query: params }, options);
|
|
180
183
|
}
|
|
184
|
+
getFlowOfFunds(code, params, options) {
|
|
185
|
+
return this.transport.request({ method: 'GET', route: '/analysis/flow-of-funds/{code}', cache: 'default', pathParams: { code }, query: params }, options);
|
|
186
|
+
}
|
|
181
187
|
getCalendar(params, options) {
|
|
182
188
|
return this.transport.request({ method: 'GET', route: '/analysis/calendar', cache: 'default', query: params }, options);
|
|
183
189
|
}
|
|
@@ -416,7 +422,7 @@ class InvezgoResources {
|
|
|
416
422
|
}
|
|
417
423
|
}
|
|
418
424
|
|
|
419
|
-
const encoder = new TextEncoder();
|
|
425
|
+
const encoder$1 = new TextEncoder();
|
|
420
426
|
class LruCache {
|
|
421
427
|
maxEntries;
|
|
422
428
|
maxBytes;
|
|
@@ -447,7 +453,7 @@ class LruCache {
|
|
|
447
453
|
return entry.body;
|
|
448
454
|
}
|
|
449
455
|
set(key, body, ttlMs) {
|
|
450
|
-
const size = encoder.encode(body).length;
|
|
456
|
+
const size = encoder$1.encode(body).length;
|
|
451
457
|
this.delete(key);
|
|
452
458
|
if (ttlMs <= 0 || size > this.maxBytes || this.maxEntries <= 0)
|
|
453
459
|
return;
|
|
@@ -537,7 +543,7 @@ function errorClassFor(status) {
|
|
|
537
543
|
return InvezgoError;
|
|
538
544
|
}
|
|
539
545
|
|
|
540
|
-
const VERSION = '2.
|
|
546
|
+
const VERSION = '2.2.0';
|
|
541
547
|
const DEFAULT_BASE_URL = 'https://api.invezgo.com';
|
|
542
548
|
const PERSONAL_SEGMENTS = new Set(['watchlists', 'journals', 'portfolios', 'trades', 'alerts', 'screener']);
|
|
543
549
|
const QUOTA_MESSAGE = /quota|kuota|monthly|bulanan/i;
|
|
@@ -763,16 +769,280 @@ function buildError(res, text) {
|
|
|
763
769
|
});
|
|
764
770
|
}
|
|
765
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
|
+
|
|
766
972
|
class Invezgo extends InvezgoResources {
|
|
767
973
|
#transport;
|
|
974
|
+
#config;
|
|
768
975
|
constructor(config) {
|
|
769
976
|
const transport = new Transport(config);
|
|
770
977
|
super(transport);
|
|
771
978
|
this.#transport = transport;
|
|
979
|
+
this.#config = config;
|
|
772
980
|
}
|
|
773
981
|
clearCache() {
|
|
774
982
|
this.#transport.clearCache();
|
|
775
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);
|
|
776
1046
|
}
|
|
777
1047
|
|
|
778
1048
|
exports.AlertsResource = AlertsResource;
|
|
@@ -781,9 +1051,11 @@ exports.AuthenticationError = AuthenticationError;
|
|
|
781
1051
|
exports.BatchResource = BatchResource;
|
|
782
1052
|
exports.ConnectionError = ConnectionError;
|
|
783
1053
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
1054
|
+
exports.DEFAULT_REALTIME_URL = DEFAULT_REALTIME_URL;
|
|
784
1055
|
exports.Invezgo = Invezgo;
|
|
785
1056
|
exports.InvezgoClient = Invezgo;
|
|
786
1057
|
exports.InvezgoError = InvezgoError;
|
|
1058
|
+
exports.InvezgoRealtime = InvezgoRealtime;
|
|
787
1059
|
exports.InvezgoResources = InvezgoResources;
|
|
788
1060
|
exports.JournalsResource = JournalsResource;
|
|
789
1061
|
exports.NotFoundError = NotFoundError;
|
|
@@ -800,5 +1072,9 @@ exports.UsageResource = UsageResource;
|
|
|
800
1072
|
exports.VERSION = VERSION;
|
|
801
1073
|
exports.ValidationError = ValidationError;
|
|
802
1074
|
exports.WatchlistsResource = WatchlistsResource;
|
|
1075
|
+
exports.WebhookVerificationError = WebhookVerificationError;
|
|
803
1076
|
exports.default = Invezgo;
|
|
1077
|
+
exports.isWebhookEvent = isWebhookEvent;
|
|
1078
|
+
exports.parseWebhook = parseWebhook;
|
|
1079
|
+
exports.verifyWebhook = verifyWebhook;
|
|
804
1080
|
//# sourceMappingURL=index.js.map
|