@piaa/sdk 1.0.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 +21 -0
- package/README.md +187 -0
- package/dist/client.d.ts +51 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/config.d.ts +92 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/http/transport.d.ts +24 -0
- package/dist/http/transport.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +822 -0
- package/dist/index.js.map +20 -0
- package/dist/logger.d.ts +25 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/realtime/events.d.ts +34 -0
- package/dist/realtime/events.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +46 -0
- package/dist/realtime/socket.d.ts.map +1 -0
- package/dist/resources/market.d.ts +25 -0
- package/dist/resources/market.d.ts.map +1 -0
- package/dist/resources/news.d.ts +14 -0
- package/dist/resources/news.d.ts.map +1 -0
- package/dist/resources/social.d.ts +14 -0
- package/dist/resources/social.d.ts.map +1 -0
- package/dist/resources/ws.d.ts +14 -0
- package/dist/resources/ws.d.ts.map +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
class PiaError extends Error {
|
|
3
|
+
isPiaError = true;
|
|
4
|
+
statusCode;
|
|
5
|
+
endpoint;
|
|
6
|
+
method;
|
|
7
|
+
requestId;
|
|
8
|
+
constructor(message, details) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "PiaError";
|
|
11
|
+
this.statusCode = details?.statusCode;
|
|
12
|
+
this.endpoint = details?.endpoint;
|
|
13
|
+
this.method = details?.method;
|
|
14
|
+
this.requestId = details?.requestId;
|
|
15
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class ConfigurationError extends PiaError {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "ConfigurationError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ValidationError extends PiaError {
|
|
27
|
+
paramName;
|
|
28
|
+
constructor(message, paramName) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "ValidationError";
|
|
31
|
+
this.paramName = paramName;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class AuthenticationError extends PiaError {
|
|
36
|
+
constructor(message = "Authentication failed. Verify that your API key is valid and active.", details) {
|
|
37
|
+
super(message, details);
|
|
38
|
+
this.name = "AuthenticationError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class PermissionError extends PiaError {
|
|
43
|
+
requiredScope;
|
|
44
|
+
constructor(message, requiredScope, details) {
|
|
45
|
+
super(message, details);
|
|
46
|
+
this.name = "PermissionError";
|
|
47
|
+
this.requiredScope = requiredScope;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class RateLimitError extends PiaError {
|
|
52
|
+
retryAfterSeconds;
|
|
53
|
+
dailyLimit;
|
|
54
|
+
dailyRemaining;
|
|
55
|
+
minuteLimit;
|
|
56
|
+
minuteRemaining;
|
|
57
|
+
constructor(message, options) {
|
|
58
|
+
super(message, options?.details);
|
|
59
|
+
this.name = "RateLimitError";
|
|
60
|
+
this.retryAfterSeconds = options?.retryAfterSeconds;
|
|
61
|
+
this.dailyLimit = options?.dailyLimit;
|
|
62
|
+
this.dailyRemaining = options?.dailyRemaining;
|
|
63
|
+
this.minuteLimit = options?.minuteLimit;
|
|
64
|
+
this.minuteRemaining = options?.minuteRemaining;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class TimeoutError extends PiaError {
|
|
69
|
+
timeoutMs;
|
|
70
|
+
constructor(message, timeoutMs, details) {
|
|
71
|
+
super(message, details);
|
|
72
|
+
this.name = "TimeoutError";
|
|
73
|
+
this.timeoutMs = timeoutMs;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class NetworkError extends PiaError {
|
|
78
|
+
causeError;
|
|
79
|
+
constructor(message, cause, details) {
|
|
80
|
+
super(message, details);
|
|
81
|
+
this.name = "NetworkError";
|
|
82
|
+
this.causeError = cause;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class ParseError extends PiaError {
|
|
87
|
+
rawText;
|
|
88
|
+
constructor(message, rawText, details) {
|
|
89
|
+
super(message, details);
|
|
90
|
+
this.name = "ParseError";
|
|
91
|
+
this.rawText = rawText;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class ApiError extends PiaError {
|
|
96
|
+
rawBody;
|
|
97
|
+
constructor(message, statusCode, rawBody, details) {
|
|
98
|
+
super(message, { ...details, statusCode });
|
|
99
|
+
this.name = "ApiError";
|
|
100
|
+
this.rawBody = rawBody;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/logger.ts
|
|
105
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
106
|
+
/wi_live_[a-zA-Z0-9_-]{10,}/g,
|
|
107
|
+
/Bearer\s+[a-zA-Z0-9._-]+/gi,
|
|
108
|
+
/api[_-]?key["':\s]+["']?([a-zA-Z0-9_-]+)["']?/gi,
|
|
109
|
+
/password["':\s]+["']?([^"'\s]+)["']?/gi
|
|
110
|
+
];
|
|
111
|
+
function redactSensitive(input) {
|
|
112
|
+
let redacted = input;
|
|
113
|
+
for (const pattern of SENSITIVE_KEY_PATTERNS) {
|
|
114
|
+
redacted = redacted.replace(pattern, (match) => {
|
|
115
|
+
if (match.startsWith("wi_live_")) {
|
|
116
|
+
return `wi_live_***${match.slice(-4)}`;
|
|
117
|
+
}
|
|
118
|
+
if (match.toLowerCase().startsWith("bearer ")) {
|
|
119
|
+
return "Bearer [REDACTED]";
|
|
120
|
+
}
|
|
121
|
+
return "[REDACTED]";
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return redacted;
|
|
125
|
+
}
|
|
126
|
+
var LOG_LEVELS = {
|
|
127
|
+
debug: 10,
|
|
128
|
+
info: 20,
|
|
129
|
+
warn: 30,
|
|
130
|
+
error: 40,
|
|
131
|
+
silent: 50
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
class DefaultLogger {
|
|
135
|
+
levelNum;
|
|
136
|
+
prefix = "[PIA-SDK]";
|
|
137
|
+
constructor(level = "warn") {
|
|
138
|
+
this.levelNum = LOG_LEVELS[level] ?? LOG_LEVELS.warn;
|
|
139
|
+
}
|
|
140
|
+
safeFormat(msg, args) {
|
|
141
|
+
const cleanMsg = `${this.prefix} ${redactSensitive(msg)}`;
|
|
142
|
+
const cleanArgs = args.map((arg) => {
|
|
143
|
+
if (typeof arg === "string")
|
|
144
|
+
return redactSensitive(arg);
|
|
145
|
+
if (typeof arg === "object" && arg !== null) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(redactSensitive(JSON.stringify(arg)));
|
|
148
|
+
} catch {
|
|
149
|
+
return "[Unserializable Object]";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return arg;
|
|
153
|
+
});
|
|
154
|
+
return [cleanMsg, ...cleanArgs];
|
|
155
|
+
}
|
|
156
|
+
debug(message, ...args) {
|
|
157
|
+
if (this.levelNum <= LOG_LEVELS.debug) {
|
|
158
|
+
const [msg, ...cleanArgs] = this.safeFormat(message, args);
|
|
159
|
+
console.debug(msg, ...cleanArgs);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
info(message, ...args) {
|
|
163
|
+
if (this.levelNum <= LOG_LEVELS.info) {
|
|
164
|
+
const [msg, ...cleanArgs] = this.safeFormat(message, args);
|
|
165
|
+
console.info(msg, ...cleanArgs);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
warn(message, ...args) {
|
|
169
|
+
if (this.levelNum <= LOG_LEVELS.warn) {
|
|
170
|
+
const [msg, ...cleanArgs] = this.safeFormat(message, args);
|
|
171
|
+
console.warn(msg, ...cleanArgs);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
error(message, ...args) {
|
|
175
|
+
if (this.levelNum <= LOG_LEVELS.error) {
|
|
176
|
+
const [msg, ...cleanArgs] = this.safeFormat(message, args);
|
|
177
|
+
console.error(msg, ...cleanArgs);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/config.ts
|
|
183
|
+
var DEFAULT_BASE_URL = "https://api-engine.wign.dev";
|
|
184
|
+
var DEFAULT_WS_URL = "wss://api-engine.wign.dev/api/v1/ws";
|
|
185
|
+
var DEFAULT_TIMEOUT_MS = 15000;
|
|
186
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
187
|
+
var DEFAULT_RETRY_DELAY_MS = 500;
|
|
188
|
+
var DEFAULT_MAX_RETRY_DELAY_MS = 1e4;
|
|
189
|
+
function resolveEnvApiKey() {
|
|
190
|
+
if (typeof process !== "undefined" && process?.env) {
|
|
191
|
+
return process.env.PIA_API_KEY || process.env.ATSLD_API_KEY || process.env.CORE_API_KEY || process.env.NEXT_PUBLIC_API_KEY;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
function resolveConfig(options = {}) {
|
|
196
|
+
const apiKey = options.apiKey?.trim() || resolveEnvApiKey()?.trim();
|
|
197
|
+
if (!apiKey) {
|
|
198
|
+
throw new ConfigurationError("API key is required. Pass { apiKey: '...' } to PiaClient or set the PIA_API_KEY environment variable.");
|
|
199
|
+
}
|
|
200
|
+
const rawBaseUrl = options.baseUrl?.trim() || DEFAULT_BASE_URL;
|
|
201
|
+
const baseUrl = rawBaseUrl.replace(/\/+$/, "");
|
|
202
|
+
try {
|
|
203
|
+
const parsed = new URL(baseUrl);
|
|
204
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
205
|
+
throw new Error("Protocol must be http or https");
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
throw new ConfigurationError(`Invalid baseUrl '${baseUrl}': ${err instanceof Error ? err.message : String(err)}`);
|
|
209
|
+
}
|
|
210
|
+
const rawWsUrl = options.wsUrl?.trim() || DEFAULT_WS_URL;
|
|
211
|
+
const wsUrl = rawWsUrl.replace(/\/+$/, "");
|
|
212
|
+
try {
|
|
213
|
+
const parsedWs = new URL(wsUrl);
|
|
214
|
+
if (!["ws:", "wss:"].includes(parsedWs.protocol)) {
|
|
215
|
+
throw new Error("Protocol must be ws or wss");
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
throw new ConfigurationError(`Invalid wsUrl '${wsUrl}': ${err instanceof Error ? err.message : String(err)}`);
|
|
219
|
+
}
|
|
220
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
221
|
+
if (timeoutMs < 100) {
|
|
222
|
+
throw new ConfigurationError("timeoutMs must be at least 100 milliseconds.");
|
|
223
|
+
}
|
|
224
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
225
|
+
if (maxRetries < 0) {
|
|
226
|
+
throw new ConfigurationError("maxRetries cannot be negative.");
|
|
227
|
+
}
|
|
228
|
+
const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
229
|
+
const maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
230
|
+
const debug = options.debug ?? false;
|
|
231
|
+
const logLevel = options.logLevel ?? (debug ? "debug" : "warn");
|
|
232
|
+
const logger = options.logger ?? new DefaultLogger(logLevel);
|
|
233
|
+
const resolvedFetch = options.fetch || (typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined);
|
|
234
|
+
if (!resolvedFetch) {
|
|
235
|
+
throw new ConfigurationError("A valid fetch implementation was not found in the runtime. Pass a custom fetch to PiaClientOptions.");
|
|
236
|
+
}
|
|
237
|
+
const resolvedWebSocket = options.WebSocket || (typeof globalThis !== "undefined" && globalThis.WebSocket ? globalThis.WebSocket : undefined);
|
|
238
|
+
return {
|
|
239
|
+
apiKey,
|
|
240
|
+
baseUrl,
|
|
241
|
+
wsUrl,
|
|
242
|
+
timeoutMs,
|
|
243
|
+
maxRetries,
|
|
244
|
+
retryDelayMs,
|
|
245
|
+
maxRetryDelayMs,
|
|
246
|
+
headers: { ...options.headers || {} },
|
|
247
|
+
fetch: resolvedFetch,
|
|
248
|
+
WebSocket: resolvedWebSocket,
|
|
249
|
+
logger,
|
|
250
|
+
debug
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/http/transport.ts
|
|
255
|
+
var RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
256
|
+
var SDK_VERSION = "1.0.0";
|
|
257
|
+
|
|
258
|
+
class HttpTransport {
|
|
259
|
+
config;
|
|
260
|
+
lastRateLimitInfo = {};
|
|
261
|
+
constructor(config) {
|
|
262
|
+
this.config = config;
|
|
263
|
+
}
|
|
264
|
+
getRateLimitInfo() {
|
|
265
|
+
return { ...this.lastRateLimitInfo };
|
|
266
|
+
}
|
|
267
|
+
async request(endpoint, method = "GET", body, options) {
|
|
268
|
+
const url = `${this.config.baseUrl}${endpoint.startsWith("/") ? "" : "/"}${endpoint}`;
|
|
269
|
+
const maxRetries = options?.maxRetries ?? this.config.maxRetries;
|
|
270
|
+
const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
|
|
271
|
+
let attempt = 0;
|
|
272
|
+
while (true) {
|
|
273
|
+
attempt++;
|
|
274
|
+
this.config.logger.debug(`HTTP ${method} ${endpoint} (Attempt ${attempt}/${maxRetries + 1})`);
|
|
275
|
+
const controller = new AbortController;
|
|
276
|
+
let isTimedOut = false;
|
|
277
|
+
const timer = setTimeout(() => {
|
|
278
|
+
isTimedOut = true;
|
|
279
|
+
controller.abort();
|
|
280
|
+
}, timeoutMs);
|
|
281
|
+
if (options?.signal) {
|
|
282
|
+
if (options.signal.aborted) {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
throw options.signal.reason;
|
|
285
|
+
}
|
|
286
|
+
options.signal.addEventListener("abort", () => {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
controller.abort(options.signal?.reason);
|
|
289
|
+
}, { once: true });
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const headers = {
|
|
293
|
+
"x-api-key": this.config.apiKey,
|
|
294
|
+
Accept: "application/json",
|
|
295
|
+
"User-Agent": `pia-sdk-ts/${SDK_VERSION}`,
|
|
296
|
+
...this.config.headers,
|
|
297
|
+
...options?.headers || {}
|
|
298
|
+
};
|
|
299
|
+
let serializedBody;
|
|
300
|
+
if (body !== undefined) {
|
|
301
|
+
headers["Content-Type"] = "application/json";
|
|
302
|
+
serializedBody = JSON.stringify(body);
|
|
303
|
+
}
|
|
304
|
+
const response = await this.config.fetch(url, {
|
|
305
|
+
method,
|
|
306
|
+
headers,
|
|
307
|
+
body: serializedBody,
|
|
308
|
+
signal: controller.signal
|
|
309
|
+
});
|
|
310
|
+
clearTimeout(timer);
|
|
311
|
+
this.parseTelemetryHeaders(response.headers);
|
|
312
|
+
if (response.ok) {
|
|
313
|
+
return await this.parseResponseBody(response, endpoint, method);
|
|
314
|
+
}
|
|
315
|
+
const status = response.status;
|
|
316
|
+
const errorText = await response.text().catch(() => "");
|
|
317
|
+
let errorJson = null;
|
|
318
|
+
try {
|
|
319
|
+
if (errorText)
|
|
320
|
+
errorJson = JSON.parse(errorText);
|
|
321
|
+
} catch {}
|
|
322
|
+
const errorMessage = errorJson?.message || errorJson?.error || (errorText ? errorText.slice(0, 200) : `HTTP ${status}`);
|
|
323
|
+
const isRetryable = RETRYABLE_STATUS_CODES.has(status) && attempt <= maxRetries;
|
|
324
|
+
if (isRetryable) {
|
|
325
|
+
const delay = this.calculateBackoffDelay(attempt, response.headers);
|
|
326
|
+
this.config.logger.warn(`Request to ${endpoint} failed with HTTP ${status}. Retrying in ${delay}ms...`);
|
|
327
|
+
await this.sleep(delay);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
this.handleHttpError(status, errorMessage, endpoint, method, response.headers, errorJson);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
if (isTimedOut) {
|
|
334
|
+
throw new TimeoutError(`Request to ${endpoint} exceeded timeout of ${timeoutMs}ms.`, timeoutMs, { endpoint, method });
|
|
335
|
+
}
|
|
336
|
+
if (options?.signal?.aborted) {
|
|
337
|
+
throw options.signal.reason;
|
|
338
|
+
}
|
|
339
|
+
if (err instanceof Error && "isPiaError" in err) {
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
if (attempt <= maxRetries) {
|
|
343
|
+
const delay = this.calculateBackoffDelay(attempt);
|
|
344
|
+
this.config.logger.warn(`Network error calling ${endpoint}: ${err instanceof Error ? err.message : String(err)}. Retrying in ${delay}ms...`);
|
|
345
|
+
await this.sleep(delay);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
throw new NetworkError(`Network transport failed connecting to ${endpoint}: ${err instanceof Error ? err.message : String(err)}`, err, { endpoint, method });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
parseTelemetryHeaders(headers) {
|
|
353
|
+
const parseNum = (name) => {
|
|
354
|
+
const val = headers.get(name);
|
|
355
|
+
if (!val)
|
|
356
|
+
return;
|
|
357
|
+
const num = parseInt(val, 10);
|
|
358
|
+
return Number.isFinite(num) ? num : undefined;
|
|
359
|
+
};
|
|
360
|
+
this.lastRateLimitInfo = {
|
|
361
|
+
limit: parseNum("x-ratelimit-limit"),
|
|
362
|
+
remaining: parseNum("x-ratelimit-remaining"),
|
|
363
|
+
resetSeconds: parseNum("x-ratelimit-reset"),
|
|
364
|
+
dailyLimit: parseNum("x-dailyquota-limit"),
|
|
365
|
+
dailyRemaining: parseNum("x-dailyquota-remaining")
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
calculateBackoffDelay(attempt, headers) {
|
|
369
|
+
const retryAfter = headers?.get("retry-after");
|
|
370
|
+
if (retryAfter) {
|
|
371
|
+
const parsedSeconds = parseInt(retryAfter, 10);
|
|
372
|
+
if (Number.isFinite(parsedSeconds) && parsedSeconds > 0) {
|
|
373
|
+
return Math.min(parsedSeconds * 1000, this.config.maxRetryDelayMs);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const base = this.config.retryDelayMs * Math.pow(2, attempt - 1);
|
|
377
|
+
const capped = Math.min(base, this.config.maxRetryDelayMs);
|
|
378
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
379
|
+
return Math.round(capped * jitter);
|
|
380
|
+
}
|
|
381
|
+
async parseResponseBody(response, endpoint, method) {
|
|
382
|
+
const text = await response.text();
|
|
383
|
+
if (!text || text.trim() === "") {
|
|
384
|
+
return {};
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
return JSON.parse(text);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
throw new ParseError(`Failed to parse JSON response from ${endpoint}: ${err instanceof Error ? err.message : String(err)}`, text.slice(0, 500), { statusCode: response.status, endpoint, method });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
handleHttpError(status, message, endpoint, method, headers, body) {
|
|
393
|
+
const details = {
|
|
394
|
+
statusCode: status,
|
|
395
|
+
endpoint,
|
|
396
|
+
method,
|
|
397
|
+
requestId: headers.get("x-request-id") || headers.get("cf-ray"),
|
|
398
|
+
rawResponse: body
|
|
399
|
+
};
|
|
400
|
+
if (status === 401) {
|
|
401
|
+
throw new AuthenticationError(message, details);
|
|
402
|
+
}
|
|
403
|
+
if (status === 403) {
|
|
404
|
+
throw new PermissionError(message, undefined, details);
|
|
405
|
+
}
|
|
406
|
+
if (status === 429) {
|
|
407
|
+
const retryAfter = headers.get("retry-after");
|
|
408
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined;
|
|
409
|
+
throw new RateLimitError(message, {
|
|
410
|
+
retryAfterSeconds: Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
|
|
411
|
+
dailyLimit: this.lastRateLimitInfo.dailyLimit,
|
|
412
|
+
dailyRemaining: this.lastRateLimitInfo.dailyRemaining,
|
|
413
|
+
minuteLimit: this.lastRateLimitInfo.limit,
|
|
414
|
+
minuteRemaining: this.lastRateLimitInfo.remaining,
|
|
415
|
+
details
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
throw new ApiError(message, status, body, details);
|
|
419
|
+
}
|
|
420
|
+
sleep(ms) {
|
|
421
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/realtime/events.ts
|
|
426
|
+
class TypedEventEmitter {
|
|
427
|
+
listeners = new Map;
|
|
428
|
+
on(event, listener) {
|
|
429
|
+
if (!this.listeners.has(event)) {
|
|
430
|
+
this.listeners.set(event, new Set);
|
|
431
|
+
}
|
|
432
|
+
this.listeners.get(event).add(listener);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
off(event, listener) {
|
|
436
|
+
const set = this.listeners.get(event);
|
|
437
|
+
if (set) {
|
|
438
|
+
set.delete(listener);
|
|
439
|
+
if (set.size === 0) {
|
|
440
|
+
this.listeners.delete(event);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
emit(event, ...args) {
|
|
446
|
+
const set = this.listeners.get(event);
|
|
447
|
+
if (!set || set.size === 0)
|
|
448
|
+
return false;
|
|
449
|
+
for (const listener of Array.from(set)) {
|
|
450
|
+
try {
|
|
451
|
+
listener(...args);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error(`[PIA-SDK] Unhandled error in '${event}' listener:`, err);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
removeAllListeners(event) {
|
|
459
|
+
if (event) {
|
|
460
|
+
this.listeners.delete(event);
|
|
461
|
+
} else {
|
|
462
|
+
this.listeners.clear();
|
|
463
|
+
}
|
|
464
|
+
return this;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/realtime/socket.ts
|
|
469
|
+
class RealtimeClient extends TypedEventEmitter {
|
|
470
|
+
config;
|
|
471
|
+
ws = null;
|
|
472
|
+
state = "DISCONNECTED";
|
|
473
|
+
subscribedSymbols = new Set;
|
|
474
|
+
reconnectAttempt = 0;
|
|
475
|
+
reconnectTimer = null;
|
|
476
|
+
isExplicitlyClosed = false;
|
|
477
|
+
pingIntervalTimer = null;
|
|
478
|
+
constructor(config) {
|
|
479
|
+
super();
|
|
480
|
+
this.config = config;
|
|
481
|
+
}
|
|
482
|
+
getState() {
|
|
483
|
+
return this.state;
|
|
484
|
+
}
|
|
485
|
+
connect() {
|
|
486
|
+
if (this.state === "CONNECTING" || this.state === "AUTHENTICATING" || this.state === "AUTHENTICATED") {
|
|
487
|
+
this.config.logger.debug("Socket already active or connecting.");
|
|
488
|
+
return this;
|
|
489
|
+
}
|
|
490
|
+
this.isExplicitlyClosed = false;
|
|
491
|
+
this.initiateConnection();
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
initiateConnection() {
|
|
495
|
+
const WsConstructor = this.config.WebSocket || typeof globalThis !== "undefined" && globalThis.WebSocket;
|
|
496
|
+
if (!WsConstructor) {
|
|
497
|
+
throw new ConfigurationError("WebSocket implementation not available. In Node.js environments, pass { WebSocket: require('ws') } in options.");
|
|
498
|
+
}
|
|
499
|
+
this.state = "CONNECTING";
|
|
500
|
+
this.config.logger.info(`Connecting to Realtime WebSocket: ${this.config.wsUrl}`);
|
|
501
|
+
try {
|
|
502
|
+
this.ws = new WsConstructor(this.config.wsUrl);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
this.handleSocketFailure(err);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.ws.onopen = () => {
|
|
508
|
+
this.config.logger.info("WebSocket TCP connection opened. Initiating In-Band Auth...");
|
|
509
|
+
this.state = "AUTHENTICATING";
|
|
510
|
+
this.reconnectAttempt = 0;
|
|
511
|
+
this.emit("connect");
|
|
512
|
+
this.sendRaw({
|
|
513
|
+
action: "auth",
|
|
514
|
+
api_key: this.config.apiKey
|
|
515
|
+
});
|
|
516
|
+
this.startHeartbeat();
|
|
517
|
+
};
|
|
518
|
+
this.ws.onmessage = (event) => {
|
|
519
|
+
this.handleIncomingMessage(event.data);
|
|
520
|
+
};
|
|
521
|
+
this.ws.onerror = (event) => {
|
|
522
|
+
const err = new NetworkError("WebSocket connection error", event?.message || event?.error || event);
|
|
523
|
+
this.config.logger.error("WebSocket error:", err.message);
|
|
524
|
+
this.emit("error", err);
|
|
525
|
+
};
|
|
526
|
+
this.ws.onclose = (event) => {
|
|
527
|
+
const code = event?.code ?? 1006;
|
|
528
|
+
const reason = event?.reason ?? "";
|
|
529
|
+
const wasClean = event?.wasClean ?? false;
|
|
530
|
+
this.stopHeartbeat();
|
|
531
|
+
this.state = "DISCONNECTED";
|
|
532
|
+
this.ws = null;
|
|
533
|
+
this.config.logger.warn(`WebSocket closed: code=${code}, reason=${reason}`);
|
|
534
|
+
this.emit("disconnect", { code, reason, wasClean });
|
|
535
|
+
if (code === 4001 || reason.toLowerCase().includes("unauthorized")) {
|
|
536
|
+
this.emit("error", new AuthenticationError("WebSocket authentication rejected by server."));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!this.isExplicitlyClosed) {
|
|
540
|
+
this.scheduleReconnect();
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
handleIncomingMessage(rawPayload) {
|
|
545
|
+
let messageText;
|
|
546
|
+
if (typeof rawPayload === "string") {
|
|
547
|
+
messageText = rawPayload;
|
|
548
|
+
} else if (rawPayload instanceof ArrayBuffer || typeof Buffer !== "undefined" && Buffer && Buffer.isBuffer(rawPayload)) {
|
|
549
|
+
messageText = new TextDecoder().decode(rawPayload);
|
|
550
|
+
} else {
|
|
551
|
+
messageText = String(rawPayload);
|
|
552
|
+
}
|
|
553
|
+
let payload;
|
|
554
|
+
try {
|
|
555
|
+
payload = JSON.parse(messageText);
|
|
556
|
+
} catch {
|
|
557
|
+
this.emit("raw", messageText);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.emit("raw", payload);
|
|
561
|
+
if (payload.event === "authenticated") {
|
|
562
|
+
this.state = "AUTHENTICATED";
|
|
563
|
+
this.config.logger.info("WebSocket authenticated successfully.");
|
|
564
|
+
this.emit("authenticated", payload.data || {});
|
|
565
|
+
if (this.subscribedSymbols.size > 0) {
|
|
566
|
+
this.flushSubscriptions();
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (payload.event === "market.trade" || payload.event === "market.tick") {
|
|
571
|
+
const tick = payload.data?.tick || payload.data;
|
|
572
|
+
if (tick) {
|
|
573
|
+
this.emit("tick", tick);
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (payload.event === "market.snapshot" && payload.data) {
|
|
578
|
+
this.emit("snapshot", payload.data);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (payload.error) {
|
|
582
|
+
if (payload.error === "unauthorized" || payload.error === 401) {
|
|
583
|
+
this.emit("error", new AuthenticationError(payload.message || "Unauthorized WebSocket message."));
|
|
584
|
+
} else {
|
|
585
|
+
this.emit("error", new NetworkError(payload.message || `Server reported error: ${payload.error}`));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
subscribe(symbols) {
|
|
590
|
+
const list = Array.isArray(symbols) ? symbols : [symbols];
|
|
591
|
+
const newSymbols = [];
|
|
592
|
+
for (const sym of list) {
|
|
593
|
+
const clean = sym.trim().toUpperCase();
|
|
594
|
+
if (clean && !this.subscribedSymbols.has(clean)) {
|
|
595
|
+
this.subscribedSymbols.add(clean);
|
|
596
|
+
newSymbols.push(clean);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (newSymbols.length > 0 && this.state === "AUTHENTICATED") {
|
|
600
|
+
this.sendRaw({
|
|
601
|
+
action: "subscribe",
|
|
602
|
+
symbols: newSymbols
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return this;
|
|
606
|
+
}
|
|
607
|
+
unsubscribe(symbols) {
|
|
608
|
+
const list = Array.isArray(symbols) ? symbols : [symbols];
|
|
609
|
+
const removed = [];
|
|
610
|
+
for (const sym of list) {
|
|
611
|
+
const clean = sym.trim().toUpperCase();
|
|
612
|
+
if (this.subscribedSymbols.delete(clean)) {
|
|
613
|
+
removed.push(clean);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (removed.length > 0 && this.state === "AUTHENTICATED") {
|
|
617
|
+
this.sendRaw({
|
|
618
|
+
action: "unsubscribe",
|
|
619
|
+
symbols: removed
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return this;
|
|
623
|
+
}
|
|
624
|
+
flushSubscriptions() {
|
|
625
|
+
if (this.subscribedSymbols.size === 0)
|
|
626
|
+
return;
|
|
627
|
+
this.sendRaw({
|
|
628
|
+
action: "subscribe",
|
|
629
|
+
symbols: Array.from(this.subscribedSymbols)
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
sendRaw(data) {
|
|
633
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
this.ws.send(JSON.stringify(data));
|
|
638
|
+
return true;
|
|
639
|
+
} catch (err) {
|
|
640
|
+
this.config.logger.error("Failed to send WebSocket message:", err);
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
scheduleReconnect() {
|
|
645
|
+
this.reconnectAttempt++;
|
|
646
|
+
const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempt - 1), 15000);
|
|
647
|
+
const jitteredDelay = Math.round(delay * (0.8 + Math.random() * 0.4));
|
|
648
|
+
this.config.logger.info(`Reconnecting in ${jitteredDelay}ms (Attempt ${this.reconnectAttempt})...`);
|
|
649
|
+
clearTimeout(this.reconnectTimer);
|
|
650
|
+
this.reconnectTimer = setTimeout(() => {
|
|
651
|
+
if (!this.isExplicitlyClosed) {
|
|
652
|
+
this.initiateConnection();
|
|
653
|
+
}
|
|
654
|
+
}, jitteredDelay);
|
|
655
|
+
}
|
|
656
|
+
handleSocketFailure(err) {
|
|
657
|
+
this.state = "DISCONNECTED";
|
|
658
|
+
this.ws = null;
|
|
659
|
+
const netErr = new NetworkError("Failed to initiate WebSocket connection", err);
|
|
660
|
+
this.emit("error", netErr);
|
|
661
|
+
if (!this.isExplicitlyClosed) {
|
|
662
|
+
this.scheduleReconnect();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
startHeartbeat() {
|
|
666
|
+
this.stopHeartbeat();
|
|
667
|
+
this.pingIntervalTimer = setInterval(() => {
|
|
668
|
+
if (this.state === "AUTHENTICATED") {
|
|
669
|
+
this.sendRaw({ action: "ping" });
|
|
670
|
+
}
|
|
671
|
+
}, 30000);
|
|
672
|
+
}
|
|
673
|
+
stopHeartbeat() {
|
|
674
|
+
if (this.pingIntervalTimer) {
|
|
675
|
+
clearInterval(this.pingIntervalTimer);
|
|
676
|
+
this.pingIntervalTimer = null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
disconnect() {
|
|
680
|
+
this.isExplicitlyClosed = true;
|
|
681
|
+
this.stopHeartbeat();
|
|
682
|
+
clearTimeout(this.reconnectTimer);
|
|
683
|
+
if (this.ws) {
|
|
684
|
+
this.state = "CLOSING";
|
|
685
|
+
try {
|
|
686
|
+
this.ws.close(1000, "Client disconnect");
|
|
687
|
+
} catch {}
|
|
688
|
+
this.ws = null;
|
|
689
|
+
}
|
|
690
|
+
this.state = "DISCONNECTED";
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/resources/market.ts
|
|
695
|
+
class MarketResource {
|
|
696
|
+
transport;
|
|
697
|
+
constructor(transport) {
|
|
698
|
+
this.transport = transport;
|
|
699
|
+
}
|
|
700
|
+
async getPrices(options) {
|
|
701
|
+
return this.transport.request("/api/v1/market/prices", "GET", undefined, options);
|
|
702
|
+
}
|
|
703
|
+
async getCandles(symbol, options) {
|
|
704
|
+
if (!symbol || typeof symbol !== "string" || symbol.trim() === "") {
|
|
705
|
+
throw new ValidationError("Symbol must be a non-empty string.", "symbol");
|
|
706
|
+
}
|
|
707
|
+
const cleanSymbol = symbol.trim().toUpperCase();
|
|
708
|
+
const params = new URLSearchParams;
|
|
709
|
+
if (options?.timeframe)
|
|
710
|
+
params.set("tf", options.timeframe);
|
|
711
|
+
if (options?.limit)
|
|
712
|
+
params.set("limit", String(options.limit));
|
|
713
|
+
if (options?.since)
|
|
714
|
+
params.set("since", String(options.since));
|
|
715
|
+
if (options?.until)
|
|
716
|
+
params.set("until", String(options.until));
|
|
717
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
718
|
+
return this.transport.request(`/api/v1/market/candles/${encodeURIComponent(cleanSymbol)}${query}`, "GET", undefined, options);
|
|
719
|
+
}
|
|
720
|
+
async getOrderBook(symbol, options) {
|
|
721
|
+
if (!symbol || typeof symbol !== "string" || symbol.trim() === "") {
|
|
722
|
+
throw new ValidationError("Symbol must be a non-empty string.", "symbol");
|
|
723
|
+
}
|
|
724
|
+
const cleanSymbol = symbol.trim().toUpperCase();
|
|
725
|
+
return this.transport.request(`/api/v1/market/orderbook/${encodeURIComponent(cleanSymbol)}`, "GET", undefined, options);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/resources/news.ts
|
|
730
|
+
class NewsResource {
|
|
731
|
+
transport;
|
|
732
|
+
constructor(transport) {
|
|
733
|
+
this.transport = transport;
|
|
734
|
+
}
|
|
735
|
+
async getNews(options) {
|
|
736
|
+
const params = new URLSearchParams;
|
|
737
|
+
if (options?.symbols && options.symbols.length > 0) {
|
|
738
|
+
params.set("symbols", options.symbols.map((s) => s.trim().toUpperCase()).join(","));
|
|
739
|
+
}
|
|
740
|
+
if (options?.limit)
|
|
741
|
+
params.set("limit", String(options.limit));
|
|
742
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
743
|
+
return this.transport.request(`/api/v1/news${query}`, "GET", undefined, options);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/resources/social.ts
|
|
748
|
+
class SocialResource {
|
|
749
|
+
transport;
|
|
750
|
+
constructor(transport) {
|
|
751
|
+
this.transport = transport;
|
|
752
|
+
}
|
|
753
|
+
async getPosts(options) {
|
|
754
|
+
const params = new URLSearchParams;
|
|
755
|
+
if (options?.symbol)
|
|
756
|
+
params.set("symbol", options.symbol.trim().toUpperCase());
|
|
757
|
+
if (options?.limit)
|
|
758
|
+
params.set("limit", String(options.limit));
|
|
759
|
+
if (options?.cursor)
|
|
760
|
+
params.set("cursor", options.cursor);
|
|
761
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
762
|
+
return this.transport.request(`/api/v1/social/posts${query}`, "GET", undefined, options);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/resources/ws.ts
|
|
767
|
+
class WsResource {
|
|
768
|
+
transport;
|
|
769
|
+
constructor(transport) {
|
|
770
|
+
this.transport = transport;
|
|
771
|
+
}
|
|
772
|
+
async createTicket(options) {
|
|
773
|
+
return this.transport.request("/api/v1/ws/ticket", "POST", {}, options);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/client.ts
|
|
778
|
+
class PiaClient {
|
|
779
|
+
config;
|
|
780
|
+
transport;
|
|
781
|
+
market;
|
|
782
|
+
social;
|
|
783
|
+
news;
|
|
784
|
+
ws;
|
|
785
|
+
realtime;
|
|
786
|
+
constructor(options = {}) {
|
|
787
|
+
this.config = Object.freeze(resolveConfig(options));
|
|
788
|
+
this.transport = new HttpTransport(this.config);
|
|
789
|
+
this.market = new MarketResource(this.transport);
|
|
790
|
+
this.social = new SocialResource(this.transport);
|
|
791
|
+
this.news = new NewsResource(this.transport);
|
|
792
|
+
this.ws = new WsResource(this.transport);
|
|
793
|
+
this.realtime = new RealtimeClient(this.config);
|
|
794
|
+
}
|
|
795
|
+
getRateLimitInfo() {
|
|
796
|
+
return this.transport.getRateLimitInfo();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
export {
|
|
800
|
+
ApiError,
|
|
801
|
+
AuthenticationError,
|
|
802
|
+
ConfigurationError,
|
|
803
|
+
DEFAULT_BASE_URL,
|
|
804
|
+
DEFAULT_MAX_RETRIES,
|
|
805
|
+
DEFAULT_TIMEOUT_MS,
|
|
806
|
+
DEFAULT_WS_URL,
|
|
807
|
+
DefaultLogger,
|
|
808
|
+
NetworkError,
|
|
809
|
+
ParseError,
|
|
810
|
+
PermissionError,
|
|
811
|
+
PiaClient,
|
|
812
|
+
PiaError,
|
|
813
|
+
RateLimitError,
|
|
814
|
+
RealtimeClient,
|
|
815
|
+
TimeoutError,
|
|
816
|
+
ValidationError,
|
|
817
|
+
redactSensitive,
|
|
818
|
+
resolveConfig
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
//# debugId=2A73CEA3E1247E3664756E2164756E21
|
|
822
|
+
//# sourceMappingURL=index.js.map
|