@bizjaya/finclient 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BizJaya TechLab S/B
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # @bizjaya/finclient
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official TypeScript SDK for FinCopy's public `/v1` API. It supports Node.js 18 and later, with ESM and CommonJS builds.
4
+
5
+ ```sh
6
+ npm install @bizjaya/finclient
7
+ ```
8
+
9
+ ```ts
10
+ import { FinClient } from "@bizjaya/finclient";
11
+
12
+ const fin = new FinClient({ apiKey: process.env.FINCOPY_API_KEY! });
13
+ const accounts = await fin.copy.accounts.list();
14
+ for (const account of accounts) console.log(account.Fleet?.Account?.Id, account.Fleet?.Account?.Login);
15
+
16
+ // Read what changed since the saved cursor, one page at a time; save the cursor after each page.
17
+ let cursor: string | undefined = loadSavedCursor();
18
+ for await (const page of fin.copy.changes.readPages(cursor)) {
19
+ for (const change of page.Changes ?? []) console.log(change.Entity, change.AccountId, change.OID, change.Status);
20
+ if (page.Cursor) saveCursor(page.Cursor);
21
+ }
22
+ ```
23
+
24
+ `loadSavedCursor` and `saveCursor` stand for your own storage. The first call without a cursor starts at "now".
25
+
26
+ A failed call throws `FinApiError` with `statusCode`, FinCopy's `code`, `errors`, and, when the SDK set it, `errorCode`
27
+ (`timeout`, `network`, `rate_limited`, `invalid_response` or `empty_response`) and `retryAfterMs`. Calls are retried on
28
+ rate limits, gateway errors and network failures, honouring the API's `Retry-After`.
29
+
30
+ The client is server-side only. Do not put an API key in browser JavaScript, a mobile application, a public repository, or a
31
+ client-side environment variable. FinCopy's envelope is handled by the client; DTO fields keep the API's PascalCase wire names.
32
+ Writes automatically carry an `Idempotency-Key`, which can be supplied explicitly in the request options when a caller needs
33
+ to retry the same operation later. Orders complete asynchronously; use the change feed or webhook to learn their result.
34
+
35
+ The API key grants the permissions assigned to that key. Store it as a server secret and give it only the permissions the
36
+ application needs.
37
+
38
+ See [FinCopy API docs](https://fincopy.io/docs) and the [FinClient source](https://github.com/bizjaya/FinClient). The
39
+ [QuickStart](./examples/QuickStart.ts) example lists accounts and reads the change feed; its order call is commented out
40
+ because it places a trade.
41
+
42
+ ## Build from source
43
+
44
+ ```sh
45
+ npm ci
46
+ npm run generate:types
47
+ npm run typecheck
48
+ npm test
49
+ npm run build
50
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,517 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AccountsResource: () => AccountsResource,
24
+ ChangesResource: () => ChangesResource,
25
+ FinApiError: () => FinApiError,
26
+ FinClient: () => FinClient,
27
+ FinHttpTransport: () => FinHttpTransport,
28
+ FinWebhook: () => FinWebhook,
29
+ OrdersResource: () => OrdersResource,
30
+ WebhooksResource: () => WebhooksResource,
31
+ readAllChanges: () => readAllChanges,
32
+ readChangePages: () => readChangePages
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/change-feed.ts
37
+ async function* readChangePages(readPage, cursor, signal) {
38
+ const seen = /* @__PURE__ */ new Set();
39
+ if (cursor) seen.add(cursor);
40
+ for (; ; ) {
41
+ if (signal?.aborted) throw signal.reason;
42
+ const page = await readPage(cursor, signal);
43
+ yield page;
44
+ if (!page.More) return;
45
+ if (!page.Cursor || page.Cursor === cursor || seen.has(page.Cursor)) {
46
+ throw new Error("The FinCopy change feed returned More without a new cursor.");
47
+ }
48
+ seen.add(page.Cursor);
49
+ cursor = page.Cursor;
50
+ }
51
+ }
52
+ async function* readAllChanges(readPage, cursor, signal) {
53
+ for await (const page of readChangePages(readPage, cursor, signal)) {
54
+ for (const change of page.Changes ?? []) yield change;
55
+ }
56
+ }
57
+
58
+ // src/http-transport.ts
59
+ var import_node_crypto = require("crypto");
60
+
61
+ // src/api-error.ts
62
+ var FinApiError = class extends Error {
63
+ /** The HTTP status code returned by the API, or 0 when no response arrived (timeout or network failure). */
64
+ statusCode;
65
+ /** The FinCopy response code, when present. */
66
+ code;
67
+ /** Validation and service errors returned by the API. */
68
+ errors;
69
+ /** Set by the SDK for timeouts, network failures, rate limits and malformed responses; null for other API errors. */
70
+ errorCode;
71
+ /** How long the API asked the caller to wait (the Retry-After header), in milliseconds, when it said. */
72
+ retryAfterMs;
73
+ constructor(statusCode, message, code = null, errors = [], details = {}) {
74
+ super(message || "The FinCopy API request failed.", details.cause === void 0 ? void 0 : { cause: details.cause });
75
+ this.name = "FinApiError";
76
+ this.statusCode = statusCode;
77
+ this.code = code;
78
+ this.errors = errors;
79
+ this.errorCode = details.errorCode ?? null;
80
+ this.retryAfterMs = details.retryAfterMs ?? null;
81
+ }
82
+ };
83
+
84
+ // src/http-transport.ts
85
+ var retryableStatuses = /* @__PURE__ */ new Set([429, 502, 503, 504]);
86
+ var writeMethods = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
87
+ var FinHttpTransport = class {
88
+ apiKey;
89
+ baseUrl;
90
+ timeoutMs;
91
+ maxRetries;
92
+ fetcher;
93
+ delay;
94
+ random;
95
+ constructor(options) {
96
+ if (!options.apiKey?.trim()) throw new TypeError("apiKey is required.");
97
+ this.apiKey = options.apiKey;
98
+ this.baseUrl = (options.baseUrl ?? "https://api.fincopy.io").replace(/\/+$/, "");
99
+ const base = new URL(this.baseUrl);
100
+ if (!/^https?:$/.test(base.protocol) || base.search || base.hash) {
101
+ throw new TypeError("baseUrl must be an absolute HTTP or HTTPS URL without a query or fragment.");
102
+ }
103
+ this.timeoutMs = options.timeoutMs ?? 3e4;
104
+ this.maxRetries = options.maxRetries ?? 3;
105
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) throw new RangeError("timeoutMs must be positive.");
106
+ if (!Number.isInteger(this.maxRetries) || this.maxRetries < 0) throw new RangeError("maxRetries cannot be negative.");
107
+ this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
108
+ this.delay = options.retryDelay ?? ((milliseconds, signal) => new Promise((resolve, reject) => {
109
+ if (signal?.aborted) {
110
+ reject(signal.reason);
111
+ return;
112
+ }
113
+ const onAbort = () => {
114
+ clearTimeout(timer);
115
+ reject(signal?.reason);
116
+ };
117
+ const timer = setTimeout(() => {
118
+ signal?.removeEventListener("abort", onAbort);
119
+ resolve();
120
+ }, milliseconds);
121
+ signal?.addEventListener("abort", onAbort, { once: true });
122
+ if (signal?.aborted) onAbort();
123
+ }));
124
+ this.random = options.random ?? Math.random;
125
+ }
126
+ async request(method, path, body, requestOptions = {}) {
127
+ const envelope = await this.sendEnvelope(method, path, body, requestOptions);
128
+ return envelope.data;
129
+ }
130
+ async sendEnvelope(method, path, body, requestOptions = {}) {
131
+ const verb = method.toUpperCase();
132
+ const writes = writeMethods.has(verb);
133
+ const bodyText = body === void 0 ? void 0 : JSON.stringify(body, (_key, value) => value === null ? void 0 : value);
134
+ const idempotencyKey = writes ? idempotencyKeyOf(requestOptions.idempotencyKey) : void 0;
135
+ const url = new URL(path.replace(/^\//, ""), `${this.baseUrl}/`);
136
+ for (let retry = 0; ; retry++) {
137
+ if (requestOptions.signal?.aborted) throw requestOptions.signal.reason;
138
+ const controller = new AbortController();
139
+ const onAbort = () => controller.abort(requestOptions.signal?.reason);
140
+ requestOptions.signal?.addEventListener("abort", onAbort, { once: true });
141
+ if (requestOptions.signal?.aborted) onAbort();
142
+ let timedOut = false;
143
+ const timer = setTimeout(() => {
144
+ timedOut = true;
145
+ controller.abort(new Error("FinCopy request timed out."));
146
+ }, this.timeoutMs);
147
+ let response;
148
+ let raw;
149
+ try {
150
+ const headers = new Headers({ "X-Api-Key": this.apiKey, Accept: "application/json" });
151
+ if (bodyText !== void 0) headers.set("Content-Type", "application/json");
152
+ if (idempotencyKey !== void 0) headers.set("Idempotency-Key", idempotencyKey);
153
+ response = await this.fetcher(url, {
154
+ method: verb,
155
+ headers,
156
+ body: bodyText,
157
+ signal: controller.signal
158
+ });
159
+ raw = await response.text();
160
+ } catch (error) {
161
+ if (requestOptions.signal?.aborted) throw error;
162
+ if (retry >= this.maxRetries) {
163
+ throw timedOut ? new FinApiError(0, "The FinCopy API did not answer in time.", null, [], { errorCode: "timeout", cause: error }) : new FinApiError(0, "The FinCopy API could not be reached.", null, [], { errorCode: "network", cause: error });
164
+ }
165
+ await this.pause(this.backoff(retry), requestOptions.signal);
166
+ continue;
167
+ } finally {
168
+ clearTimeout(timer);
169
+ requestOptions.signal?.removeEventListener("abort", onAbort);
170
+ }
171
+ const serverWait = retryAfterMsOf(response.headers.get("Retry-After"));
172
+ if (retryableStatuses.has(response.status)) {
173
+ if (retry < this.maxRetries && (serverWait === null || serverWait <= maxServerRetryAfterMs)) {
174
+ await this.pause(serverWait ?? (response.status === 429 ? rateLimitBackoff(retry) : this.backoff(retry)), requestOptions.signal);
175
+ continue;
176
+ }
177
+ }
178
+ let envelope = null;
179
+ try {
180
+ envelope = raw ? JSON.parse(raw) : null;
181
+ } catch {
182
+ }
183
+ if (!response.ok || !envelope || envelope.isOk !== true) {
184
+ const fallback = `The FinCopy API request failed with HTTP ${response.status}.`;
185
+ const message = typeof envelope?.message === "string" && envelope.message ? envelope.message : fallback;
186
+ const errorCode = response.status === 429 ? "rate_limited" : response.ok ? "invalid_response" : null;
187
+ throw new FinApiError(
188
+ response.status,
189
+ message,
190
+ envelope?.code ?? null,
191
+ errorsOf(envelope?.errors),
192
+ { errorCode, retryAfterMs: serverWait }
193
+ );
194
+ }
195
+ if (!Object.prototype.hasOwnProperty.call(envelope, "data") || envelope.data === null) {
196
+ throw new FinApiError(
197
+ response.status,
198
+ "The FinCopy API returned an empty response.",
199
+ envelope.code ?? null,
200
+ errorsOf(envelope.errors),
201
+ { errorCode: "empty_response" }
202
+ );
203
+ }
204
+ return envelope;
205
+ }
206
+ }
207
+ async requestPage(method, path, requestOptions = {}) {
208
+ const envelope = await this.sendEnvelope(method, path, void 0, requestOptions);
209
+ if (!Array.isArray(envelope.data)) {
210
+ throw new FinApiError(200, "The FinCopy API returned an invalid page.", envelope.code ?? null, envelope.errors ?? []);
211
+ }
212
+ return {
213
+ items: envelope.data,
214
+ paging: envelope.paging ?? null,
215
+ code: envelope.code ?? 200,
216
+ isOk: true,
217
+ message: envelope.message ?? null,
218
+ errors: envelope.errors ?? [],
219
+ chksum: envelope.chksum ?? null
220
+ };
221
+ }
222
+ backoff(retry) {
223
+ return Math.min(maxBackoffMs, 250 * 2 ** retry * (0.75 + this.random() * 0.5));
224
+ }
225
+ async pause(milliseconds, signal) {
226
+ if (signal?.aborted) throw signal.reason;
227
+ if (milliseconds > 0) await this.delay(milliseconds, signal);
228
+ }
229
+ };
230
+ var maxServerRetryAfterMs = 6e4;
231
+ var maxBackoffMs = 3e4;
232
+ function rateLimitBackoff(retry) {
233
+ return Math.min(maxBackoffMs, Math.max(1e3, 1e3 * 2 ** retry));
234
+ }
235
+ function retryAfterMsOf(header) {
236
+ const value = header?.trim();
237
+ if (!value) return null;
238
+ if (/^\d+$/.test(value)) return Number(value) * 1e3;
239
+ const at = Date.parse(value);
240
+ if (Number.isNaN(at)) return null;
241
+ const wait = at - Date.now();
242
+ return wait > 0 ? wait : null;
243
+ }
244
+ function errorsOf(errors) {
245
+ return Array.isArray(errors) ? errors.map((error) => typeof error === "string" ? error : JSON.stringify(error)) : [];
246
+ }
247
+ function idempotencyKeyOf(supplied) {
248
+ const key = supplied?.trim();
249
+ if (!key) return (0, import_node_crypto.randomUUID)();
250
+ if (key.length > 120 || /[\u0000-\u001f\u007f]/.test(key)) {
251
+ throw new TypeError("idempotencyKey must be 1-120 characters without control characters.");
252
+ }
253
+ return key;
254
+ }
255
+
256
+ // src/resources.ts
257
+ function pageQuery(query = {}) {
258
+ 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));
263
+ }
264
+ 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}` : "";
270
+ }
271
+ function accountPath(accountId) {
272
+ if (!accountId?.trim()) throw new TypeError("accountId is required.");
273
+ return `v1/accounts/${encodeURIComponent(accountId)}`;
274
+ }
275
+ function numericId(value, name) {
276
+ if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative safe integer.`);
277
+ return String(value);
278
+ }
279
+ function dateQuery(value) {
280
+ return value instanceof Date ? value.toISOString() : value;
281
+ }
282
+ var AccountsResource = class {
283
+ constructor(http) {
284
+ this.http = http;
285
+ }
286
+ http;
287
+ /** Lists the accounts this API key can reach. */
288
+ list(options = {}) {
289
+ return this.http.request("GET", "v1/accounts", void 0, options);
290
+ }
291
+ /** Adds a hosted account for the key's owner. */
292
+ make(command, options = {}) {
293
+ return this.http.request("POST", "v1/accounts/make", command, options);
294
+ }
295
+ /** Reads one account and its latest state. */
296
+ get(accountId, options = {}) {
297
+ return this.http.request("GET", accountPath(accountId), void 0, options);
298
+ }
299
+ /** 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);
302
+ }
303
+ /** Reads the account's trading settings. */
304
+ getTrading(accountId, options = {}) {
305
+ return this.http.request("GET", `${accountPath(accountId)}/fxs`, void 0, options);
306
+ }
307
+ /** Changes the account's trading settings. */
308
+ patchTrading(accountId, command, options = {}) {
309
+ return this.http.request("PATCH", `${accountPath(accountId)}/fxs`, command, options);
310
+ }
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);
314
+ }
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);
318
+ }
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);
322
+ }
323
+ /** 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);
326
+ }
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);
330
+ }
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);
334
+ }
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);
371
+ }
372
+ };
373
+ var OrdersResource = class {
374
+ constructor(http) {
375
+ this.http = http;
376
+ }
377
+ 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);
381
+ }
382
+ /** Lists settled orders, newest first. */
383
+ history(accountId, query = {}, options = {}) {
384
+ return this.http.requestPage("GET", `${accountPath(accountId)}/orders/history${pageQuery(query)}`, options);
385
+ }
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);
389
+ }
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);
393
+ }
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);
397
+ }
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);
401
+ }
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);
405
+ }
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);
409
+ }
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);
413
+ }
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);
417
+ }
418
+ };
419
+ var ChangesResource = class {
420
+ constructor(http) {
421
+ this.http = http;
422
+ }
423
+ http;
424
+ /** Reads one changes page using an optional cursor. */
425
+ get(cursor, take = 500, options = {}) {
426
+ 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);
430
+ }
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
+ */
435
+ readPages(cursor, take = 500, signal) {
436
+ return readChangePages((next, nextSignal) => this.page(next, take, nextSignal), cursor, signal);
437
+ }
438
+ /** Streams every currently available change, following each returned cursor. Use `readPages` to save the cursor. */
439
+ readAll(cursor, take = 500, signal) {
440
+ return readAllChanges((next, nextSignal) => this.page(next, take, nextSignal), cursor, signal);
441
+ }
442
+ async page(cursor, take, signal) {
443
+ const page = await this.get(cursor, take, { signal });
444
+ return { Changes: page.Changes ?? [], Cursor: page.Cursor ?? null, More: page.More ?? false };
445
+ }
446
+ };
447
+ var WebhooksResource = class {
448
+ constructor(http) {
449
+ this.http = http;
450
+ }
451
+ http;
452
+ /** Reads this key's webhook configuration. */
453
+ get(options = {}) {
454
+ return this.http.request("GET", "v1/accounts/webhook", void 0, options);
455
+ }
456
+ /** Sets the HTTPS destination and returns its signing secret once. */
457
+ set(command, options = {}) {
458
+ return this.http.request("PUT", "v1/accounts/webhook", command, options);
459
+ }
460
+ /** Removes this key's webhook. */
461
+ delete(options = {}) {
462
+ return this.http.request("DELETE", "v1/accounts/webhook", void 0, options);
463
+ }
464
+ /** Sends a signed test POST to this key's webhook. */
465
+ ping(options = {}) {
466
+ return this.http.request("POST", "v1/accounts/webhook/ping", void 0, options);
467
+ }
468
+ };
469
+
470
+ // src/client.ts
471
+ var FinClient = class {
472
+ copy;
473
+ constructor(options) {
474
+ const transport = new FinHttpTransport(options);
475
+ this.copy = {
476
+ accounts: new AccountsResource(transport),
477
+ orders: new OrdersResource(transport),
478
+ changes: new ChangesResource(transport),
479
+ webhooks: new WebhooksResource(transport)
480
+ };
481
+ }
482
+ };
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
+ // Annotate the CommonJS export names for ESM import in node:
506
+ 0 && (module.exports = {
507
+ AccountsResource,
508
+ ChangesResource,
509
+ FinApiError,
510
+ FinClient,
511
+ FinHttpTransport,
512
+ FinWebhook,
513
+ OrdersResource,
514
+ WebhooksResource,
515
+ readAllChanges,
516
+ readChangePages
517
+ });