@bizjaya/finclient 0.1.1 → 0.2.1

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.cjs CHANGED
@@ -25,7 +25,7 @@ __export(index_exports, {
25
25
  FinApiError: () => FinApiError,
26
26
  FinClient: () => FinClient,
27
27
  FinHttpTransport: () => FinHttpTransport,
28
- FinWebhook: () => FinWebhook,
28
+ FxResource: () => FxResource,
29
29
  OrdersResource: () => OrdersResource,
30
30
  WebhooksResource: () => WebhooksResource,
31
31
  readAllChanges: () => readAllChanges,
@@ -55,9 +55,6 @@ async function* readAllChanges(readPage, cursor, signal) {
55
55
  }
56
56
  }
57
57
 
58
- // src/http-transport.ts
59
- var import_node_crypto = require("crypto");
60
-
61
58
  // src/api-error.ts
62
59
  var FinApiError = class extends Error {
63
60
  /** The HTTP status code returned by the API, or 0 when no response arrived (timeout or network failure). */
@@ -81,20 +78,48 @@ var FinApiError = class extends Error {
81
78
  }
82
79
  };
83
80
 
81
+ // src/sse.ts
82
+ var FinSseParser = class {
83
+ eventName = null;
84
+ data = [];
85
+ /** Adds one line (without its line break). Returns the event the line completed, if any. */
86
+ add(line) {
87
+ if (!line) return this.flush();
88
+ if (line.startsWith(":")) return null;
89
+ const separator = line.indexOf(":");
90
+ const field = separator < 0 ? line : line.slice(0, separator);
91
+ const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
92
+ if (field === "event") this.eventName = value;
93
+ else if (field === "data") this.data.push(value);
94
+ return null;
95
+ }
96
+ /** Ends the stream: an event still being read is returned when it carries data. */
97
+ complete() {
98
+ return this.data.length ? this.flush() : null;
99
+ }
100
+ flush() {
101
+ const item = this.data.length ? { event: this.eventName ?? "message", data: this.data.join("\n") } : null;
102
+ this.eventName = null;
103
+ this.data = [];
104
+ return item;
105
+ }
106
+ };
107
+
84
108
  // src/http-transport.ts
85
109
  var retryableStatuses = /* @__PURE__ */ new Set([429, 502, 503, 504]);
86
110
  var writeMethods = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
87
111
  var FinHttpTransport = class {
88
112
  apiKey;
89
113
  baseUrl;
114
+ credentials;
90
115
  timeoutMs;
91
116
  maxRetries;
92
117
  fetcher;
93
118
  delay;
94
119
  random;
95
120
  constructor(options) {
96
- if (!options.apiKey?.trim()) throw new TypeError("apiKey is required.");
97
- this.apiKey = options.apiKey;
121
+ this.apiKey = options.apiKey?.trim() ?? "";
122
+ if (!this.apiKey && !options.fetch) throw new TypeError("apiKey is required unless a custom fetch authenticates the member session.");
98
123
  this.baseUrl = (options.baseUrl ?? "https://api.fincopy.io").replace(/\/+$/, "");
99
124
  const base = new URL(this.baseUrl);
100
125
  if (!/^https?:$/.test(base.protocol) || base.search || base.hash) {
@@ -105,6 +130,7 @@ var FinHttpTransport = class {
105
130
  if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) throw new RangeError("timeoutMs must be positive.");
106
131
  if (!Number.isInteger(this.maxRetries) || this.maxRetries < 0) throw new RangeError("maxRetries cannot be negative.");
107
132
  this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
133
+ this.credentials = options.credentials ?? (this.apiKey ? "same-origin" : "include");
108
134
  this.delay = options.retryDelay ?? ((milliseconds, signal) => new Promise((resolve, reject) => {
109
135
  if (signal?.aborted) {
110
136
  reject(signal.reason);
@@ -147,14 +173,16 @@ var FinHttpTransport = class {
147
173
  let response;
148
174
  let raw;
149
175
  try {
150
- const headers = new Headers({ "X-Api-Key": this.apiKey, Accept: "application/json" });
176
+ const headers = new Headers({ Accept: "application/json" });
177
+ if (this.apiKey) headers.set("X-Api-Key", this.apiKey);
151
178
  if (bodyText !== void 0) headers.set("Content-Type", "application/json");
152
179
  if (idempotencyKey !== void 0) headers.set("Idempotency-Key", idempotencyKey);
153
180
  response = await this.fetcher(url, {
154
181
  method: verb,
155
182
  headers,
156
183
  body: bodyText,
157
- signal: controller.signal
184
+ signal: controller.signal,
185
+ credentials: this.credentials
158
186
  });
159
187
  raw = await response.text();
160
188
  } catch (error) {
@@ -219,6 +247,79 @@ var FinHttpTransport = class {
219
247
  chksum: envelope.chksum ?? null
220
248
  };
221
249
  }
250
+ /** Reads the event stream until the server closes it or the caller aborts. It does not reconnect. */
251
+ async *requestEvents(path, requestOptions = {}) {
252
+ const url = new URL(path.replace(/^\//, ""), `${this.baseUrl}/`);
253
+ const headers = new Headers({ Accept: "text/event-stream" });
254
+ if (this.apiKey) headers.set("X-Api-Key", this.apiKey);
255
+ let response;
256
+ try {
257
+ response = await this.fetcher(url, {
258
+ method: "GET",
259
+ headers,
260
+ signal: requestOptions.signal,
261
+ credentials: this.credentials
262
+ });
263
+ } catch (error) {
264
+ if (requestOptions.signal?.aborted) throw error;
265
+ throw new FinApiError(0, "The FinCopy event stream could not be opened.", null, [], { errorCode: "network", cause: error });
266
+ }
267
+ if (!response.ok) {
268
+ const raw = await response.text();
269
+ let envelope = null;
270
+ try {
271
+ envelope = raw ? JSON.parse(raw) : null;
272
+ } catch {
273
+ }
274
+ const fallback = `The FinCopy API request failed with HTTP ${response.status}.`;
275
+ throw new FinApiError(
276
+ response.status,
277
+ envelope?.message || fallback,
278
+ envelope?.code ?? null,
279
+ errorsOf(envelope?.errors),
280
+ {
281
+ errorCode: response.status === 429 ? "rate_limited" : null,
282
+ retryAfterMs: retryAfterMsOf(response.headers.get("Retry-After"))
283
+ }
284
+ );
285
+ }
286
+ if (!response.body) throw new FinApiError(response.status, "The FinCopy API returned an empty event stream.", null, [], { errorCode: "empty_response" });
287
+ const reader = response.body.getReader();
288
+ const decoder = new TextDecoder();
289
+ const parser = new FinSseParser();
290
+ let buffer = "";
291
+ try {
292
+ while (true) {
293
+ if (requestOptions.signal?.aborted) throw requestOptions.signal.reason;
294
+ let next;
295
+ try {
296
+ next = await reader.read();
297
+ } catch (error) {
298
+ if (requestOptions.signal?.aborted) throw error;
299
+ throw new FinApiError(0, "The FinCopy event stream was interrupted.", null, [], { errorCode: "network", cause: error });
300
+ }
301
+ buffer += decoder.decode(next.value, { stream: !next.done });
302
+ const lines = buffer.split(/\r?\n/);
303
+ buffer = lines.pop() ?? "";
304
+ for (const line of lines) {
305
+ const item = parser.add(line);
306
+ if (item) yield item;
307
+ }
308
+ if (next.done) {
309
+ if (buffer) {
310
+ const item = parser.add(buffer);
311
+ if (item) yield item;
312
+ }
313
+ const final = parser.complete();
314
+ if (final) yield final;
315
+ return;
316
+ }
317
+ }
318
+ } finally {
319
+ await reader.cancel().catch(() => void 0);
320
+ reader.releaseLock();
321
+ }
322
+ }
222
323
  backoff(retry) {
223
324
  return Math.min(maxBackoffMs, 250 * 2 ** retry * (0.75 + this.random() * 0.5));
224
325
  }
@@ -246,128 +347,194 @@ function errorsOf(errors) {
246
347
  }
247
348
  function idempotencyKeyOf(supplied) {
248
349
  const key = supplied?.trim();
249
- if (!key) return (0, import_node_crypto.randomUUID)();
350
+ if (!key) return newIdempotencyKey();
250
351
  if (key.length > 120 || /[\u0000-\u001f\u007f]/.test(key)) {
251
352
  throw new TypeError("idempotencyKey must be 1-120 characters without control characters.");
252
353
  }
253
354
  return key;
254
355
  }
356
+ function newIdempotencyKey() {
357
+ const webCrypto = globalThis.crypto;
358
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
359
+ const bytes = new Uint8Array(16);
360
+ if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
361
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
362
+ bytes[6] = bytes[6] & 15 | 64;
363
+ bytes[8] = bytes[8] & 63 | 128;
364
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
365
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
366
+ }
255
367
 
256
368
  // src/resources.ts
257
369
  function pageQuery(query = {}) {
258
370
  const params = new URLSearchParams();
259
- const scalarKeys = ["PageNo", "PageSize", "PageOff", "IsAsc", "SortCol", "SortDir", "SrchCol", "SrchVal"];
260
- for (const key of scalarKeys) {
261
- const value2 = query[key];
262
- if (value2 !== void 0) params.append(key, String(value2));
371
+ for (const key of ["PageNo", "PageSize"]) {
372
+ const value = query[key];
373
+ if (value !== void 0) params.append(key, String(value));
263
374
  }
264
375
  if (query.Search !== void 0) params.append("search", query.Search);
265
- for (const key of ["SortBy", "SrchBy", "Include"]) {
266
- for (const value2 of query[key] ?? []) params.append(key, value2);
267
- }
268
- const value = params.toString();
269
- return value ? `?${value}` : "";
376
+ for (const key of ["SortBy", "SrchBy"])
377
+ (query[key] ?? []).forEach((clause, index) => clause.forEach((value, part) => params.append(`${key}[${index}][${part}]`, value)));
378
+ for (const value of query.Include ?? []) params.append("Include", value);
379
+ return params.toString();
380
+ }
381
+ function queryPath(path, page, filters = {}) {
382
+ const params = new URLSearchParams(pageQuery(page));
383
+ for (const [key, value] of Object.entries(filters)) if (value !== void 0 && value !== null) params.set(key, String(value));
384
+ const suffix = params.toString();
385
+ return suffix ? `${path}?${suffix}` : path;
270
386
  }
271
- function accountPath(accountId) {
272
- if (!accountId?.trim()) throw new TypeError("accountId is required.");
273
- return `v1/accounts/${encodeURIComponent(accountId)}`;
387
+ function accountPath(acno) {
388
+ if (!Number.isSafeInteger(acno) || acno <= 0) throw new RangeError("acno must be a positive safe integer.");
389
+ return `v1/fxs/accounts/${acno}`;
274
390
  }
275
- function numericId(value, name) {
276
- if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative safe integer.`);
391
+ function numericId(value, name, allowZero = true) {
392
+ if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) throw new RangeError(`${name} is out of range.`);
277
393
  return String(value);
278
394
  }
279
- function dateQuery(value) {
395
+ function dateValue(value) {
280
396
  return value instanceof Date ? value.toISOString() : value;
281
397
  }
398
+ function dateFilters(query) {
399
+ return {
400
+ fromDt: query.fromDt === void 0 ? void 0 : dateValue(query.fromDt),
401
+ toDt: query.toDt === void 0 ? void 0 : dateValue(query.toDt)
402
+ };
403
+ }
282
404
  var AccountsResource = class {
283
405
  constructor(http) {
284
406
  this.http = http;
285
407
  }
286
408
  http;
287
- /** Lists the accounts this API key can reach. */
288
- list(options = {}) {
289
- return this.http.request("GET", "v1/accounts", void 0, options);
409
+ /** Lists reachable accounts, optionally filtered by external reference. */
410
+ list(page = {}, externalRef, options = {}) {
411
+ return this.http.requestPage("GET", queryPath("v1/fxs/accounts", page, { externalRef }), options);
290
412
  }
291
- /** Adds a hosted account for the key's owner. */
413
+ /** Adds a hosted account. */
292
414
  make(command, options = {}) {
293
- return this.http.request("POST", "v1/accounts/make", command, options);
415
+ return this.http.request("POST", "v1/fxs/accounts", command, options);
416
+ }
417
+ /** Creates a demo account. */
418
+ makeDemo(command, options = {}) {
419
+ return this.http.request("POST", "v1/fxs/accounts/demo", command, options);
294
420
  }
295
- /** Reads one account and its latest state. */
296
- get(accountId, options = {}) {
297
- return this.http.request("GET", accountPath(accountId), void 0, options);
421
+ /** Reads demo availability for one platform. */
422
+ demoAvailability(platform, options = {}) {
423
+ return this.http.request("GET", queryPath("v1/fxs/accounts/demo-availability", void 0, { platform }), void 0, options);
424
+ }
425
+ /** Reads one account by ACNO. */
426
+ get(acno, options = {}) {
427
+ return this.http.request("GET", accountPath(acno), void 0, options);
298
428
  }
299
429
  /** Sets or clears the account's external reference. */
300
- setExternalRef(accountId, command, options = {}) {
301
- return this.http.request("PUT", `${accountPath(accountId)}/external-ref`, command, options);
430
+ setExternalRef(acno, command, options = {}) {
431
+ return this.http.request("PUT", `${accountPath(acno)}/external-ref`, command, options);
432
+ }
433
+ /** Reads account settings. */
434
+ getSettings(acno, options = {}) {
435
+ return this.http.request("GET", `${accountPath(acno)}/settings`, void 0, options);
436
+ }
437
+ /** Changes account settings. */
438
+ patchSettings(acno, command, options = {}) {
439
+ return this.http.request("PATCH", `${accountPath(acno)}/settings`, command, options);
440
+ }
441
+ /** Lists pairs for one account. */
442
+ pairs(acno, page = {}, search, options = {}) {
443
+ return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/pairs`, page, { search }), options);
444
+ }
445
+ /** Reads one pair by PRNO. */
446
+ pair(acno, prno, options = {}) {
447
+ return this.http.request("GET", `${accountPath(acno)}/pairs/${numericId(prno, "prno")}`, void 0, options);
302
448
  }
303
- /** Reads the account's trading settings. */
304
- getTrading(accountId, options = {}) {
305
- return this.http.request("GET", `${accountPath(accountId)}/fxs`, void 0, options);
449
+ /** Changes one pair's settings. */
450
+ patchPair(acno, prno, command, options = {}) {
451
+ return this.http.request("PATCH", `${accountPath(acno)}/pairs/${numericId(prno, "prno")}`, command, options);
306
452
  }
307
- /** Changes the account's trading settings. */
308
- patchTrading(accountId, command, options = {}) {
309
- return this.http.request("PATCH", `${accountPath(accountId)}/fxs`, command, options);
453
+ /** Reads the broker symbols and enabled list. */
454
+ symbols(acno, options = {}) {
455
+ return this.http.request("GET", `${accountPath(acno)}/symbols`, void 0, options);
310
456
  }
311
- /** Lists the account's symbols and their BUY and SELL books. */
312
- listPairs(accountId, query = {}, options = {}) {
313
- return this.http.requestPage("GET", `${accountPath(accountId)}/pairs${pageQuery(query)}`, options);
457
+ /** Adds or removes enabled symbols. */
458
+ editSymbols(acno, command, options = {}) {
459
+ return this.http.request("PUT", `${accountPath(acno)}/symbols`, command, options);
314
460
  }
315
- /** Reads one symbol and its settings. */
316
- getPair(accountId, prno, options = {}) {
317
- return this.http.request("GET", `${accountPath(accountId)}/pairs/${numericId(prno, "prno")}`, void 0, options);
461
+ /** Reads a symbol's broker specification and latest quote. */
462
+ quote(acno, symbol, options = {}) {
463
+ if (!symbol.trim()) throw new TypeError("symbol is required.");
464
+ return this.http.request("GET", `${accountPath(acno)}/quotes/${encodeURIComponent(symbol)}`, void 0, options);
465
+ }
466
+ /** Lists live orders for one account. */
467
+ orders(acno, page = {}, search, options = {}) {
468
+ return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/orders`, page, { search }), options);
469
+ }
470
+ /** Reads one live or settled order. */
471
+ order(acno, oid, options = {}) {
472
+ return this.http.request("GET", `${accountPath(acno)}/orders/${numericId(oid, "oid")}`, void 0, options);
318
473
  }
319
- /** Changes one symbol's settings. */
320
- patchPair(accountId, prno, command, options = {}) {
321
- return this.http.request("PATCH", `${accountPath(accountId)}/pairs/${numericId(prno, "prno")}`, command, options);
474
+ /** Lists settled orders. */
475
+ history(acno, page = {}, search, options = {}) {
476
+ return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/history`, page, { search }), options);
477
+ }
478
+ /** Reads one settled order by OID. */
479
+ historyItem(acno, oid, options = {}) {
480
+ return this.http.request("GET", `${accountPath(acno)}/history/${numericId(oid, "oid")}`, void 0, options);
322
481
  }
323
482
  /** Lists HYBRID DCA proposals waiting for an answer. */
324
- dcaQueue(accountId, options = {}) {
325
- return this.http.request("GET", `${accountPath(accountId)}/dca-queue`, void 0, options);
483
+ dcaQueue(acno, options = {}) {
484
+ return this.http.request("GET", `${accountPath(acno)}/dca`, void 0, options);
326
485
  }
327
- /** Reads broker symbols and the account's enabled symbols. */
328
- symbols(accountId, options = {}) {
329
- return this.http.request("GET", `${accountPath(accountId)}/symbols`, void 0, options);
486
+ /** Lists daily performance rows. */
487
+ performance(acno, query = {}) {
488
+ return this.http.request("GET", queryPath(`${accountPath(acno)}/performance`, void 0, dateFilters(query)), void 0, query);
330
489
  }
331
- /** Adds and removes symbols from the account's enabled list. */
332
- editSymbols(accountId, command, options = {}) {
333
- return this.http.request("PUT", `${accountPath(accountId)}/symbols`, command, options);
490
+ /** Reads the account's performance window. */
491
+ performanceWindow(acno, options = {}) {
492
+ return this.http.request("GET", `${accountPath(acno)}/performance/window`, void 0, options);
334
493
  }
335
- /** Reads a symbol's broker specification and latest quote. */
336
- quote(accountId, symbol, options = {}) {
337
- if (!symbol?.trim()) throw new TypeError("symbol is required.");
338
- return this.http.request("GET", `${accountPath(accountId)}/quotes/${encodeURIComponent(symbol)}`, void 0, options);
339
- }
340
- /** Reads daily performance rows for one account and optional UTC date range. */
341
- performance(accountId, query = {}) {
342
- const params = new URLSearchParams();
343
- if (query.fromDt !== void 0) params.set("fromDt", dateQuery(query.fromDt));
344
- if (query.toDt !== void 0) params.set("toDt", dateQuery(query.toDt));
345
- const encoded = params.toString();
346
- const suffix = encoded ? `?${encoded}` : "";
347
- return this.http.request("GET", `${accountPath(accountId)}/performance${suffix}`, void 0, query);
348
- }
349
- /** Reads recent activity for one account. */
350
- activity(accountId, options = {}) {
351
- return this.http.request("GET", `${accountPath(accountId)}/activity`, void 0, options);
352
- }
353
- /** Queues a start operation for the hosted terminal. */
354
- start(accountId, options = {}) {
355
- return this.operation(accountId, "start", options);
356
- }
357
- /** Queues a stop operation for the hosted terminal. */
358
- stop(accountId, options = {}) {
359
- return this.operation(accountId, "stop", options);
360
- }
361
- /** Queues a restart operation for the hosted terminal. */
362
- restart(accountId, options = {}) {
363
- return this.operation(accountId, "restart", options);
364
- }
365
- /** Queues removal of the hosted account and its terminal. */
366
- remove(accountId, options = {}) {
367
- return this.operation(accountId, "remove", options);
368
- }
369
- operation(accountId, operation, options) {
370
- return this.http.request("POST", `${accountPath(accountId)}/${operation}`, void 0, options);
494
+ /** Reads performance bars for an optional UTC date range. */
495
+ performanceBars(acno, query = {}) {
496
+ return this.http.request("GET", queryPath(`${accountPath(acno)}/performance/bars`, void 0, dateFilters(query)), void 0, query);
497
+ }
498
+ /** Lists paged account activity. */
499
+ activity(acno, page = {}, search, options = {}) {
500
+ return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/activity`, page, { search }), options);
501
+ }
502
+ /** Previews applying a strategy (the account owner's or a FinCopy preset): what each symbol would change. Nothing changes. */
503
+ previewStrategy(acno, command, options = {}) {
504
+ return this.http.request("POST", `${accountPath(acno)}/strategy/preview`, command, options);
505
+ }
506
+ /** Applies a strategy to the account and its symbols; a side holding orders keeps what it must keep. */
507
+ applyStrategy(acno, command, options = {}) {
508
+ return this.http.request("POST", `${accountPath(acno)}/strategy/apply`, command, options);
509
+ }
510
+ /** Lists recent terminal operations. */
511
+ operations(acno, page = {}, options = {}) {
512
+ return this.http.requestPage("GET", queryPath(`${accountPath(acno)}/operations`, page), options);
513
+ }
514
+ /** Queues a broker-password change. */
515
+ changePassword(acno, command, options = {}) {
516
+ return this.http.request("POST", `${accountPath(acno)}/password`, command, options);
517
+ }
518
+ /** Queues a terminal start. */
519
+ start(acno, options = {}) {
520
+ return this.operation(acno, "start", options);
521
+ }
522
+ /** Queues a terminal stop. */
523
+ stop(acno, options = {}) {
524
+ return this.operation(acno, "stop", options);
525
+ }
526
+ /** Queues a terminal restart. */
527
+ restart(acno, options = {}) {
528
+ return this.operation(acno, "restart", options);
529
+ }
530
+ remove(acno, commandOrOptions = {}, options = {}) {
531
+ const hasCommand = Object.prototype.hasOwnProperty.call(commandOrOptions, "TrPin");
532
+ const command = hasCommand ? commandOrOptions : { TrPin: null };
533
+ const requestOptions = hasCommand ? options : commandOrOptions;
534
+ return this.http.request("DELETE", accountPath(acno), command, requestOptions);
535
+ }
536
+ operation(acno, action, options) {
537
+ return this.http.request("POST", `${accountPath(acno)}/${action}`, void 0, options);
371
538
  }
372
539
  };
373
540
  var OrdersResource = class {
@@ -375,45 +542,110 @@ var OrdersResource = class {
375
542
  this.http = http;
376
543
  }
377
544
  http;
378
- /** Lists live, queued and pending orders. */
379
- list(accountId, query = {}, options = {}) {
380
- return this.http.requestPage("GET", `${accountPath(accountId)}/orders${pageQuery(query)}`, options);
545
+ /** Preflights a trade request. */
546
+ preInfo(acno, command, options = {}) {
547
+ return this.http.request("POST", `${accountPath(acno)}/trade/preinfo`, command, options);
381
548
  }
382
- /** Lists settled orders, newest first. */
383
- history(accountId, query = {}, options = {}) {
384
- return this.http.requestPage("GET", `${accountPath(accountId)}/orders/history${pageQuery(query)}`, options);
549
+ /** Sends one trade action. */
550
+ make(acno, command, options = {}) {
551
+ return this.http.request("POST", `${accountPath(acno)}/trade`, command, options);
385
552
  }
386
- /** Reads one settled order. */
387
- historyItem(accountId, oid, options = {}) {
388
- return this.http.request("GET", `${accountPath(accountId)}/orders/history/${numericId(oid, "oid")}`, void 0, options);
553
+ /** Sends paired BUY and SELL actions. */
554
+ hedge(acno, command, options = {}) {
555
+ return this.http.request("POST", `${accountPath(acno)}/trade/hedge`, command, options);
389
556
  }
390
- /** Reads one live or settled order. */
391
- get(accountId, oid, options = {}) {
392
- return this.http.request("GET", `${accountPath(accountId)}/orders/${numericId(oid, "oid")}`, void 0, options);
557
+ /** Modifies a trade or pending order. */
558
+ modify(acno, command, options = {}) {
559
+ return this.http.request("POST", `${accountPath(acno)}/trade/modify`, command, options);
393
560
  }
394
- /** Sends one FXS action. A successful response means queued for the terminal, not filled. */
395
- make(accountId, command, options = {}) {
396
- return this.http.request("POST", `${accountPath(accountId)}/orders`, command, options);
561
+ /** Cancels an order that has not reached the terminal. */
562
+ cancelWaiting(acno, command, options = {}) {
563
+ return this.http.request("POST", `${accountPath(acno)}/trade/cancel`, command, options);
397
564
  }
398
- /** Accepts or rejects one HYBRID DCA proposal. */
399
- answerDca(accountId, oid, command, options = {}) {
400
- return this.http.request("POST", `${accountPath(accountId)}/dca/${numericId(oid, "oid")}/answer`, command, options);
565
+ /** Accepts or rejects one DCA proposal. */
566
+ answerDca(acno, oid, command, options = {}) {
567
+ return this.http.request("POST", `${accountPath(acno)}/dca/${numericId(oid, "oid")}`, command, options);
401
568
  }
402
- /** Reads the quote, limits, books and settings used by the trade ticket. */
403
- preInfo(accountId, command, options = {}) {
404
- return this.http.request("POST", `${accountPath(accountId)}/orders/preinfo`, command, options);
569
+ };
570
+ var FxResource = class {
571
+ constructor(http) {
572
+ this.http = http;
405
573
  }
406
- /** Sends a BUY and SELL trade together. A successful response means queued, not filled. */
407
- hedge(accountId, command, options = {}) {
408
- return this.http.request("POST", `${accountPath(accountId)}/orders/hedge`, command, options);
574
+ http;
575
+ /** Lists pairs across reachable accounts. */
576
+ pairs(query = {}, options = {}) {
577
+ return this.http.requestPage("GET", listPath("v1/fxs/pairs", query), options);
578
+ }
579
+ /** Lists open orders across reachable accounts. */
580
+ orders(query = {}, options = {}) {
581
+ return this.http.requestPage("GET", listPath("v1/fxs/orders", query), options);
582
+ }
583
+ /** Lists settled orders across reachable accounts. */
584
+ history(query = {}, options = {}) {
585
+ return this.http.requestPage("GET", listPath("v1/fxs/history", query), options);
586
+ }
587
+ /** Lists activity across reachable accounts. */
588
+ activity(query = {}, options = {}) {
589
+ return this.http.requestPage("GET", listPath("v1/fxs/activity", query), options);
590
+ }
591
+ /** HYBRID DCA proposals waiting for an answer across reachable accounts, newest first. */
592
+ dca(acno, options = {}) {
593
+ return this.http.request("GET", queryPath("v1/fxs/dca", void 0, { acno }), void 0, options);
594
+ }
595
+ /** The newest terminal operations across reachable accounts. */
596
+ operations(page = {}, acno, options = {}) {
597
+ return this.http.requestPage("GET", queryPath("v1/fxs/operations", page, { acno }), options);
598
+ }
599
+ /** The last 30 days by currency book across reachable accounts. */
600
+ performanceWindow(query = {}) {
601
+ return this.http.request("GET", queryPath("v1/fxs/performance/window", void 0, { acno: query.acno, demo: query.demo }), void 0, query);
602
+ }
603
+ /** Daily performance bars across reachable accounts for an optional UTC date range. */
604
+ performanceBars(query = {}) {
605
+ return this.http.request("GET", queryPath(
606
+ "v1/fxs/performance/bars",
607
+ void 0,
608
+ { acno: query.acno, demo: query.demo, ...dateFilters(query) }
609
+ ), void 0, query);
610
+ }
611
+ /** Lists the strategies you can apply: your own and FinCopy's presets; with `acno`, that account owner's. */
612
+ strategies(acno, options = {}) {
613
+ return this.http.request("GET", queryPath("v1/fxs/strategies", void 0, { acno }), void 0, options);
614
+ }
615
+ /** Reads one strategy. */
616
+ strategy(isno, options = {}) {
617
+ return this.http.request("GET", strategyPath(isno), void 0, options);
618
+ }
619
+ /** Creates one of your own strategies. */
620
+ makeStrategy(command, options = {}) {
621
+ return this.http.request("POST", "v1/fxs/strategies", command, options);
622
+ }
623
+ /** Changes one of your own strategies; fields left unset keep their values. */
624
+ patchStrategy(isno, command, options = {}) {
625
+ return this.http.request("PATCH", strategyPath(isno), command, options);
626
+ }
627
+ /** Deletes one of your own strategies. */
628
+ deleteStrategy(isno, options = {}) {
629
+ return this.http.request("DELETE", strategyPath(isno), void 0, options);
630
+ }
631
+ /** Reads the signals shown for enabled symbols. */
632
+ signals(options = {}) {
633
+ return this.http.request("GET", "v1/fxs/signals", void 0, options);
634
+ }
635
+ /** Lists warning and error activity. */
636
+ alerts(page = {}, options = {}) {
637
+ return this.http.requestPage("GET", queryPath("v1/fxs/alerts", page), options);
409
638
  }
410
- /** Changes an order's TP field, SL field, expiry or pending price. */
411
- modify(accountId, command, options = {}) {
412
- return this.http.request("POST", `${accountPath(accountId)}/trades/modify`, command, options);
639
+ /**
640
+ * Opens the live event stream for reachable accounts. It does not reconnect: it ends when the server closes it (about
641
+ * every 30 minutes) or the connection breaks, so open it again in a loop, waiting a few seconds between attempts.
642
+ */
643
+ stream(options = {}) {
644
+ return this.http.requestEvents("v1/fxs/stream", options);
413
645
  }
414
- /** Cancels an order that has not yet been delivered to the terminal. */
415
- cancelWaiting(accountId, command, options = {}) {
416
- return this.http.request("POST", `${accountPath(accountId)}/trades/cancel-waiting`, command, options);
646
+ /** Searches broker servers available when adding an account. */
647
+ brokers(query, platform, demosOnly, options = {}) {
648
+ return this.http.request("GET", queryPath("v1/brokers", void 0, { Q: query, Platform: platform, DemosOnly: demosOnly }), void 0, options);
417
649
  }
418
650
  };
419
651
  var ChangesResource = class {
@@ -424,18 +656,13 @@ var ChangesResource = class {
424
656
  /** Reads one changes page using an optional cursor. */
425
657
  get(cursor, take = 500, options = {}) {
426
658
  if (!Number.isInteger(take) || take < 1 || take > 1e3) throw new RangeError("take must be between 1 and 1000.");
427
- const params = new URLSearchParams({ take: String(take) });
428
- if (cursor) params.set("cursor", cursor);
429
- return this.http.request("GET", `v1/accounts/changes?${params}`, void 0, options);
659
+ return this.http.request("GET", queryPath("v1/changes", void 0, { cursor, take }), void 0, options);
430
660
  }
431
- /**
432
- * Streams the available changes page by page, following each returned cursor. Save each page's `Cursor` after handling
433
- * its `Changes`, so a restart resumes from there.
434
- */
661
+ /** Reads changes page by page, following each cursor. */
435
662
  readPages(cursor, take = 500, signal) {
436
663
  return readChangePages((next, nextSignal) => this.page(next, take, nextSignal), cursor, signal);
437
664
  }
438
- /** Streams every currently available change, following each returned cursor. Use `readPages` to save the cursor. */
665
+ /** Streams all available changes. Use readPages to retain a resume cursor. */
439
666
  readAll(cursor, take = 500, signal) {
440
667
  return readAllChanges((next, nextSignal) => this.page(next, take, nextSignal), cursor, signal);
441
668
  }
@@ -451,21 +678,27 @@ var WebhooksResource = class {
451
678
  http;
452
679
  /** Reads this key's webhook configuration. */
453
680
  get(options = {}) {
454
- return this.http.request("GET", "v1/accounts/webhook", void 0, options);
681
+ return this.http.request("GET", "v1/webhook", void 0, options);
455
682
  }
456
- /** Sets the HTTPS destination and returns its signing secret once. */
683
+ /** Sets the webhook destination and returns its signing secret once. */
457
684
  set(command, options = {}) {
458
- return this.http.request("PUT", "v1/accounts/webhook", command, options);
685
+ return this.http.request("PUT", "v1/webhook", command, options);
459
686
  }
460
687
  /** Removes this key's webhook. */
461
688
  delete(options = {}) {
462
- return this.http.request("DELETE", "v1/accounts/webhook", void 0, options);
689
+ return this.http.request("DELETE", "v1/webhook", void 0, options);
463
690
  }
464
- /** Sends a signed test POST to this key's webhook. */
691
+ /** Sends a signed test delivery. */
465
692
  ping(options = {}) {
466
- return this.http.request("POST", "v1/accounts/webhook/ping", void 0, options);
693
+ return this.http.request("POST", "v1/webhook/ping", void 0, options);
467
694
  }
468
695
  };
696
+ function strategyPath(isno) {
697
+ return `v1/fxs/strategies/${numericId(isno, "isno")}`;
698
+ }
699
+ function listPath(path, query) {
700
+ return queryPath(path, query.page, { acno: query.acno, prno: query.prno, demo: query.demo, platform: query.platform, search: query.search });
701
+ }
469
702
 
470
703
  // src/client.ts
471
704
  var FinClient = class {
@@ -475,33 +708,12 @@ var FinClient = class {
475
708
  this.copy = {
476
709
  accounts: new AccountsResource(transport),
477
710
  orders: new OrdersResource(transport),
711
+ fx: new FxResource(transport),
478
712
  changes: new ChangesResource(transport),
479
713
  webhooks: new WebhooksResource(transport)
480
714
  };
481
715
  }
482
716
  };
483
-
484
- // src/webhook.ts
485
- var import_node_crypto2 = require("crypto");
486
- var FinWebhook = class {
487
- static allowedSkewSeconds = 300;
488
- /** Verify X-FinCopy-Signature over the timestamp and the exact raw request body. */
489
- static verify(secret, timestamp, body, signature) {
490
- if (!secret || !signature) return false;
491
- const timestampText = String(timestamp);
492
- if (!/^\d+$/.test(timestampText)) return false;
493
- const seconds = Number(timestampText);
494
- if (!Number.isSafeInteger(seconds)) return false;
495
- const now = Math.floor(Date.now() / 1e3);
496
- if (now - seconds > this.allowedSkewSeconds || seconds - now > this.allowedSkewSeconds) return false;
497
- const suppliedHex = signature.startsWith("sha256=") ? signature.slice(7) : signature;
498
- if (!/^[a-fA-F0-9]{64}$/.test(suppliedHex)) return false;
499
- const supplied = Buffer.from(suppliedHex, "hex");
500
- const signedBody = Buffer.isBuffer(body) ? body : Buffer.from(body);
501
- const expected = (0, import_node_crypto2.createHmac)("sha256", Buffer.from(secret, "utf8")).update(Buffer.from(`${timestampText}.`, "utf8")).update(signedBody).digest();
502
- return supplied.length === expected.length && (0, import_node_crypto2.timingSafeEqual)(supplied, expected);
503
- }
504
- };
505
717
  // Annotate the CommonJS export names for ESM import in node:
506
718
  0 && (module.exports = {
507
719
  AccountsResource,
@@ -509,7 +721,7 @@ var FinWebhook = class {
509
721
  FinApiError,
510
722
  FinClient,
511
723
  FinHttpTransport,
512
- FinWebhook,
724
+ FxResource,
513
725
  OrdersResource,
514
726
  WebhooksResource,
515
727
  readAllChanges,