@legalize-dev/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,1063 @@
1
+ import * as os from 'os';
2
+ import { createHmac, timingSafeEqual } from 'crypto';
3
+
4
+ // src/client.ts
5
+
6
+ // src/retry.ts
7
+ var DEFAULT_MAX_RETRIES = 3;
8
+ var DEFAULT_INITIAL_DELAY = 0.5;
9
+ var DEFAULT_MAX_DELAY = 30;
10
+ var DEFAULT_BACKOFF_FACTOR = 2;
11
+ var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
12
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
13
+ "GET",
14
+ "HEAD",
15
+ "OPTIONS",
16
+ "PUT",
17
+ "DELETE"
18
+ ]);
19
+ var RetryPolicy = class {
20
+ maxRetries;
21
+ initialDelay;
22
+ maxDelay;
23
+ backoffFactor;
24
+ retryNonIdempotent;
25
+ constructor(options = {}) {
26
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
27
+ this.initialDelay = options.initialDelay ?? DEFAULT_INITIAL_DELAY;
28
+ this.maxDelay = options.maxDelay ?? DEFAULT_MAX_DELAY;
29
+ this.backoffFactor = options.backoffFactor ?? DEFAULT_BACKOFF_FACTOR;
30
+ this.retryNonIdempotent = options.retryNonIdempotent ?? false;
31
+ }
32
+ /**
33
+ * Decide whether to retry given attempt index, HTTP status, and method.
34
+ *
35
+ * `status` is undefined when the failure was a transport error before
36
+ * the server returned a status. Transport errors are retried for any
37
+ * method (the request never hit the server, so the "don't duplicate
38
+ * mutations" concern doesn't apply).
39
+ */
40
+ shouldRetry(attempt, options) {
41
+ if (attempt >= this.maxRetries) return false;
42
+ const method = (options.method ?? "GET").toUpperCase();
43
+ if (options.status === void 0) {
44
+ return true;
45
+ }
46
+ if (!RETRY_STATUSES.has(options.status)) return false;
47
+ if (this.retryNonIdempotent) return true;
48
+ return IDEMPOTENT_METHODS.has(method);
49
+ }
50
+ /**
51
+ * Seconds to wait before retry `attempt` (0-indexed).
52
+ *
53
+ * `Retry-After` wins unambiguously when present and non-negative: the
54
+ * server is telling us exactly how long to wait. Otherwise we use
55
+ * exponential backoff with full jitter:
56
+ *
57
+ * delay = random.uniform(0, min(maxDelay, initial * factor^attempt))
58
+ *
59
+ * Full jitter beats "equal jitter" and "decorrelated jitter" for
60
+ * preventing thundering-herd recovery spikes.
61
+ */
62
+ computeDelay(attempt, options) {
63
+ if (options.retryAfter !== void 0 && options.retryAfter >= 0) {
64
+ return Math.min(options.retryAfter, this.maxDelay);
65
+ }
66
+ const base = this.initialDelay * Math.pow(this.backoffFactor, attempt);
67
+ const capped = Math.min(base, this.maxDelay);
68
+ return Math.random() * capped;
69
+ }
70
+ };
71
+ function parseRetryAfter(header) {
72
+ if (header === null || header === void 0) return void 0;
73
+ const trimmed = header.trim();
74
+ if (!trimmed) return void 0;
75
+ if (/^-?\d+$/.test(trimmed)) {
76
+ const n = parseInt(trimmed, 10);
77
+ return Math.max(0, n);
78
+ }
79
+ const parsed = Date.parse(trimmed);
80
+ if (Number.isNaN(parsed)) return void 0;
81
+ const nowMs = Date.now();
82
+ const delta = (parsed - nowMs) / 1e3;
83
+ return Math.max(0, delta);
84
+ }
85
+ function sleep(seconds) {
86
+ if (seconds <= 0) return Promise.resolve();
87
+ return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
88
+ }
89
+
90
+ // src/errors.ts
91
+ var LegalizeError = class extends Error {
92
+ name = "LegalizeError";
93
+ constructor(message, options) {
94
+ super(message);
95
+ if (options?.cause !== void 0) {
96
+ this.cause = options.cause;
97
+ }
98
+ Object.setPrototypeOf(this, new.target.prototype);
99
+ }
100
+ };
101
+ var APIError = class extends LegalizeError {
102
+ name = "APIError";
103
+ statusCode;
104
+ code;
105
+ body;
106
+ data;
107
+ requestId;
108
+ response;
109
+ constructor(options) {
110
+ super(options.message, options.cause !== void 0 ? { cause: options.cause } : void 0);
111
+ this.statusCode = options.statusCode;
112
+ this.code = options.code;
113
+ this.body = options.body;
114
+ this.data = options.data;
115
+ this.requestId = options.requestId;
116
+ this.response = options.response;
117
+ }
118
+ toString() {
119
+ const parts = [];
120
+ if (this.statusCode !== void 0) parts.push(`HTTP ${this.statusCode}`);
121
+ if (this.code) parts.push(this.code);
122
+ parts.push(this.message);
123
+ if (this.requestId) parts.push(`(request_id=${this.requestId})`);
124
+ return parts.join(" ");
125
+ }
126
+ /**
127
+ * Build the most specific APIError subclass for a response.
128
+ *
129
+ * `body` is the raw bytes of the response body (or the parsed JSON
130
+ * object if already consumed). `data` is the parsed JSON, which
131
+ * drives error-code dispatch. Reads `X-Request-Id` for support.
132
+ */
133
+ static fromResponse(response, body, data) {
134
+ const status = response.status;
135
+ const parsedData = data !== void 0 ? data : body;
136
+ const { code, message, extras } = parseErrorBody(parsedData, response);
137
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("X-Request-Id") ?? void 0;
138
+ const ErrorClass = pickErrorClass(status);
139
+ const err = new ErrorClass({
140
+ message,
141
+ statusCode: status,
142
+ code,
143
+ body,
144
+ data: parsedData,
145
+ requestId,
146
+ response
147
+ });
148
+ for (const [k, v] of Object.entries(extras)) {
149
+ err[k] = v;
150
+ }
151
+ return err;
152
+ }
153
+ };
154
+ var AuthenticationError = class extends APIError {
155
+ name = "AuthenticationError";
156
+ };
157
+ var ForbiddenError = class extends APIError {
158
+ name = "ForbiddenError";
159
+ };
160
+ var NotFoundError = class extends APIError {
161
+ name = "NotFoundError";
162
+ };
163
+ var InvalidRequestError = class extends APIError {
164
+ name = "InvalidRequestError";
165
+ };
166
+ var ValidationError = class extends APIError {
167
+ name = "ValidationError";
168
+ errors = [];
169
+ };
170
+ var RateLimitError = class extends APIError {
171
+ name = "RateLimitError";
172
+ retryAfter;
173
+ limit;
174
+ };
175
+ var ServerError = class extends APIError {
176
+ name = "ServerError";
177
+ };
178
+ var ServiceUnavailableError = class extends ServerError {
179
+ name = "ServiceUnavailableError";
180
+ };
181
+ var APIConnectionError = class extends LegalizeError {
182
+ name = "APIConnectionError";
183
+ };
184
+ var APITimeoutError = class extends APIConnectionError {
185
+ name = "APITimeoutError";
186
+ };
187
+ var WebhookVerificationError = class extends LegalizeError {
188
+ name = "WebhookVerificationError";
189
+ reason;
190
+ constructor(reason) {
191
+ super("Webhook signature verification failed");
192
+ this.reason = reason;
193
+ }
194
+ };
195
+ function parseErrorBody(data, response) {
196
+ const extras = {};
197
+ let code;
198
+ let message = "";
199
+ if (data && typeof data === "object" && !Array.isArray(data)) {
200
+ const dataObj = data;
201
+ const detailRaw = "detail" in dataObj ? dataObj.detail : dataObj;
202
+ if (detailRaw && typeof detailRaw === "object" && !Array.isArray(detailRaw)) {
203
+ const detail = detailRaw;
204
+ code = detail.error ?? detail.code;
205
+ message = detail.message ?? detail.detail ?? "";
206
+ for (const key of ["retry_after", "limit", "upgrade_url"]) {
207
+ if (key in detail) {
208
+ if (key === "retry_after") {
209
+ extras.retryAfter = detail[key];
210
+ } else {
211
+ extras[key] = detail[key];
212
+ }
213
+ }
214
+ }
215
+ } else if (Array.isArray(detailRaw)) {
216
+ extras.errors = detailRaw;
217
+ if (detailRaw.length > 0) {
218
+ const first = detailRaw[0];
219
+ message = first.msg ?? "validation error";
220
+ } else {
221
+ message = "validation error";
222
+ }
223
+ } else if (typeof detailRaw === "string") {
224
+ message = detailRaw;
225
+ }
226
+ }
227
+ if (extras.retryAfter === void 0) {
228
+ const parsed = parseRetryAfter(response.headers.get("retry-after"));
229
+ if (parsed !== void 0) {
230
+ extras.retryAfter = parsed;
231
+ }
232
+ }
233
+ if (!message) {
234
+ message = `HTTP ${response.status}`;
235
+ }
236
+ return { code, message, extras };
237
+ }
238
+ function pickErrorClass(status) {
239
+ if (status === 400) return InvalidRequestError;
240
+ if (status === 401) return AuthenticationError;
241
+ if (status === 403) return ForbiddenError;
242
+ if (status === 404) return NotFoundError;
243
+ if (status === 422) return ValidationError;
244
+ if (status === 429) return RateLimitError;
245
+ if (status === 503) return ServiceUnavailableError;
246
+ if (status >= 500 && status < 600) return ServerError;
247
+ return APIError;
248
+ }
249
+
250
+ // src/env.ts
251
+ var DEFAULT_BASE_URL = "https://legalize.dev";
252
+ var DEFAULT_API_VERSION = "v1";
253
+ var DEFAULT_TIMEOUT = 3e4;
254
+ var KEY_PREFIX = "leg_";
255
+ function resolveApiKey(apiKey) {
256
+ let key = apiKey;
257
+ if (key === void 0) {
258
+ const envKey = process.env.LEGALIZE_API_KEY;
259
+ key = envKey || void 0;
260
+ }
261
+ if (!key) {
262
+ throw new AuthenticationError({
263
+ message: "Missing API key. Pass apiKey=... or set LEGALIZE_API_KEY.",
264
+ statusCode: 401,
265
+ code: "missing_api_key"
266
+ });
267
+ }
268
+ if (!key.startsWith(KEY_PREFIX)) {
269
+ throw new AuthenticationError({
270
+ message: `API key format unrecognized. Keys start with "${KEY_PREFIX}".`,
271
+ statusCode: 401,
272
+ code: "invalid_api_key"
273
+ });
274
+ }
275
+ return key;
276
+ }
277
+ function resolveBaseUrl(baseUrl) {
278
+ if (baseUrl !== void 0) {
279
+ return baseUrl;
280
+ }
281
+ const envUrl = process.env.LEGALIZE_BASE_URL;
282
+ if (envUrl && envUrl.length > 0) {
283
+ return envUrl;
284
+ }
285
+ return DEFAULT_BASE_URL;
286
+ }
287
+ function resolveApiVersion(apiVersion) {
288
+ if (apiVersion !== void 0) {
289
+ return apiVersion;
290
+ }
291
+ const envVer = process.env.LEGALIZE_API_VERSION;
292
+ if (envVer && envVer.length > 0) {
293
+ return envVer;
294
+ }
295
+ return DEFAULT_API_VERSION;
296
+ }
297
+
298
+ // src/resources/countries.ts
299
+ var API = "/api/v1";
300
+ var Countries = class {
301
+ client;
302
+ constructor(client) {
303
+ this.client = client;
304
+ }
305
+ /** Return every country the API serves, with law counts. */
306
+ async list(options = {}) {
307
+ return this.client.request("GET", `${API}/countries`, {
308
+ ...options.signal ? { signal: options.signal } : {}
309
+ });
310
+ }
311
+ };
312
+
313
+ // src/resources/jurisdictions.ts
314
+ var API2 = "/api/v1";
315
+ var Jurisdictions = class {
316
+ client;
317
+ constructor(client) {
318
+ this.client = client;
319
+ }
320
+ /** List jurisdictions for a country (e.g. Spain's comunidades). */
321
+ async list(country, options = {}) {
322
+ return this.client.request("GET", `${API2}/${country}/jurisdictions`, {
323
+ ...options.signal ? { signal: options.signal } : {}
324
+ });
325
+ }
326
+ };
327
+
328
+ // src/pagination.ts
329
+ var PAGE_MAX = 100;
330
+ var PageIterator = class {
331
+ fetchPage;
332
+ perPage;
333
+ limit;
334
+ page;
335
+ yielded;
336
+ buffer;
337
+ bufferIdx;
338
+ total;
339
+ shortPage;
340
+ constructor(fetchPage, options = {}) {
341
+ const perPage = options.perPage ?? PAGE_MAX;
342
+ if (perPage < 1 || perPage > PAGE_MAX) {
343
+ throw new RangeError(`perPage must be between 1 and ${PAGE_MAX}`);
344
+ }
345
+ if (options.limit !== void 0 && options.limit < 0) {
346
+ throw new RangeError("limit must be >= 0");
347
+ }
348
+ this.fetchPage = fetchPage;
349
+ this.perPage = perPage;
350
+ this.limit = options.limit;
351
+ this.page = options.startPage ?? 1;
352
+ this.yielded = 0;
353
+ this.buffer = [];
354
+ this.bufferIdx = 0;
355
+ this.total = 0;
356
+ this.shortPage = false;
357
+ }
358
+ [Symbol.asyncIterator]() {
359
+ return this;
360
+ }
361
+ async next() {
362
+ while (true) {
363
+ if (this.limit !== void 0 && this.yielded >= this.limit) {
364
+ return { value: void 0, done: true };
365
+ }
366
+ if (this.bufferIdx < this.buffer.length) {
367
+ const item = this.buffer[this.bufferIdx];
368
+ this.bufferIdx += 1;
369
+ this.yielded += 1;
370
+ return { value: item, done: false };
371
+ }
372
+ if (this.yielded > 0 && this.yielded >= this.total) {
373
+ return { value: void 0, done: true };
374
+ }
375
+ if (this.shortPage) {
376
+ return { value: void 0, done: true };
377
+ }
378
+ const [items, total] = await this.fetchPage(this.page, this.perPage);
379
+ if (items.length === 0) {
380
+ return { value: void 0, done: true };
381
+ }
382
+ this.buffer = items;
383
+ this.bufferIdx = 0;
384
+ this.total = total;
385
+ this.shortPage = items.length < this.perPage;
386
+ this.page += 1;
387
+ }
388
+ }
389
+ };
390
+ var OffsetIterator = class {
391
+ fetchPage;
392
+ batch;
393
+ limit;
394
+ offset;
395
+ yielded;
396
+ buffer;
397
+ bufferIdx;
398
+ done;
399
+ constructor(fetchPage, options = {}) {
400
+ const batch = options.batch ?? 100;
401
+ if (batch < 1) {
402
+ throw new RangeError("batch must be >= 1");
403
+ }
404
+ if (options.limit !== void 0 && options.limit < 0) {
405
+ throw new RangeError("limit must be >= 0");
406
+ }
407
+ this.fetchPage = fetchPage;
408
+ this.batch = batch;
409
+ this.limit = options.limit;
410
+ this.offset = options.startOffset ?? 0;
411
+ this.yielded = 0;
412
+ this.buffer = [];
413
+ this.bufferIdx = 0;
414
+ this.done = false;
415
+ }
416
+ [Symbol.asyncIterator]() {
417
+ return this;
418
+ }
419
+ async next() {
420
+ while (true) {
421
+ if (this.limit !== void 0 && this.yielded >= this.limit) {
422
+ return { value: void 0, done: true };
423
+ }
424
+ if (this.bufferIdx < this.buffer.length) {
425
+ const item = this.buffer[this.bufferIdx];
426
+ this.bufferIdx += 1;
427
+ this.yielded += 1;
428
+ return { value: item, done: false };
429
+ }
430
+ if (this.done) return { value: void 0, done: true };
431
+ const [items, total] = await this.fetchPage(this.batch, this.offset);
432
+ if (items.length === 0) {
433
+ this.done = true;
434
+ return { value: void 0, done: true };
435
+ }
436
+ this.buffer = items;
437
+ this.bufferIdx = 0;
438
+ this.offset += items.length;
439
+ if (this.offset >= total) this.done = true;
440
+ else if (items.length < this.batch) this.done = true;
441
+ }
442
+ }
443
+ };
444
+
445
+ // src/resources/laws.ts
446
+ var API3 = "/api/v1";
447
+ function buildFilterParams(opts, extras) {
448
+ return {
449
+ law_type: opts.lawType,
450
+ year: opts.year,
451
+ status: opts.status,
452
+ jurisdiction: opts.jurisdiction,
453
+ from_date: opts.fromDate,
454
+ to_date: opts.toDate,
455
+ sort: opts.sort,
456
+ ...extras
457
+ };
458
+ }
459
+ var Laws = class {
460
+ client;
461
+ constructor(client) {
462
+ this.client = client;
463
+ }
464
+ /** Return a single page of laws for a country. */
465
+ async list(country, options = {}) {
466
+ const params = buildFilterParams(options, {
467
+ page: options.page ?? 1,
468
+ per_page: options.perPage ?? 50
469
+ });
470
+ return this.client.request("GET", `${API3}/${country}/laws`, {
471
+ params,
472
+ ...options.signal ? { signal: options.signal } : {}
473
+ });
474
+ }
475
+ /** Full-text search for laws. `q` is required. */
476
+ async search(country, q, options = {}) {
477
+ if (!q || !q.trim()) {
478
+ throw new TypeError("q must be a non-empty search query");
479
+ }
480
+ const params = buildFilterParams(options, {
481
+ page: options.page ?? 1,
482
+ per_page: options.perPage ?? 50,
483
+ q
484
+ });
485
+ return this.client.request("GET", `${API3}/${country}/laws`, {
486
+ params,
487
+ ...options.signal ? { signal: options.signal } : {}
488
+ });
489
+ }
490
+ /** Auto-paginate across every matching law. */
491
+ iter(country, options = {}) {
492
+ const perPage = options.perPage ?? 100;
493
+ const limit = options.limit;
494
+ const fetchPage = async (page, per) => {
495
+ const listOpts = {
496
+ page,
497
+ perPage: per,
498
+ lawType: options.lawType,
499
+ year: options.year,
500
+ status: options.status,
501
+ jurisdiction: options.jurisdiction,
502
+ fromDate: options.fromDate,
503
+ toDate: options.toDate,
504
+ sort: options.sort
505
+ };
506
+ if (options.signal) listOpts.signal = options.signal;
507
+ const resp = await this.list(country, listOpts);
508
+ return [resp.results, resp.total];
509
+ };
510
+ return new PageIterator(fetchPage, { perPage, ...limit !== void 0 ? { limit } : {} });
511
+ }
512
+ /** Auto-paginate across every match of a full-text search. */
513
+ searchIter(country, q, options = {}) {
514
+ if (!q || !q.trim()) {
515
+ throw new TypeError("q must be a non-empty search query");
516
+ }
517
+ const perPage = options.perPage ?? 100;
518
+ const limit = options.limit;
519
+ const fetchPage = async (page, per) => {
520
+ const searchOpts = {
521
+ page,
522
+ perPage: per,
523
+ lawType: options.lawType,
524
+ year: options.year,
525
+ status: options.status,
526
+ jurisdiction: options.jurisdiction,
527
+ fromDate: options.fromDate,
528
+ toDate: options.toDate,
529
+ sort: options.sort
530
+ };
531
+ if (options.signal) searchOpts.signal = options.signal;
532
+ const resp = await this.search(country, q, searchOpts);
533
+ return [resp.results, resp.total];
534
+ };
535
+ return new PageIterator(fetchPage, { perPage, ...limit !== void 0 ? { limit } : {} });
536
+ }
537
+ /** Fetch the full law including Markdown content. */
538
+ async retrieve(country, lawId, options = {}) {
539
+ return this.client.request("GET", `${API3}/${country}/laws/${lawId}`, {
540
+ ...options.signal ? { signal: options.signal } : {}
541
+ });
542
+ }
543
+ /** Fetch only the law metadata (no content). */
544
+ async meta(country, lawId, options = {}) {
545
+ return this.client.request("GET", `${API3}/${country}/laws/${lawId}/meta`, {
546
+ ...options.signal ? { signal: options.signal } : {}
547
+ });
548
+ }
549
+ /** Git commit history for the law. */
550
+ async commits(country, lawId, options = {}) {
551
+ return this.client.request(
552
+ "GET",
553
+ `${API3}/${country}/laws/${lawId}/commits`,
554
+ { ...options.signal ? { signal: options.signal } : {} }
555
+ );
556
+ }
557
+ /** Return the law's full text at a specific historical version. */
558
+ async atCommit(country, lawId, sha, options = {}) {
559
+ return this.client.request(
560
+ "GET",
561
+ `${API3}/${country}/laws/${lawId}/at/${sha}`,
562
+ { ...options.signal ? { signal: options.signal } : {} }
563
+ );
564
+ }
565
+ };
566
+
567
+ // src/resources/lawTypes.ts
568
+ var API4 = "/api/v1";
569
+ var LawTypes = class {
570
+ client;
571
+ constructor(client) {
572
+ this.client = client;
573
+ }
574
+ /** List law type identifiers (e.g. `["constitucion", "ley", "real_decreto"]`). */
575
+ async list(country, options = {}) {
576
+ return this.client.request("GET", `${API4}/${country}/law-types`, {
577
+ ...options.signal ? { signal: options.signal } : {}
578
+ });
579
+ }
580
+ };
581
+
582
+ // src/resources/reforms.ts
583
+ var API5 = "/api/v1";
584
+ var Reforms = class {
585
+ client;
586
+ constructor(client) {
587
+ this.client = client;
588
+ }
589
+ /** Return a single page of reforms for a law. */
590
+ async list(country, lawId, options = {}) {
591
+ const params = {
592
+ limit: options.limit ?? 100,
593
+ offset: options.offset ?? 0
594
+ };
595
+ return this.client.request(
596
+ "GET",
597
+ `${API5}/${country}/laws/${lawId}/reforms`,
598
+ { params, ...options.signal ? { signal: options.signal } : {} }
599
+ );
600
+ }
601
+ /** Auto-paginate across every reform for a law. */
602
+ iter(country, lawId, options = {}) {
603
+ const batch = options.batch ?? 100;
604
+ const limit = options.limit;
605
+ const fetchPage = async (limitArg, offset) => {
606
+ const listOpts = {
607
+ limit: limitArg,
608
+ offset
609
+ };
610
+ if (options.signal) listOpts.signal = options.signal;
611
+ const resp = await this.list(country, lawId, listOpts);
612
+ return [resp.reforms, resp.total];
613
+ };
614
+ return new OffsetIterator(fetchPage, { batch, ...limit !== void 0 ? { limit } : {} });
615
+ }
616
+ };
617
+
618
+ // src/resources/stats.ts
619
+ var API6 = "/api/v1";
620
+ var Stats = class {
621
+ client;
622
+ constructor(client) {
623
+ this.client = client;
624
+ }
625
+ /** Return aggregate stats for a country (and optionally a jurisdiction). */
626
+ async retrieve(country, options = {}) {
627
+ const params = { jurisdiction: options.jurisdiction };
628
+ return this.client.request("GET", `${API6}/${country}/stats`, {
629
+ params,
630
+ ...options.signal ? { signal: options.signal } : {}
631
+ });
632
+ }
633
+ };
634
+
635
+ // src/resources/webhooks.ts
636
+ var API7 = "/api/v1";
637
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["failed", "success", "pending"]);
638
+ var Webhooks = class {
639
+ client;
640
+ constructor(client) {
641
+ this.client = client;
642
+ }
643
+ /** Create a webhook endpoint. Returns the signing secret ONCE. */
644
+ async create(options) {
645
+ const body = {
646
+ url: options.url,
647
+ event_types: options.eventTypes,
648
+ countries: options.countries ?? null,
649
+ description: options.description ?? ""
650
+ };
651
+ return this.client.request("POST", `${API7}/webhooks`, {
652
+ json: body,
653
+ ...options.signal ? { signal: options.signal } : {}
654
+ });
655
+ }
656
+ /** List all webhook endpoints for the authenticated org. */
657
+ async list(options = {}) {
658
+ return this.client.request("GET", `${API7}/webhooks`, {
659
+ ...options.signal ? { signal: options.signal } : {}
660
+ });
661
+ }
662
+ /** Fetch a single endpoint by id. */
663
+ async retrieve(endpointId, options = {}) {
664
+ return this.client.request("GET", `${API7}/webhooks/${endpointId}`, {
665
+ ...options.signal ? { signal: options.signal } : {}
666
+ });
667
+ }
668
+ /** Patch mutable fields on a webhook endpoint. */
669
+ async update(endpointId, options) {
670
+ const body = {};
671
+ if (options.url !== void 0) body.url = options.url;
672
+ if (options.eventTypes !== void 0) body.event_types = options.eventTypes;
673
+ if (options.countries !== void 0) body.countries = options.countries;
674
+ if (options.description !== void 0) body.description = options.description;
675
+ if (options.enabled !== void 0) body.enabled = options.enabled;
676
+ return this.client.request("PATCH", `${API7}/webhooks/${endpointId}`, {
677
+ json: body,
678
+ ...options.signal ? { signal: options.signal } : {}
679
+ });
680
+ }
681
+ /** Delete a webhook endpoint. */
682
+ async delete(endpointId, options = {}) {
683
+ return this.client.request(
684
+ "DELETE",
685
+ `${API7}/webhooks/${endpointId}`,
686
+ { ...options.signal ? { signal: options.signal } : {} }
687
+ );
688
+ }
689
+ /** List delivery attempts for an endpoint, optionally filtered by status. */
690
+ async deliveries(endpointId, options = {}) {
691
+ if (options.status !== void 0 && !VALID_STATUSES.has(options.status)) {
692
+ throw new TypeError(
693
+ "status must be 'failed', 'success', 'pending', or omitted"
694
+ );
695
+ }
696
+ const params = {
697
+ page: options.page ?? 1,
698
+ status: options.status
699
+ };
700
+ return this.client.request(
701
+ "GET",
702
+ `${API7}/webhooks/${endpointId}/deliveries`,
703
+ { params, ...options.signal ? { signal: options.signal } : {} }
704
+ );
705
+ }
706
+ /** Retry a failed delivery. */
707
+ async retry(endpointId, deliveryId, options = {}) {
708
+ return this.client.request(
709
+ "POST",
710
+ `${API7}/webhooks/${endpointId}/deliveries/${deliveryId}/retry`,
711
+ { ...options.signal ? { signal: options.signal } : {} }
712
+ );
713
+ }
714
+ /** Send a `test.ping` event to verify the endpoint is reachable. */
715
+ async test(endpointId, options = {}) {
716
+ return this.client.request(
717
+ "POST",
718
+ `${API7}/webhooks/${endpointId}/test`,
719
+ { ...options.signal ? { signal: options.signal } : {} }
720
+ );
721
+ }
722
+ };
723
+
724
+ // src/version.ts
725
+ var SDK_VERSION = "0.1.0";
726
+
727
+ // src/client.ts
728
+ function defaultUserAgent() {
729
+ return `legalize-node/${SDK_VERSION} node/${process.version} ${os.platform()}`;
730
+ }
731
+ function stripTrailingSlashes(s) {
732
+ let end = s.length;
733
+ while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
734
+ return s.slice(0, end);
735
+ }
736
+ var Legalize = class {
737
+ countries;
738
+ jurisdictions;
739
+ lawTypes;
740
+ laws;
741
+ reforms;
742
+ stats;
743
+ webhooks;
744
+ /** Exposed for tests; treat as private otherwise. */
745
+ _apiKey;
746
+ _baseUrl;
747
+ _apiVersion;
748
+ _headers;
749
+ _timeout;
750
+ _retry;
751
+ _fetch;
752
+ _lastResponse = null;
753
+ constructor(options = {}) {
754
+ this._apiKey = resolveApiKey(options.apiKey);
755
+ this._baseUrl = stripTrailingSlashes(resolveBaseUrl(options.baseUrl));
756
+ this._apiVersion = resolveApiVersion(options.apiVersion);
757
+ this._timeout = options.timeout ?? DEFAULT_TIMEOUT;
758
+ this._retry = resolveRetryPolicy(options.retry, options.maxRetries);
759
+ this._fetch = options.fetch ?? globalThis.fetch;
760
+ this._headers = buildHeaders(this._apiKey, this._apiVersion, options.defaultHeaders);
761
+ this.countries = new Countries(this);
762
+ this.jurisdictions = new Jurisdictions(this);
763
+ this.lawTypes = new LawTypes(this);
764
+ this.laws = new Laws(this);
765
+ this.reforms = new Reforms(this);
766
+ this.stats = new Stats(this);
767
+ this.webhooks = new Webhooks(this);
768
+ }
769
+ /** The raw HTTP response from the most recent request, or null. */
770
+ get lastResponse() {
771
+ return this._lastResponse;
772
+ }
773
+ /**
774
+ * Execute a request and return the parsed JSON body (or null for 204).
775
+ *
776
+ * Throws an APIError subclass on non-2xx responses (after retries),
777
+ * APITimeoutError on timeout, or APIConnectionError on transport
778
+ * failure.
779
+ */
780
+ async request(method, path, options = {}) {
781
+ const upperMethod = method.toUpperCase();
782
+ const url = this.buildUrl(path, options.params);
783
+ const headers = { ...this._headers };
784
+ if (options.extraHeaders) Object.assign(headers, options.extraHeaders);
785
+ if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
786
+ const hasJson = options.json !== void 0;
787
+ if (hasJson) headers["Content-Type"] = "application/json";
788
+ let attempt = 0;
789
+ while (true) {
790
+ let response;
791
+ try {
792
+ response = await this.sendOnce(url, upperMethod, headers, options, hasJson);
793
+ } catch (err) {
794
+ const shouldRetry2 = this._retry.shouldRetry(attempt, { method: upperMethod });
795
+ if (!shouldRetry2) {
796
+ throw wrapTransportError(err);
797
+ }
798
+ const delay2 = this._retry.computeDelay(attempt, {});
799
+ await sleep(delay2);
800
+ attempt += 1;
801
+ continue;
802
+ }
803
+ if (response.status >= 200 && response.status < 300) {
804
+ this._lastResponse = response;
805
+ if (response.status === 204) return null;
806
+ const text = await response.text();
807
+ if (!text) return null;
808
+ try {
809
+ return JSON.parse(text);
810
+ } catch (err) {
811
+ throw new APIError({
812
+ message: "Server returned non-JSON body",
813
+ statusCode: response.status,
814
+ body: text,
815
+ response,
816
+ cause: err
817
+ });
818
+ }
819
+ }
820
+ const shouldRetry = this._retry.shouldRetry(attempt, {
821
+ status: response.status,
822
+ method: upperMethod
823
+ });
824
+ if (!shouldRetry) {
825
+ this._lastResponse = response;
826
+ throw await errorFromResponse(response);
827
+ }
828
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
829
+ const delay = this._retry.computeDelay(attempt, { retryAfter });
830
+ try {
831
+ await response.text();
832
+ } catch {
833
+ }
834
+ await sleep(delay);
835
+ attempt += 1;
836
+ }
837
+ }
838
+ /** Release any resources held by the client. Kept for API symmetry. */
839
+ async close() {
840
+ }
841
+ /** TS 5.2+ `using` / `await using` support. */
842
+ async [Symbol.asyncDispose]() {
843
+ await this.close();
844
+ }
845
+ // ---- internals --------------------------------------------------------
846
+ buildUrl(path, params) {
847
+ let base;
848
+ if (path.startsWith("http://") || path.startsWith("https://")) {
849
+ base = path;
850
+ } else {
851
+ const p = path.startsWith("/") ? path : `/${path}`;
852
+ base = this._baseUrl + p;
853
+ }
854
+ if (!params) return base;
855
+ const query = buildQueryString(params);
856
+ if (!query) return base;
857
+ return base.includes("?") ? `${base}&${query}` : `${base}?${query}`;
858
+ }
859
+ async sendOnce(url, method, headers, options, hasJson) {
860
+ const controller = new AbortController();
861
+ const timeoutMs = this._timeout;
862
+ const timeoutHandle = setTimeout(() => controller.abort(new TimeoutSignal()), timeoutMs);
863
+ let externalListener;
864
+ if (options.signal) {
865
+ if (options.signal.aborted) controller.abort(options.signal.reason);
866
+ externalListener = () => controller.abort(options.signal.reason);
867
+ options.signal.addEventListener("abort", externalListener, { once: true });
868
+ }
869
+ try {
870
+ const init = {
871
+ method,
872
+ headers,
873
+ signal: controller.signal,
874
+ redirect: "manual"
875
+ };
876
+ if (hasJson) init.body = JSON.stringify(options.json);
877
+ return await this._fetch(url, init);
878
+ } finally {
879
+ clearTimeout(timeoutHandle);
880
+ if (options.signal && externalListener) {
881
+ options.signal.removeEventListener("abort", externalListener);
882
+ }
883
+ }
884
+ }
885
+ };
886
+ var TimeoutSignal = class extends Error {
887
+ name = "TimeoutSignal";
888
+ constructor() {
889
+ super("request timed out");
890
+ }
891
+ };
892
+ function resolveRetryPolicy(policy, maxRetries) {
893
+ if (policy !== void 0) return policy;
894
+ if (maxRetries === void 0) return new RetryPolicy();
895
+ return new RetryPolicy({ maxRetries });
896
+ }
897
+ function buildHeaders(apiKey, apiVersion, extra) {
898
+ const headers = {
899
+ Authorization: `Bearer ${apiKey}`,
900
+ "User-Agent": defaultUserAgent(),
901
+ "Legalize-API-Version": apiVersion,
902
+ Accept: "application/json"
903
+ };
904
+ if (extra) Object.assign(headers, extra);
905
+ return headers;
906
+ }
907
+ function buildQueryString(params) {
908
+ const parts = [];
909
+ for (const [key, value] of Object.entries(params)) {
910
+ if (value === void 0 || value === null) continue;
911
+ let serialized;
912
+ if (typeof value === "boolean") {
913
+ serialized = value ? "true" : "false";
914
+ } else if (Array.isArray(value)) {
915
+ if (value.length === 0) continue;
916
+ serialized = value.map((v) => String(v)).join(",");
917
+ } else {
918
+ serialized = String(value);
919
+ }
920
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(serialized)}`);
921
+ }
922
+ return parts.join("&");
923
+ }
924
+ async function errorFromResponse(response) {
925
+ const text = await response.text();
926
+ let data = void 0;
927
+ if (text) {
928
+ try {
929
+ data = JSON.parse(text);
930
+ } catch {
931
+ data = void 0;
932
+ }
933
+ }
934
+ return APIError.fromResponse(response, text, data);
935
+ }
936
+ function wrapTransportError(err) {
937
+ if (err instanceof Error) {
938
+ const isAbort = err.name === "AbortError" || err.code === "ABORT_ERR";
939
+ const causeName = err.cause?.name;
940
+ const isTimeout = isAbort && (err.message.includes("timed out") || err.message.toLowerCase().includes("timeout") || causeName === "TimeoutSignal");
941
+ if (isTimeout) {
942
+ return new APITimeoutError("request timed out", { cause: err });
943
+ }
944
+ if (isAbort) {
945
+ return new APIConnectionError("request aborted", { cause: err });
946
+ }
947
+ return new APIConnectionError(err.message || "transport error", { cause: err });
948
+ }
949
+ return new APIConnectionError("transport error");
950
+ }
951
+ var DEFAULT_TOLERANCE_SECONDS = 300;
952
+ var SUPPORTED_SCHEMES = ["v1"];
953
+ var Webhook = class _Webhook {
954
+ /** Default anti-replay tolerance. */
955
+ static TOLERANCE = DEFAULT_TOLERANCE_SECONDS;
956
+ /** Compute the canonical `v1=<hex>` signature for (payload, timestamp). */
957
+ static computeSignature(secret, payload, timestamp) {
958
+ const payloadBuf = normalizePayload(payload);
959
+ const signed = Buffer.concat([
960
+ Buffer.from(timestamp, "utf8"),
961
+ Buffer.from(".", "utf8"),
962
+ payloadBuf
963
+ ]);
964
+ const sig = createHmac("sha256", secret).update(signed).digest("hex");
965
+ return `v1=${sig}`;
966
+ }
967
+ /**
968
+ * Verify a webhook delivery and return the parsed event.
969
+ *
970
+ * Signature verification happens BEFORE JSON parsing, to protect the
971
+ * process from resource-exhaustion on unauthenticated bodies.
972
+ *
973
+ * Throws WebhookVerificationError on any failure. The `.reason`
974
+ * field identifies which check tripped for server-side logging,
975
+ * while the user-facing message stays generic.
976
+ */
977
+ static verify(options) {
978
+ if (!options.sigHeader || !options.timestamp || !options.secret) {
979
+ throw new WebhookVerificationError("missing_header");
980
+ }
981
+ const payloadBuf = normalizePayload(options.payload);
982
+ if (!/^-?\d+$/.test(options.timestamp.trim())) {
983
+ throw new WebhookVerificationError("bad_timestamp");
984
+ }
985
+ const ts = parseInt(options.timestamp.trim(), 10);
986
+ if (Number.isNaN(ts)) {
987
+ throw new WebhookVerificationError("bad_timestamp");
988
+ }
989
+ const tol = options.tolerance ?? _Webhook.TOLERANCE;
990
+ const reference = options.now ?? Date.now() / 1e3;
991
+ if (Math.abs(reference - ts) > tol) {
992
+ throw new WebhookVerificationError("timestamp_outside_tolerance");
993
+ }
994
+ const expected = _Webhook.computeSignature(options.secret, payloadBuf, options.timestamp);
995
+ const expectedHex = expected.slice(expected.indexOf("=") + 1);
996
+ const candidates = extractSchemeHexes(options.sigHeader);
997
+ if (candidates.length === 0) {
998
+ throw new WebhookVerificationError("no_valid_signature");
999
+ }
1000
+ let match = false;
1001
+ const expectedBuf = Buffer.from(expectedHex, "hex");
1002
+ for (const candidate of candidates) {
1003
+ let candBuf;
1004
+ try {
1005
+ candBuf = Buffer.from(candidate, "hex");
1006
+ } catch {
1007
+ continue;
1008
+ }
1009
+ if (candBuf.length !== expectedBuf.length) continue;
1010
+ if (timingSafeEqual(expectedBuf, candBuf)) {
1011
+ match = true;
1012
+ break;
1013
+ }
1014
+ }
1015
+ if (!match) {
1016
+ throw new WebhookVerificationError("bad_signature");
1017
+ }
1018
+ let parsed;
1019
+ try {
1020
+ parsed = JSON.parse(payloadBuf.toString("utf8"));
1021
+ } catch {
1022
+ throw new WebhookVerificationError("bad_signature");
1023
+ }
1024
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1025
+ throw new WebhookVerificationError("bad_signature");
1026
+ }
1027
+ return webhookEventFromPayload(parsed);
1028
+ }
1029
+ };
1030
+ function normalizePayload(payload) {
1031
+ if (Buffer.isBuffer(payload)) return payload;
1032
+ if (typeof payload === "string") return Buffer.from(payload, "utf8");
1033
+ return Buffer.from(payload);
1034
+ }
1035
+ function extractSchemeHexes(header) {
1036
+ const out = [];
1037
+ for (const rawPart of header.split(",")) {
1038
+ const part = rawPart.trim();
1039
+ const eq = part.indexOf("=");
1040
+ if (eq < 0) continue;
1041
+ const scheme = part.slice(0, eq);
1042
+ const value = part.slice(eq + 1).trim();
1043
+ if (!value) continue;
1044
+ if (!SUPPORTED_SCHEMES.includes(scheme)) continue;
1045
+ if (!/^[0-9a-fA-F]+$/.test(value)) continue;
1046
+ out.push(value);
1047
+ }
1048
+ return out;
1049
+ }
1050
+ function webhookEventFromPayload(payload) {
1051
+ const eventType = payload.event_type ?? payload.type ?? "";
1052
+ return {
1053
+ id: String(payload.id ?? ""),
1054
+ type: String(eventType),
1055
+ createdAt: String(payload.created_at ?? ""),
1056
+ data: payload.data && typeof payload.data === "object" && !Array.isArray(payload.data) ? { ...payload.data } : {},
1057
+ raw: { ...payload }
1058
+ };
1059
+ }
1060
+
1061
+ export { APIConnectionError, APIError, APITimeoutError, AuthenticationError, Countries, DEFAULT_API_VERSION, DEFAULT_BACKOFF_FACTOR, DEFAULT_BASE_URL, DEFAULT_INITIAL_DELAY, DEFAULT_MAX_DELAY, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, DEFAULT_TOLERANCE_SECONDS, ForbiddenError, IDEMPOTENT_METHODS, InvalidRequestError, Jurisdictions, KEY_PREFIX, LawTypes, Laws, Legalize, LegalizeError, NotFoundError, OffsetIterator, PAGE_MAX, PageIterator, RETRY_STATUSES, RateLimitError, Reforms, RetryPolicy, SDK_VERSION, ServerError, ServiceUnavailableError, Stats, ValidationError, Webhook, WebhookVerificationError, Webhooks, buildQueryString, defaultUserAgent, parseRetryAfter, resolveApiKey, resolveApiVersion, resolveBaseUrl, sleep };
1062
+ //# sourceMappingURL=index.js.map
1063
+ //# sourceMappingURL=index.js.map