@chadwin/sdk 0.1.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/dist/index.js ADDED
@@ -0,0 +1,701 @@
1
+ // src/error.ts
2
+ var APIError = class extends Error {
3
+ status;
4
+ code;
5
+ headers;
6
+ constructor(message, options = {}) {
7
+ super(message);
8
+ this.name = "APIError";
9
+ this.status = options.status;
10
+ this.code = options.code;
11
+ this.headers = options.headers;
12
+ }
13
+ };
14
+
15
+ // src/promise.ts
16
+ function createPromiseWithResponse(response) {
17
+ const promise = response.then(({ data }) => data);
18
+ Object.defineProperty(promise, "withResponse", {
19
+ value: () => response
20
+ });
21
+ void promise.catch(() => void 0);
22
+ return promise;
23
+ }
24
+
25
+ // src/internal/transport.ts
26
+ var ATTEMPT_TIMEOUT_MS = 3e4;
27
+ var MAX_RETRIES = 2;
28
+ var BASE_RETRY_DELAY_MS = 250;
29
+ var MAX_RETRY_DELAY_MS = 3e4;
30
+ var PRODUCTION_BASE_URL = "https://api.chadwin.co";
31
+ var INVALID_BASE_URL_MESSAGE = "baseURL must be an HTTP or HTTPS URL without credentials, a query, or a fragment";
32
+ var SAFE_ERROR_CODE = /^[a-z][a-z0-9_]{0,63}$/;
33
+ var AttemptFailure = class extends Error {
34
+ constructor(timedOut) {
35
+ super(timedOut ? "Request timed out" : "Network request failed");
36
+ this.timedOut = timedOut;
37
+ }
38
+ timedOut;
39
+ };
40
+ var HttpTransport = class {
41
+ #apiKey;
42
+ #fetch;
43
+ #baseURL;
44
+ constructor(options) {
45
+ this.#apiKey = options.apiKey;
46
+ this.#baseURL = normalizeBaseURL(options.baseURL ?? PRODUCTION_BASE_URL);
47
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
48
+ }
49
+ request(request) {
50
+ return createPromiseWithResponse(this.#performRequest(request));
51
+ }
52
+ async #performRequest(request) {
53
+ const url = buildUrl(this.#baseURL, request);
54
+ let lastFailureWasTimeout = false;
55
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
56
+ let result;
57
+ try {
58
+ result = await this.#attempt(url, request.responseType ?? "json");
59
+ } catch (error) {
60
+ if (error instanceof APIError) {
61
+ throw error;
62
+ }
63
+ if (!(error instanceof AttemptFailure)) {
64
+ throw new APIError("API request failed");
65
+ }
66
+ lastFailureWasTimeout = error.timedOut;
67
+ if (attempt === MAX_RETRIES) {
68
+ throw new APIError(
69
+ lastFailureWasTimeout ? "API request timed out" : "API request failed"
70
+ );
71
+ }
72
+ await wait(backoffDelay(attempt));
73
+ continue;
74
+ }
75
+ if ("data" in result) {
76
+ return result;
77
+ }
78
+ if (shouldRetry(result.response.status, result.code) && attempt < MAX_RETRIES) {
79
+ await wait(retryDelay(result.response, attempt));
80
+ continue;
81
+ }
82
+ throw errorFromResponse(result.response, result.code, this.#apiKey);
83
+ }
84
+ throw new APIError(
85
+ lastFailureWasTimeout ? "API request timed out" : "API request failed"
86
+ );
87
+ }
88
+ async #attempt(url, responseType) {
89
+ const controller = new AbortController();
90
+ let timedOut = false;
91
+ const timeout = setTimeout(() => {
92
+ timedOut = true;
93
+ controller.abort();
94
+ }, ATTEMPT_TIMEOUT_MS);
95
+ try {
96
+ let response;
97
+ try {
98
+ response = await this.#fetch(url, {
99
+ headers: {
100
+ authorization: `Bearer ${this.#apiKey}`
101
+ },
102
+ method: "GET",
103
+ signal: controller.signal
104
+ });
105
+ } catch {
106
+ throw new AttemptFailure(timedOut);
107
+ }
108
+ if (!response.ok) {
109
+ return {
110
+ code: await readErrorCode(response, this.#apiKey),
111
+ response
112
+ };
113
+ }
114
+ try {
115
+ const data = responseType === "text" ? await response.text() : await response.json();
116
+ return { data, response };
117
+ } catch (error) {
118
+ if (responseType === "json" && error instanceof SyntaxError) {
119
+ throw new APIError("API returned an invalid JSON response", {
120
+ headers: safeErrorHeaders(response.headers, this.#apiKey),
121
+ status: response.status
122
+ });
123
+ }
124
+ throw new APIError("API returned an invalid response body", {
125
+ headers: safeErrorHeaders(response.headers, this.#apiKey),
126
+ status: response.status
127
+ });
128
+ }
129
+ } finally {
130
+ clearTimeout(timeout);
131
+ }
132
+ }
133
+ };
134
+ function normalizeBaseURL(value) {
135
+ let url;
136
+ try {
137
+ url = new URL(value);
138
+ } catch {
139
+ throw new TypeError(INVALID_BASE_URL_MESSAGE);
140
+ }
141
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.href !== `${url.origin}${url.pathname}`) {
142
+ throw new TypeError(INVALID_BASE_URL_MESSAGE);
143
+ }
144
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/`;
145
+ return url;
146
+ }
147
+ function buildUrl(baseURL, request) {
148
+ const path = request.path.map((segment) => encodeURIComponent(segment)).join("/");
149
+ const url = new URL(path, baseURL);
150
+ for (const [key, value] of Object.entries(request.query ?? {})) {
151
+ if (value === void 0 || value === null) continue;
152
+ const values = Array.isArray(value) ? value : [value];
153
+ for (const entry of values) {
154
+ url.searchParams.append(key, String(entry));
155
+ }
156
+ }
157
+ return url;
158
+ }
159
+ async function readErrorCode(response, apiKey) {
160
+ try {
161
+ const body = await response.json();
162
+ if (body !== null && typeof body === "object" && "error" in body && typeof body.error === "string" && body.error !== apiKey && SAFE_ERROR_CODE.test(body.error)) {
163
+ return body.error;
164
+ }
165
+ } catch {
166
+ }
167
+ return void 0;
168
+ }
169
+ function shouldRetry(status, code) {
170
+ if (code === "billing_period_quota_exceeded") return false;
171
+ return code === "rate_limited" || status === 502 || status === 503 || status === 504;
172
+ }
173
+ function retryDelay(response, retry) {
174
+ const retryAfter = response.headers.get("retry-after");
175
+ if (retryAfter === null) return backoffDelay(retry);
176
+ const seconds = Number(retryAfter);
177
+ if (Number.isFinite(seconds) && seconds >= 0) {
178
+ return Math.min(seconds * 1e3, MAX_RETRY_DELAY_MS);
179
+ }
180
+ const date = Date.parse(retryAfter);
181
+ if (!Number.isNaN(date)) {
182
+ return Math.min(Math.max(0, date - Date.now()), MAX_RETRY_DELAY_MS);
183
+ }
184
+ return backoffDelay(retry);
185
+ }
186
+ function backoffDelay(retry) {
187
+ return Math.min(BASE_RETRY_DELAY_MS * 2 ** retry, MAX_RETRY_DELAY_MS);
188
+ }
189
+ function wait(milliseconds) {
190
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
191
+ }
192
+ function errorFromResponse(response, code, apiKey) {
193
+ const suffix = code ? ` (${code})` : "";
194
+ return new APIError(
195
+ `API request failed with status ${response.status}${suffix}`,
196
+ {
197
+ ...code === void 0 ? {} : { code },
198
+ headers: safeErrorHeaders(response.headers, apiKey),
199
+ status: response.status
200
+ }
201
+ );
202
+ }
203
+ function safeErrorHeaders(headers, apiKey) {
204
+ const safe = new Headers();
205
+ for (const [name, value] of headers) {
206
+ const normalizedName = name.toLowerCase();
207
+ if (normalizedName === "authorization" || normalizedName === "cookie" || normalizedName === "proxy-authorization" || normalizedName === "set-cookie" || value.includes(apiKey)) {
208
+ continue;
209
+ }
210
+ safe.append(name, value);
211
+ }
212
+ return safe;
213
+ }
214
+
215
+ // src/resources/feeds.ts
216
+ var Feeds = class {
217
+ companyReports;
218
+ insiderActivity;
219
+ insiderTransactions;
220
+ institutionalFilings;
221
+ constructor(transport) {
222
+ this.companyReports = new CompanyReportFeed(transport);
223
+ this.insiderActivity = new InsiderActivityFeed(transport);
224
+ this.insiderTransactions = new InsiderTransactionFeed(transport);
225
+ this.institutionalFilings = new InstitutionalFilingFeed(transport);
226
+ }
227
+ };
228
+ var CompanyReportFeed = class {
229
+ constructor(transport) {
230
+ this.transport = transport;
231
+ }
232
+ transport;
233
+ listLatest(filters = {}) {
234
+ return this.#request(filters, {});
235
+ }
236
+ listAfter({
237
+ cursor,
238
+ ...filters
239
+ }) {
240
+ return this.#request(filters, { cursor });
241
+ }
242
+ startFromNow(filters = {}) {
243
+ return this.#request(filters, { start: "now" });
244
+ }
245
+ #request(filters, mode) {
246
+ return this.transport.request({
247
+ path: ["v1", "company-reports", "feed"],
248
+ query: {
249
+ ...mode,
250
+ forms: filters.forms?.join(","),
251
+ limit: filters.limit
252
+ }
253
+ });
254
+ }
255
+ };
256
+ var InsiderActivityFeed = class {
257
+ constructor(transport) {
258
+ this.transport = transport;
259
+ }
260
+ transport;
261
+ listLatest(filters = {}) {
262
+ return this.#request(filters, {});
263
+ }
264
+ listAfter({
265
+ cursor,
266
+ ...filters
267
+ }) {
268
+ return this.#request(filters, { cursor });
269
+ }
270
+ startFromNow(filters = {}) {
271
+ return this.#request(filters, { start: "now" });
272
+ }
273
+ #request(filters, mode) {
274
+ return this.transport.request({
275
+ path: ["v1", "insider-activity", "feed"],
276
+ query: {
277
+ ...mode,
278
+ forms: filters.forms?.join(","),
279
+ limit: filters.limit
280
+ }
281
+ });
282
+ }
283
+ };
284
+ var InsiderTransactionFeed = class {
285
+ constructor(transport) {
286
+ this.transport = transport;
287
+ }
288
+ transport;
289
+ listLatest(filters = {}) {
290
+ return this.#request(filters, {});
291
+ }
292
+ listAfter({
293
+ cursor,
294
+ ...filters
295
+ }) {
296
+ return this.#request(filters, { cursor });
297
+ }
298
+ startFromNow(filters = {}) {
299
+ return this.#request(filters, { start: "now" });
300
+ }
301
+ #request(filters, mode) {
302
+ return this.transport.request({
303
+ path: ["v1", "insider-transactions", "feed"],
304
+ query: {
305
+ ...mode,
306
+ limit: filters.limit,
307
+ transaction_codes: filters.transactionCodes?.join(",")
308
+ }
309
+ });
310
+ }
311
+ };
312
+ var InstitutionalFilingFeed = class {
313
+ constructor(transport) {
314
+ this.transport = transport;
315
+ }
316
+ transport;
317
+ listLatest(filters = {}) {
318
+ return this.#request(filters, {});
319
+ }
320
+ listAfter({
321
+ cursor,
322
+ ...filters
323
+ }) {
324
+ return this.#request(filters, { cursor });
325
+ }
326
+ startFromNow(filters = {}) {
327
+ return this.#request(filters, { start: "now" });
328
+ }
329
+ #request(filters, mode) {
330
+ return this.transport.request({
331
+ path: ["v1", "institutional-holdings", "feed"],
332
+ query: {
333
+ ...mode,
334
+ forms: filters.forms?.join(","),
335
+ limit: filters.limit
336
+ }
337
+ });
338
+ }
339
+ };
340
+
341
+ // src/resources/products.ts
342
+ var Companies = class {
343
+ constructor(transport) {
344
+ this.transport = transport;
345
+ }
346
+ transport;
347
+ search({ query }) {
348
+ return this.transport.request({
349
+ path: ["v1", "companies", "US", "search"],
350
+ query: { query }
351
+ });
352
+ }
353
+ get({ ticker }) {
354
+ return this.transport.request({
355
+ path: ["v1", "companies", "US", ticker]
356
+ });
357
+ }
358
+ };
359
+ var CompanyReports = class {
360
+ constructor(transport) {
361
+ this.transport = transport;
362
+ }
363
+ transport;
364
+ get({
365
+ accessionNumber
366
+ }) {
367
+ return this.transport.request({
368
+ path: ["v1", "sec", "company-reports", accessionNumber]
369
+ });
370
+ }
371
+ getByFiscalYear({
372
+ ticker,
373
+ fiscalYear
374
+ }) {
375
+ return this.transport.request({
376
+ path: [
377
+ "v1",
378
+ "companies",
379
+ "US",
380
+ ticker,
381
+ "reports",
382
+ "annual",
383
+ String(fiscalYear)
384
+ ]
385
+ });
386
+ }
387
+ getHtml({
388
+ accessionNumber
389
+ }) {
390
+ return this.transport.request({
391
+ path: [
392
+ "v1",
393
+ "sec",
394
+ "company-reports",
395
+ accessionNumber,
396
+ "content.html"
397
+ ],
398
+ responseType: "text"
399
+ });
400
+ }
401
+ };
402
+ var InsiderTransactions = class {
403
+ constructor(transport) {
404
+ this.transport = transport;
405
+ }
406
+ transport;
407
+ list({
408
+ filingDateFrom,
409
+ filingDateUntil,
410
+ tickers,
411
+ transactionCodes
412
+ }) {
413
+ return this.transport.request({
414
+ path: ["v1", "insider-transactions"],
415
+ query: {
416
+ from: filingDateFrom,
417
+ tickers: commaSeparated(tickers),
418
+ transaction_codes: commaSeparated(transactionCodes),
419
+ until: filingDateUntil
420
+ }
421
+ });
422
+ }
423
+ listForCompany({
424
+ ticker,
425
+ filingDateFrom,
426
+ filingDateUntil,
427
+ transactionCodes
428
+ }) {
429
+ return this.transport.request({
430
+ path: ["v1", "companies", "US", ticker, "insider-transactions"],
431
+ query: {
432
+ from: filingDateFrom,
433
+ transaction_codes: commaSeparated(transactionCodes),
434
+ until: filingDateUntil
435
+ }
436
+ });
437
+ }
438
+ };
439
+ var InsiderFilings = class {
440
+ constructor(transport) {
441
+ this.transport = transport;
442
+ }
443
+ transport;
444
+ get({
445
+ accessionNumber
446
+ }) {
447
+ return this.transport.request({
448
+ path: ["v1", "sec", "insider-filings", accessionNumber]
449
+ });
450
+ }
451
+ };
452
+ var ProposedSales = class {
453
+ constructor(transport) {
454
+ this.transport = transport;
455
+ }
456
+ transport;
457
+ list({
458
+ filingDateFrom,
459
+ filingDateUntil,
460
+ tickers
461
+ }) {
462
+ return this.transport.request({
463
+ path: ["v1", "proposed-sales"],
464
+ query: {
465
+ from: filingDateFrom,
466
+ tickers: commaSeparated(tickers),
467
+ until: filingDateUntil
468
+ }
469
+ });
470
+ }
471
+ listForCompany({
472
+ ticker,
473
+ filingDateFrom,
474
+ filingDateUntil
475
+ }) {
476
+ return this.transport.request({
477
+ path: ["v1", "companies", "US", ticker, "proposed-sales"],
478
+ query: { from: filingDateFrom, until: filingDateUntil }
479
+ });
480
+ }
481
+ getFiling({
482
+ accessionNumber
483
+ }) {
484
+ return this.transport.request({
485
+ path: ["v1", "sec", "proposed-sales", accessionNumber]
486
+ });
487
+ }
488
+ };
489
+ var InstitutionalFilings = class {
490
+ constructor(transport) {
491
+ this.transport = transport;
492
+ }
493
+ transport;
494
+ get({
495
+ accessionNumber
496
+ }) {
497
+ return this.transport.request({
498
+ path: ["v1", "sec", "institutional-filings", accessionNumber]
499
+ });
500
+ }
501
+ listHoldings({
502
+ accessionNumber,
503
+ cursor,
504
+ limit
505
+ }) {
506
+ return this.transport.request({
507
+ path: [
508
+ "v1",
509
+ "sec",
510
+ "institutional-filings",
511
+ accessionNumber,
512
+ "holdings"
513
+ ],
514
+ query: { cursor, limit }
515
+ });
516
+ }
517
+ async *iterateHoldings({
518
+ accessionNumber,
519
+ limit
520
+ }) {
521
+ let cursor;
522
+ do {
523
+ const page = await this.listHoldings({
524
+ accessionNumber,
525
+ ...cursor === void 0 ? {} : { cursor },
526
+ ...limit === void 0 ? {} : { limit }
527
+ });
528
+ yield* page.holdings;
529
+ cursor = page.next_cursor ?? void 0;
530
+ } while (cursor !== void 0);
531
+ }
532
+ };
533
+ var InstitutionalManagers = class {
534
+ constructor(transport) {
535
+ this.transport = transport;
536
+ }
537
+ transport;
538
+ list({
539
+ cik,
540
+ form13fFileNumber,
541
+ cursor,
542
+ limit
543
+ } = {}) {
544
+ return this.transport.request({
545
+ path: ["v1", "institutional-holdings", "managers"],
546
+ query: {
547
+ cik,
548
+ cursor,
549
+ form_13f_file_number: form13fFileNumber,
550
+ limit
551
+ }
552
+ });
553
+ }
554
+ get({
555
+ form13fFileNumber
556
+ }) {
557
+ return this.transport.request({
558
+ path: [
559
+ "v1",
560
+ "institutional-holdings",
561
+ "managers",
562
+ form13fFileNumber
563
+ ]
564
+ });
565
+ }
566
+ listPeriods({
567
+ form13fFileNumber
568
+ }) {
569
+ return this.transport.request({
570
+ path: [
571
+ "v1",
572
+ "institutional-holdings",
573
+ "managers",
574
+ form13fFileNumber,
575
+ "periods"
576
+ ]
577
+ });
578
+ }
579
+ listHoldings({
580
+ form13fFileNumber,
581
+ reportPeriod,
582
+ cursor,
583
+ limit
584
+ }) {
585
+ return this.transport.request({
586
+ path: [
587
+ "v1",
588
+ "institutional-holdings",
589
+ "managers",
590
+ form13fFileNumber,
591
+ "holdings"
592
+ ],
593
+ query: { cursor, limit, period: reportPeriod }
594
+ });
595
+ }
596
+ listPositions({
597
+ form13fFileNumber,
598
+ reportPeriod,
599
+ cursor,
600
+ limit
601
+ }) {
602
+ return this.transport.request({
603
+ path: [
604
+ "v1",
605
+ "institutional-holdings",
606
+ "managers",
607
+ form13fFileNumber,
608
+ "positions"
609
+ ],
610
+ query: { cursor, limit, period: reportPeriod }
611
+ });
612
+ }
613
+ async *iterate({
614
+ cik,
615
+ form13fFileNumber,
616
+ limit
617
+ } = {}) {
618
+ let cursor;
619
+ do {
620
+ const page = await this.list({
621
+ ...cik === void 0 ? {} : { cik },
622
+ ...cursor === void 0 ? {} : { cursor },
623
+ ...form13fFileNumber === void 0 ? {} : { form13fFileNumber },
624
+ ...limit === void 0 ? {} : { limit }
625
+ });
626
+ yield* page.managers;
627
+ cursor = page.next_cursor ?? void 0;
628
+ } while (cursor !== void 0);
629
+ }
630
+ async *iterateHoldings({
631
+ form13fFileNumber,
632
+ reportPeriod,
633
+ limit
634
+ }) {
635
+ let cursor;
636
+ do {
637
+ const page = await this.listHoldings({
638
+ form13fFileNumber,
639
+ reportPeriod,
640
+ ...cursor === void 0 ? {} : { cursor },
641
+ ...limit === void 0 ? {} : { limit }
642
+ });
643
+ yield* page.holdings;
644
+ cursor = page.next_cursor ?? void 0;
645
+ } while (cursor !== void 0);
646
+ }
647
+ async *iteratePositions({
648
+ form13fFileNumber,
649
+ reportPeriod,
650
+ limit
651
+ }) {
652
+ let cursor;
653
+ do {
654
+ const page = await this.listPositions({
655
+ form13fFileNumber,
656
+ reportPeriod,
657
+ ...cursor === void 0 ? {} : { cursor },
658
+ ...limit === void 0 ? {} : { limit }
659
+ });
660
+ yield* page.positions;
661
+ cursor = page.next_cursor ?? void 0;
662
+ } while (cursor !== void 0);
663
+ }
664
+ };
665
+ function commaSeparated(values) {
666
+ return values?.join(",");
667
+ }
668
+
669
+ // src/client.ts
670
+ var Chadwin = class {
671
+ companies;
672
+ companyReports;
673
+ feeds;
674
+ insiderFilings;
675
+ insiderTransactions;
676
+ institutionalFilings;
677
+ institutionalManagers;
678
+ proposedSales;
679
+ constructor(options) {
680
+ if (!options || typeof options.apiKey !== "string" || options.apiKey.trim().length === 0) {
681
+ throw new TypeError("A non-empty Chadwin API key is required");
682
+ }
683
+ const transport = new HttpTransport({
684
+ apiKey: options.apiKey,
685
+ ...options.baseURL === void 0 ? {} : { baseURL: options.baseURL }
686
+ });
687
+ this.companies = new Companies(transport);
688
+ this.companyReports = new CompanyReports(transport);
689
+ this.feeds = new Feeds(transport);
690
+ this.insiderFilings = new InsiderFilings(transport);
691
+ this.insiderTransactions = new InsiderTransactions(transport);
692
+ this.institutionalFilings = new InstitutionalFilings(transport);
693
+ this.institutionalManagers = new InstitutionalManagers(transport);
694
+ this.proposedSales = new ProposedSales(transport);
695
+ }
696
+ };
697
+ export {
698
+ APIError,
699
+ Chadwin
700
+ };
701
+ //# sourceMappingURL=index.js.map