@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.
@@ -0,0 +1,1012 @@
1
+ /**
2
+ * Environment-variable resolution helpers.
3
+ *
4
+ * Contract: see `sdk/ENVIRONMENT.md`. Summary:
5
+ * - `LEGALIZE_API_KEY` — required (unless `apiKey` passed explicitly).
6
+ * - `LEGALIZE_BASE_URL` — default "https://legalize.dev".
7
+ * - `LEGALIZE_API_VERSION` — default "v1".
8
+ *
9
+ * Precedence: explicit arg > env var > default. Empty string = unset.
10
+ * Prefix `LEGALIZE_` is mandatory; no aliases.
11
+ */
12
+ declare const DEFAULT_BASE_URL = "https://legalize.dev";
13
+ declare const DEFAULT_API_VERSION = "v1";
14
+ declare const DEFAULT_TIMEOUT = 30000;
15
+ declare const KEY_PREFIX = "leg_";
16
+ /**
17
+ * Resolve the API key from the explicit argument or the environment.
18
+ *
19
+ * Raises AuthenticationError synchronously if no key is available or the
20
+ * key prefix is unrecognized. Catching obviously-bad inputs before the
21
+ * first network call saves operators from debugging 401s later.
22
+ */
23
+ declare function resolveApiKey(apiKey: string | undefined): string;
24
+ /**
25
+ * Resolve the base URL from the explicit argument, env, or default.
26
+ * Empty strings are treated as unset.
27
+ */
28
+ declare function resolveBaseUrl(baseUrl: string | undefined): string;
29
+ /**
30
+ * Resolve the API version from the explicit argument, env, or default.
31
+ * Empty strings are treated as unset.
32
+ */
33
+ declare function resolveApiVersion(apiVersion: string | undefined): string;
34
+
35
+ /**
36
+ * Retry policy for transient failures.
37
+ *
38
+ * Retries happen on:
39
+ * - Network errors (DNS, connect, read timeout, TLS)
40
+ * - HTTP 429 (rate limit)
41
+ * - HTTP 500, 502, 503, 504 (transient server issues)
42
+ *
43
+ * Retries do NOT happen on:
44
+ * - 4xx other than 429 (caller error, retrying won't help)
45
+ * - Non-idempotent methods by default (POST, PATCH): mutations are
46
+ * never auto-retried. Only GET/HEAD/OPTIONS/PUT/DELETE are retried.
47
+ * This prevents duplicate webhook creation and duplicate retry calls.
48
+ *
49
+ * The `Retry-After` header wins when present and non-negative. Otherwise
50
+ * exponential backoff with full jitter, capped at `maxDelay`.
51
+ */
52
+ declare const DEFAULT_MAX_RETRIES = 3;
53
+ declare const DEFAULT_INITIAL_DELAY = 0.5;
54
+ declare const DEFAULT_MAX_DELAY = 30;
55
+ declare const DEFAULT_BACKOFF_FACTOR = 2;
56
+ declare const RETRY_STATUSES: ReadonlySet<number>;
57
+ /** HTTP methods considered safe to auto-retry (idempotent). */
58
+ declare const IDEMPOTENT_METHODS: ReadonlySet<string>;
59
+ interface RetryPolicyOptions {
60
+ maxRetries?: number;
61
+ initialDelay?: number;
62
+ maxDelay?: number;
63
+ backoffFactor?: number;
64
+ /**
65
+ * When true, retry POST/PATCH too. Default false — the SDK never
66
+ * auto-retries mutations unless the caller opts in explicitly.
67
+ */
68
+ retryNonIdempotent?: boolean;
69
+ }
70
+ /**
71
+ * Configuration for automatic retries.
72
+ *
73
+ * Immutable — construct a new instance to tweak. Matches the Python
74
+ * `RetryPolicy` contract:
75
+ * - `maxRetries`: total attempts is at most `maxRetries + 1`. 0 disables retries.
76
+ * - `initialDelay`: seconds before the first retry (pre-jitter).
77
+ * - `maxDelay`: cap on any single sleep in seconds.
78
+ * - `backoffFactor`: multiplier applied to delay each attempt.
79
+ */
80
+ declare class RetryPolicy {
81
+ readonly maxRetries: number;
82
+ readonly initialDelay: number;
83
+ readonly maxDelay: number;
84
+ readonly backoffFactor: number;
85
+ readonly retryNonIdempotent: boolean;
86
+ constructor(options?: RetryPolicyOptions);
87
+ /**
88
+ * Decide whether to retry given attempt index, HTTP status, and method.
89
+ *
90
+ * `status` is undefined when the failure was a transport error before
91
+ * the server returned a status. Transport errors are retried for any
92
+ * method (the request never hit the server, so the "don't duplicate
93
+ * mutations" concern doesn't apply).
94
+ */
95
+ shouldRetry(attempt: number, options: {
96
+ status?: number;
97
+ method?: string;
98
+ }): boolean;
99
+ /**
100
+ * Seconds to wait before retry `attempt` (0-indexed).
101
+ *
102
+ * `Retry-After` wins unambiguously when present and non-negative: the
103
+ * server is telling us exactly how long to wait. Otherwise we use
104
+ * exponential backoff with full jitter:
105
+ *
106
+ * delay = random.uniform(0, min(maxDelay, initial * factor^attempt))
107
+ *
108
+ * Full jitter beats "equal jitter" and "decorrelated jitter" for
109
+ * preventing thundering-herd recovery spikes.
110
+ */
111
+ computeDelay(attempt: number, options: {
112
+ retryAfter?: number;
113
+ }): number;
114
+ }
115
+ /**
116
+ * Parse the `Retry-After` header into seconds.
117
+ *
118
+ * RFC 9110 allows two forms:
119
+ * - A non-negative integer (delta-seconds): `Retry-After: 120`
120
+ * - An HTTP-date: `Retry-After: Wed, 21 Oct 2025 07:28:00 GMT`
121
+ *
122
+ * Unparseable input returns `undefined` so the caller can fall back to
123
+ * its own backoff. HTTP-date values in the past clamp to `0`.
124
+ */
125
+ declare function parseRetryAfter(header: string | null | undefined): number | undefined;
126
+ /** Promise-based sleep — used by the client's retry loop. */
127
+ declare function sleep(seconds: number): Promise<void>;
128
+
129
+ interface components {
130
+ schemas: {
131
+ /** Commit */
132
+ Commit: {
133
+ /** Date */
134
+ date: string;
135
+ /** Message */
136
+ message: string;
137
+ /** Sha */
138
+ sha: string;
139
+ };
140
+ /** CommitsResponse */
141
+ CommitsResponse: {
142
+ /** Commits */
143
+ commits: components["schemas"]["Commit"][];
144
+ /** Law Id */
145
+ law_id: string;
146
+ };
147
+ /** CountryInfo */
148
+ CountryInfo: {
149
+ /** Count */
150
+ count: number;
151
+ /** Country */
152
+ country: string;
153
+ };
154
+ /** HTTPValidationError */
155
+ HTTPValidationError: {
156
+ /** Detail */
157
+ detail?: components["schemas"]["ValidationError"][];
158
+ };
159
+ /** JurisdictionInfo */
160
+ JurisdictionInfo: {
161
+ /** Count */
162
+ count: number;
163
+ /** Jurisdiction */
164
+ jurisdiction?: string | null;
165
+ };
166
+ /** LawAtCommitResponse */
167
+ LawAtCommitResponse: {
168
+ /** Content Md */
169
+ content_md: string;
170
+ /** Law Id */
171
+ law_id: string;
172
+ /** Sha */
173
+ sha: string;
174
+ };
175
+ /**
176
+ * LawDetail
177
+ * @description Full law with Markdown content (fetched from GitHub).
178
+ */
179
+ LawDetail: {
180
+ /** Article Count */
181
+ article_count?: number | null;
182
+ /** Content Md */
183
+ content_md?: string | null;
184
+ /** Country */
185
+ country: string;
186
+ /** Department */
187
+ department?: string | null;
188
+ /** Extra */
189
+ extra?: {
190
+ [key: string]: unknown;
191
+ } | null;
192
+ /** Frontmatter */
193
+ frontmatter?: {
194
+ [key: string]: unknown;
195
+ } | null;
196
+ /** Id */
197
+ id: string;
198
+ /** Jurisdiction */
199
+ jurisdiction?: string | null;
200
+ /** Last Updated */
201
+ last_updated?: string | null;
202
+ /** Law Type */
203
+ law_type: string;
204
+ /** Publication Date */
205
+ publication_date?: string | null;
206
+ /** Short Title */
207
+ short_title?: string | null;
208
+ /** Source */
209
+ source?: string | null;
210
+ /** Status */
211
+ status?: string | null;
212
+ /** Title */
213
+ title: string;
214
+ };
215
+ /**
216
+ * LawMeta
217
+ * @description Law metadata without content. Lightweight — no GitHub fetch.
218
+ */
219
+ LawMeta: {
220
+ /** Article Count */
221
+ article_count?: number | null;
222
+ /** Country */
223
+ country: string;
224
+ /** Department */
225
+ department?: string | null;
226
+ /** Extra */
227
+ extra?: {
228
+ [key: string]: unknown;
229
+ } | null;
230
+ /** Id */
231
+ id: string;
232
+ /** Jurisdiction */
233
+ jurisdiction?: string | null;
234
+ /** Last Updated */
235
+ last_updated?: string | null;
236
+ /** Law Type */
237
+ law_type: string;
238
+ /** Publication Date */
239
+ publication_date?: string | null;
240
+ /** Short Title */
241
+ short_title?: string | null;
242
+ /** Source */
243
+ source?: string | null;
244
+ /** Status */
245
+ status?: string | null;
246
+ /** Title */
247
+ title: string;
248
+ };
249
+ /**
250
+ * LawSearchResult
251
+ * @description Law result with search snippet.
252
+ */
253
+ LawSearchResult: {
254
+ /** Article Count */
255
+ article_count?: number | null;
256
+ /** Country */
257
+ country: string;
258
+ /** Id */
259
+ id: string;
260
+ /** Jurisdiction */
261
+ jurisdiction?: string | null;
262
+ /** Law Type */
263
+ law_type: string;
264
+ /** Publication Date */
265
+ publication_date?: string | null;
266
+ /** Short Title */
267
+ short_title?: string | null;
268
+ /** Status */
269
+ status?: string | null;
270
+ /** Title */
271
+ title: string;
272
+ /** Title Snippet */
273
+ title_snippet?: string | null;
274
+ };
275
+ /**
276
+ * PaginatedLaws
277
+ * @description Unified response for law listing and search.
278
+ *
279
+ * When searching (query is not None), both ``total`` and ``count``
280
+ * are populated with the same value for backward compatibility.
281
+ * When listing, ``query`` and ``count`` are None.
282
+ */
283
+ PaginatedLaws: {
284
+ /** Count */
285
+ count?: number | null;
286
+ /** Country */
287
+ country: string;
288
+ /** From Date */
289
+ from_date?: string | null;
290
+ /** Jurisdiction */
291
+ jurisdiction?: string | null;
292
+ /** Page */
293
+ page: number;
294
+ /** Per Page */
295
+ per_page: number;
296
+ /** Query */
297
+ query?: string | null;
298
+ /** Results */
299
+ results: components["schemas"]["LawSearchResult"][];
300
+ /** Sort */
301
+ sort?: string | null;
302
+ /** To Date */
303
+ to_date?: string | null;
304
+ /** Total */
305
+ total: number;
306
+ };
307
+ /** Reform */
308
+ Reform: {
309
+ /** Articles Affected */
310
+ articles_affected?: string | null;
311
+ /** Date */
312
+ date: string;
313
+ /** Source Id */
314
+ source_id?: string | null;
315
+ };
316
+ /** ReformsResponse */
317
+ ReformsResponse: {
318
+ /** Law Id */
319
+ law_id: string;
320
+ /** Limit */
321
+ limit: number;
322
+ /** Offset */
323
+ offset: number;
324
+ /** Reforms */
325
+ reforms: components["schemas"]["Reform"][];
326
+ /** Total */
327
+ total: number;
328
+ };
329
+ /** StatsResponse */
330
+ StatsResponse: {
331
+ /** Country */
332
+ country: string;
333
+ /** Jurisdiction */
334
+ jurisdiction?: string | null;
335
+ /** Law Types */
336
+ law_types: string[];
337
+ /** Most Reformed Laws */
338
+ most_reformed_laws: {
339
+ [key: string]: unknown;
340
+ }[];
341
+ /** Reform Activity By Year */
342
+ reform_activity_by_year: {
343
+ [key: string]: unknown;
344
+ }[];
345
+ };
346
+ /** ValidationError */
347
+ ValidationError: {
348
+ /** Context */
349
+ ctx?: Record<string, never>;
350
+ /** Input */
351
+ input?: unknown;
352
+ /** Location */
353
+ loc: (string | number)[];
354
+ /** Message */
355
+ msg: string;
356
+ /** Error Type */
357
+ type: string;
358
+ };
359
+ /** WebhookEndpointCreate */
360
+ WebhookEndpointCreate: {
361
+ /** Countries */
362
+ countries?: string[] | null;
363
+ /**
364
+ * Description
365
+ * @default
366
+ */
367
+ description: string;
368
+ /** Event Types */
369
+ event_types: string[];
370
+ /** Url */
371
+ url: string;
372
+ };
373
+ /** WebhookEndpointUpdate */
374
+ WebhookEndpointUpdate: {
375
+ /** Countries */
376
+ countries?: string[] | null;
377
+ /** Description */
378
+ description?: string | null;
379
+ /** Enabled */
380
+ enabled?: boolean | null;
381
+ /** Event Types */
382
+ event_types?: string[] | null;
383
+ /** Url */
384
+ url?: string | null;
385
+ };
386
+ };
387
+ responses: never;
388
+ parameters: never;
389
+ requestBodies: never;
390
+ headers: never;
391
+ pathItems: never;
392
+ }
393
+
394
+ /**
395
+ * Public response and request types.
396
+ *
397
+ * The underlying schemas live in `generated.ts` (produced by
398
+ * `openapi-typescript` from `../openapi-sdk.json`). This file re-exports
399
+ * the curated subset the SDK user interacts with, plus a few hand-written
400
+ * helper types where OpenAPI generation is awkward.
401
+ *
402
+ * We preserve the server's `snake_case` field names on response objects
403
+ * to match the wire format byte-for-byte — same philosophy as the Python
404
+ * SDK, which keeps Pydantic aliases and populates fields from the raw
405
+ * JSON directly.
406
+ */
407
+
408
+ type CountryInfo = components["schemas"]["CountryInfo"];
409
+ type JurisdictionInfo = components["schemas"]["JurisdictionInfo"];
410
+ type Commit = components["schemas"]["Commit"];
411
+ type CommitsResponse = components["schemas"]["CommitsResponse"];
412
+ type LawAtCommitResponse = components["schemas"]["LawAtCommitResponse"];
413
+ type LawDetail = components["schemas"]["LawDetail"];
414
+ type LawMeta = components["schemas"]["LawMeta"];
415
+ type LawSearchResult = components["schemas"]["LawSearchResult"];
416
+ type PaginatedLaws = components["schemas"]["PaginatedLaws"];
417
+ type Reform = components["schemas"]["Reform"];
418
+ type ReformsResponse = components["schemas"]["ReformsResponse"];
419
+ type StatsResponse = components["schemas"]["StatsResponse"];
420
+ type ApiValidationError = components["schemas"]["ValidationError"];
421
+ type HTTPValidationError = components["schemas"]["HTTPValidationError"];
422
+ type WebhookEndpointCreate = components["schemas"]["WebhookEndpointCreate"];
423
+ type WebhookEndpointUpdate = components["schemas"]["WebhookEndpointUpdate"];
424
+ /**
425
+ * Law listing sort options.
426
+ *
427
+ * The OpenAPI schema types this as `string` but the server enforces a
428
+ * specific allowed set. We keep it open (`string`) to tolerate server
429
+ * additions without bumping the SDK, but document the current values.
430
+ */
431
+ type LawSort = "publication_date" | "-publication_date" | "last_updated" | "-last_updated" | "title" | "-title" | (string & {});
432
+ /** Options shared by `laws.list` and `laws.iter` filter surfaces. */
433
+ interface LawFilterOptions {
434
+ lawType?: string | string[];
435
+ year?: number;
436
+ status?: string;
437
+ jurisdiction?: string;
438
+ fromDate?: string;
439
+ toDate?: string;
440
+ sort?: LawSort;
441
+ }
442
+ interface LawListOptions extends LawFilterOptions {
443
+ page?: number;
444
+ perPage?: number;
445
+ }
446
+ interface LawSearchOptions extends LawFilterOptions {
447
+ page?: number;
448
+ perPage?: number;
449
+ }
450
+ interface LawIterOptions extends LawFilterOptions {
451
+ perPage?: number;
452
+ limit?: number;
453
+ }
454
+ interface ReformListOptions {
455
+ limit?: number;
456
+ offset?: number;
457
+ }
458
+ interface ReformIterOptions {
459
+ batch?: number;
460
+ limit?: number;
461
+ }
462
+ interface StatsOptions {
463
+ jurisdiction?: string;
464
+ }
465
+ /** Webhook endpoint CRUD types — server-side response. */
466
+ interface WebhookEndpoint {
467
+ id: number;
468
+ url: string;
469
+ event_types: string[];
470
+ countries?: string[] | null;
471
+ description?: string;
472
+ enabled?: boolean;
473
+ created_at?: string;
474
+ /** Only populated on creation, never on list/retrieve. */
475
+ secret?: string;
476
+ [key: string]: unknown;
477
+ }
478
+ interface WebhookDeliveriesPage {
479
+ total: number;
480
+ deliveries: Array<Record<string, unknown>>;
481
+ [key: string]: unknown;
482
+ }
483
+ interface WebhookDelivery {
484
+ id: number;
485
+ status: string;
486
+ [key: string]: unknown;
487
+ }
488
+ type WebhookDeliveryStatus = "failed" | "success" | "pending";
489
+ interface WebhookCreateOptions {
490
+ url: string;
491
+ eventTypes: string[];
492
+ countries?: string[];
493
+ description?: string;
494
+ }
495
+ interface WebhookUpdateOptions {
496
+ url?: string;
497
+ eventTypes?: string[];
498
+ countries?: string[];
499
+ description?: string;
500
+ enabled?: boolean;
501
+ }
502
+ interface WebhookDeliveriesOptions {
503
+ page?: number;
504
+ status?: WebhookDeliveryStatus;
505
+ }
506
+
507
+ /** `/api/v1/countries` — list every country the API serves. */
508
+
509
+ declare class Countries {
510
+ private readonly client;
511
+ constructor(client: Legalize);
512
+ /** Return every country the API serves, with law counts. */
513
+ list(options?: {
514
+ signal?: AbortSignal;
515
+ }): Promise<CountryInfo[]>;
516
+ }
517
+
518
+ /** `/api/v1/{country}/jurisdictions` — regions/states within a country. */
519
+
520
+ declare class Jurisdictions {
521
+ private readonly client;
522
+ constructor(client: Legalize);
523
+ /** List jurisdictions for a country (e.g. Spain's comunidades). */
524
+ list(country: string, options?: {
525
+ signal?: AbortSignal;
526
+ }): Promise<JurisdictionInfo[]>;
527
+ }
528
+
529
+ /**
530
+ * `/api/v1/{country}/laws` and sub-resources.
531
+ *
532
+ * Covers:
533
+ * - `list` / `search` — listing vs. full-text search
534
+ * - `iter` / `searchIter` — auto-paginated async iterators
535
+ * - `retrieve` — full law with Markdown content
536
+ * - `meta` — metadata only (fast, no GitHub fetch)
537
+ * - `commits` — git commit history
538
+ * - `atCommit` — time-travel to a specific SHA
539
+ */
540
+
541
+ declare class Laws {
542
+ private readonly client;
543
+ constructor(client: Legalize);
544
+ /** Return a single page of laws for a country. */
545
+ list(country: string, options?: LawListOptions & {
546
+ signal?: AbortSignal;
547
+ }): Promise<PaginatedLaws>;
548
+ /** Full-text search for laws. `q` is required. */
549
+ search(country: string, q: string, options?: LawSearchOptions & {
550
+ signal?: AbortSignal;
551
+ }): Promise<PaginatedLaws>;
552
+ /** Auto-paginate across every matching law. */
553
+ iter(country: string, options?: LawIterOptions & {
554
+ signal?: AbortSignal;
555
+ }): AsyncIterableIterator<LawSearchResult>;
556
+ /** Auto-paginate across every match of a full-text search. */
557
+ searchIter(country: string, q: string, options?: LawIterOptions & {
558
+ signal?: AbortSignal;
559
+ }): AsyncIterableIterator<LawSearchResult>;
560
+ /** Fetch the full law including Markdown content. */
561
+ retrieve(country: string, lawId: string, options?: {
562
+ signal?: AbortSignal;
563
+ }): Promise<LawDetail>;
564
+ /** Fetch only the law metadata (no content). */
565
+ meta(country: string, lawId: string, options?: {
566
+ signal?: AbortSignal;
567
+ }): Promise<LawMeta>;
568
+ /** Git commit history for the law. */
569
+ commits(country: string, lawId: string, options?: {
570
+ signal?: AbortSignal;
571
+ }): Promise<CommitsResponse>;
572
+ /** Return the law's full text at a specific historical version. */
573
+ atCommit(country: string, lawId: string, sha: string, options?: {
574
+ signal?: AbortSignal;
575
+ }): Promise<LawAtCommitResponse>;
576
+ }
577
+
578
+ /** `/api/v1/{country}/law-types` — law types per country. */
579
+
580
+ declare class LawTypes {
581
+ private readonly client;
582
+ constructor(client: Legalize);
583
+ /** List law type identifiers (e.g. `["constitucion", "ley", "real_decreto"]`). */
584
+ list(country: string, options?: {
585
+ signal?: AbortSignal;
586
+ }): Promise<string[]>;
587
+ }
588
+
589
+ /** `/api/v1/{country}/laws/{law_id}/reforms` — reform history. */
590
+
591
+ declare class Reforms {
592
+ private readonly client;
593
+ constructor(client: Legalize);
594
+ /** Return a single page of reforms for a law. */
595
+ list(country: string, lawId: string, options?: ReformListOptions & {
596
+ signal?: AbortSignal;
597
+ }): Promise<ReformsResponse>;
598
+ /** Auto-paginate across every reform for a law. */
599
+ iter(country: string, lawId: string, options?: ReformIterOptions & {
600
+ signal?: AbortSignal;
601
+ }): AsyncIterableIterator<Reform>;
602
+ }
603
+
604
+ /** `/api/v1/{country}/stats` — aggregate statistics for a country. */
605
+
606
+ declare class Stats {
607
+ private readonly client;
608
+ constructor(client: Legalize);
609
+ /** Return aggregate stats for a country (and optionally a jurisdiction). */
610
+ retrieve(country: string, options?: StatsOptions & {
611
+ signal?: AbortSignal;
612
+ }): Promise<StatsResponse>;
613
+ }
614
+
615
+ /**
616
+ * `/api/v1/webhooks` — endpoint management + delivery history + test ping.
617
+ *
618
+ * The management endpoints (this file) require Pro+ tier. The webhook
619
+ * signature verification utility lives in `webhooks.ts` at the package
620
+ * root — it runs on the recipient's server and doesn't touch this API.
621
+ */
622
+
623
+ declare class Webhooks {
624
+ private readonly client;
625
+ constructor(client: Legalize);
626
+ /** Create a webhook endpoint. Returns the signing secret ONCE. */
627
+ create(options: WebhookCreateOptions & {
628
+ signal?: AbortSignal;
629
+ }): Promise<WebhookEndpoint>;
630
+ /** List all webhook endpoints for the authenticated org. */
631
+ list(options?: {
632
+ signal?: AbortSignal;
633
+ }): Promise<WebhookEndpoint[]>;
634
+ /** Fetch a single endpoint by id. */
635
+ retrieve(endpointId: number, options?: {
636
+ signal?: AbortSignal;
637
+ }): Promise<WebhookEndpoint>;
638
+ /** Patch mutable fields on a webhook endpoint. */
639
+ update(endpointId: number, options: WebhookUpdateOptions & {
640
+ signal?: AbortSignal;
641
+ }): Promise<WebhookEndpoint>;
642
+ /** Delete a webhook endpoint. */
643
+ delete(endpointId: number, options?: {
644
+ signal?: AbortSignal;
645
+ }): Promise<Record<string, unknown>>;
646
+ /** List delivery attempts for an endpoint, optionally filtered by status. */
647
+ deliveries(endpointId: number, options?: WebhookDeliveriesOptions & {
648
+ signal?: AbortSignal;
649
+ }): Promise<WebhookDeliveriesPage>;
650
+ /** Retry a failed delivery. */
651
+ retry(endpointId: number, deliveryId: number, options?: {
652
+ signal?: AbortSignal;
653
+ }): Promise<WebhookDelivery>;
654
+ /** Send a `test.ping` event to verify the endpoint is reachable. */
655
+ test(endpointId: number, options?: {
656
+ signal?: AbortSignal;
657
+ }): Promise<WebhookDelivery>;
658
+ }
659
+
660
+ /**
661
+ * HTTP client core — the `Legalize` class.
662
+ *
663
+ * Wraps the built-in `fetch` with:
664
+ * - auth + identifying headers on every request
665
+ * - query-param cleanup matching the Python reference
666
+ * - retry policy with exponential backoff (see `retry.ts`)
667
+ * - error hierarchy mapping (see `errors.ts`)
668
+ * - per-request AbortSignal support
669
+ * - `lastResponse` populated on success AND error, for inspecting
670
+ * rate-limit headers and X-Request-Id after failures
671
+ *
672
+ * Design notes:
673
+ * - No runtime deps. Node 20+ ships `fetch`, `AbortController`, and
674
+ * `crypto.webcrypto` — we use only those.
675
+ * - `follow_redirects` is effectively false: the server doesn't
676
+ * redirect, and silently following one would hide misconfigurations.
677
+ * `fetch` supports "manual" redirect mode so we use it.
678
+ */
679
+
680
+ /** Compose the User-Agent the SDK sends with every request. */
681
+ declare function defaultUserAgent(): string;
682
+ /** The fetch implementation the client uses. Global by default; overridable. */
683
+ type FetchImpl = typeof fetch;
684
+ interface LegalizeOptions {
685
+ apiKey?: string;
686
+ baseUrl?: string;
687
+ apiVersion?: string;
688
+ /** Timeout in milliseconds. Default 30000. */
689
+ timeout?: number;
690
+ maxRetries?: number;
691
+ retry?: RetryPolicy;
692
+ defaultHeaders?: Record<string, string>;
693
+ /** Override fetch — primarily for tests. */
694
+ fetch?: FetchImpl;
695
+ }
696
+ interface RequestOptions {
697
+ params?: Record<string, unknown>;
698
+ json?: unknown;
699
+ extraHeaders?: Record<string, string>;
700
+ signal?: AbortSignal;
701
+ /**
702
+ * Override the retry policy's idempotency check for this call.
703
+ * POST/PATCH are NOT retried by default.
704
+ */
705
+ idempotencyKey?: string;
706
+ }
707
+ /**
708
+ * Synchronous-API, promise-based client for the Legalize API.
709
+ *
710
+ * Example:
711
+ *
712
+ * import { Legalize } from "@legalize-dev/sdk";
713
+ *
714
+ * const client = new Legalize({ apiKey: "leg_..." });
715
+ * const countries = await client.countries.list();
716
+ *
717
+ * Use `await using` (TS 5.2+) for deterministic cleanup:
718
+ *
719
+ * await using client = new Legalize({ apiKey: "leg_..." });
720
+ */
721
+ declare class Legalize {
722
+ readonly countries: Countries;
723
+ readonly jurisdictions: Jurisdictions;
724
+ readonly lawTypes: LawTypes;
725
+ readonly laws: Laws;
726
+ readonly reforms: Reforms;
727
+ readonly stats: Stats;
728
+ readonly webhooks: Webhooks;
729
+ /** Exposed for tests; treat as private otherwise. */
730
+ readonly _apiKey: string;
731
+ readonly _baseUrl: string;
732
+ readonly _apiVersion: string;
733
+ readonly _headers: Record<string, string>;
734
+ private readonly _timeout;
735
+ private readonly _retry;
736
+ private readonly _fetch;
737
+ private _lastResponse;
738
+ constructor(options?: LegalizeOptions);
739
+ /** The raw HTTP response from the most recent request, or null. */
740
+ get lastResponse(): Response | null;
741
+ /**
742
+ * Execute a request and return the parsed JSON body (or null for 204).
743
+ *
744
+ * Throws an APIError subclass on non-2xx responses (after retries),
745
+ * APITimeoutError on timeout, or APIConnectionError on transport
746
+ * failure.
747
+ */
748
+ request<T = unknown>(method: string, path: string, options?: RequestOptions): Promise<T>;
749
+ /** Release any resources held by the client. Kept for API symmetry. */
750
+ close(): Promise<void>;
751
+ /** TS 5.2+ `using` / `await using` support. */
752
+ [Symbol.asyncDispose](): Promise<void>;
753
+ private buildUrl;
754
+ private sendOnce;
755
+ }
756
+ /**
757
+ * Serialize query params matching the Python reference:
758
+ * - undefined/null → dropped.
759
+ * - booleans → "true" / "false".
760
+ * - arrays → comma-joined; empty arrays dropped.
761
+ * - everything else → String(value).
762
+ */
763
+ declare function buildQueryString(params: Record<string, unknown>): string;
764
+
765
+ /**
766
+ * Error hierarchy for the Legalize SDK.
767
+ *
768
+ * The server returns three response shapes:
769
+ *
770
+ * 1. Structured dict from the API layer:
771
+ * { "detail": { "error": "quota_exceeded", "message": "...", "retry_after": 3600, ... } }
772
+ *
773
+ * 2. FastAPI validation errors (422):
774
+ * { "detail": [{ "loc": [...], "msg": "...", "type": "..." }, ...] }
775
+ *
776
+ * 3. Plain string detail for simple 404/400:
777
+ * { "detail": "Law not found: xyz" }
778
+ *
779
+ * `APIError.fromResponse` normalizes all three into an instance of the
780
+ * most specific subclass, keeping the raw body on `.body` and the parsed
781
+ * payload on `.data`.
782
+ */
783
+ /** Base error for everything the SDK raises. */
784
+ declare class LegalizeError extends Error {
785
+ readonly name: string;
786
+ constructor(message: string, options?: {
787
+ cause?: unknown;
788
+ });
789
+ }
790
+ interface APIErrorOptions {
791
+ message: string;
792
+ statusCode?: number;
793
+ code?: string | undefined;
794
+ body?: unknown;
795
+ data?: unknown;
796
+ requestId?: string | undefined;
797
+ response?: Response | undefined;
798
+ cause?: unknown;
799
+ }
800
+ /** Any non-2xx HTTP response. */
801
+ declare class APIError extends LegalizeError {
802
+ readonly name: string;
803
+ readonly statusCode: number | undefined;
804
+ readonly code: string | undefined;
805
+ readonly body: unknown;
806
+ readonly data: unknown;
807
+ readonly requestId: string | undefined;
808
+ readonly response: Response | undefined;
809
+ constructor(options: APIErrorOptions);
810
+ toString(): string;
811
+ /**
812
+ * Build the most specific APIError subclass for a response.
813
+ *
814
+ * `body` is the raw bytes of the response body (or the parsed JSON
815
+ * object if already consumed). `data` is the parsed JSON, which
816
+ * drives error-code dispatch. Reads `X-Request-Id` for support.
817
+ */
818
+ static fromResponse(response: Response, body: unknown, data?: unknown): APIError;
819
+ }
820
+ declare class AuthenticationError extends APIError {
821
+ readonly name = "AuthenticationError";
822
+ }
823
+ declare class ForbiddenError extends APIError {
824
+ readonly name = "ForbiddenError";
825
+ }
826
+ declare class NotFoundError extends APIError {
827
+ readonly name = "NotFoundError";
828
+ }
829
+ declare class InvalidRequestError extends APIError {
830
+ readonly name = "InvalidRequestError";
831
+ }
832
+ declare class ValidationError extends APIError {
833
+ readonly name = "ValidationError";
834
+ readonly errors: Array<Record<string, unknown>>;
835
+ }
836
+ declare class RateLimitError extends APIError {
837
+ readonly name = "RateLimitError";
838
+ readonly retryAfter: number | undefined;
839
+ readonly limit: number | undefined;
840
+ }
841
+ declare class ServerError extends APIError {
842
+ readonly name: string;
843
+ }
844
+ declare class ServiceUnavailableError extends ServerError {
845
+ readonly name: string;
846
+ }
847
+ /** Transport failure, no response. */
848
+ declare class APIConnectionError extends LegalizeError {
849
+ readonly name: string;
850
+ }
851
+ /** Request exceeded timeout. */
852
+ declare class APITimeoutError extends APIConnectionError {
853
+ readonly name: string;
854
+ }
855
+ type WebhookVerificationReason = "missing_header" | "bad_timestamp" | "timestamp_outside_tolerance" | "no_valid_signature" | "bad_signature";
856
+ /**
857
+ * Raised by Webhook.verify on any signature or timestamp failure.
858
+ *
859
+ * The message is intentionally generic so that an attacker probing the
860
+ * endpoint can't distinguish "bad signature" from "stale timestamp". The
861
+ * specific reason is available on `.reason` for server-side logging.
862
+ */
863
+ declare class WebhookVerificationError extends LegalizeError {
864
+ readonly name: string;
865
+ readonly reason: WebhookVerificationReason;
866
+ constructor(reason: WebhookVerificationReason);
867
+ }
868
+
869
+ /**
870
+ * Pagination helpers — async iterators over page- and offset-based endpoints.
871
+ *
872
+ * The Legalize API uses two pagination styles:
873
+ *
874
+ * - page + perPage — laws list, webhook deliveries
875
+ * - limit + offset — reforms
876
+ *
877
+ * Each paginated response carries `total` (true match count). That lets
878
+ * iterators terminate without inferring end-of-stream.
879
+ *
880
+ * The iterators here are transport-agnostic: they delegate fetching to
881
+ * a caller-supplied async callback.
882
+ */
883
+ declare const PAGE_MAX = 100;
884
+ /** Async iterator over a page-based endpoint (page + perPage). */
885
+ declare class PageIterator<T> implements AsyncIterableIterator<T> {
886
+ private readonly fetchPage;
887
+ private readonly perPage;
888
+ private readonly limit;
889
+ private page;
890
+ private yielded;
891
+ private buffer;
892
+ private bufferIdx;
893
+ private total;
894
+ private shortPage;
895
+ constructor(fetchPage: (page: number, perPage: number) => Promise<[T[], number]>, options?: {
896
+ perPage?: number;
897
+ limit?: number;
898
+ startPage?: number;
899
+ });
900
+ [Symbol.asyncIterator](): AsyncIterableIterator<T>;
901
+ next(): Promise<IteratorResult<T>>;
902
+ }
903
+ /** Async iterator over a limit/offset-based endpoint (reforms). */
904
+ declare class OffsetIterator<T> implements AsyncIterableIterator<T> {
905
+ private readonly fetchPage;
906
+ private readonly batch;
907
+ private readonly limit;
908
+ private offset;
909
+ private yielded;
910
+ private buffer;
911
+ private bufferIdx;
912
+ private done;
913
+ constructor(fetchPage: (limit: number, offset: number) => Promise<[T[], number]>, options?: {
914
+ batch?: number;
915
+ limit?: number;
916
+ startOffset?: number;
917
+ });
918
+ [Symbol.asyncIterator](): AsyncIterableIterator<T>;
919
+ next(): Promise<IteratorResult<T>>;
920
+ }
921
+
922
+ /**
923
+ * Webhook signature verification.
924
+ *
925
+ * Mirrors the server-side signer in the Legalize web backend. The scheme
926
+ * is Stripe-shaped:
927
+ *
928
+ * - Signed content: `${timestamp}.${rawJsonBody}` — RAW bytes, never
929
+ * a re-serialized JSON string. Reserialization will break the signature.
930
+ * - Algorithm: HMAC-SHA256, hex-encoded.
931
+ * - Header format: `v1=<hex>`. A future v2 scheme can coexist:
932
+ * `X-Legalize-Signature: v1=<sig1>,v2=<sig2>`.
933
+ * - Replay protection: reject if the header timestamp is more than
934
+ * `tolerance` seconds away from `now`. Default 300 seconds.
935
+ *
936
+ * Usage (Express):
937
+ *
938
+ * import express from "express";
939
+ * import { Webhook, WebhookVerificationError } from "@legalize-dev/sdk";
940
+ *
941
+ * app.post("/webhooks/legalize",
942
+ * express.raw({ type: "application/json" }),
943
+ * (req, res) => {
944
+ * try {
945
+ * const event = Webhook.verify({
946
+ * payload: req.body, // Buffer
947
+ * sigHeader: req.header("X-Legalize-Signature") ?? "",
948
+ * timestamp: req.header("X-Legalize-Timestamp") ?? "",
949
+ * secret: process.env.LEGALIZE_WHSEC!,
950
+ * });
951
+ * if (event.type === "law.updated") { ... }
952
+ * res.status(204).send();
953
+ * } catch (err) {
954
+ * res.status(400).send();
955
+ * }
956
+ * });
957
+ */
958
+ declare const DEFAULT_TOLERANCE_SECONDS = 300;
959
+ interface WebhookEvent {
960
+ /** Server-assigned event id (e.g. `evt_...`). */
961
+ readonly id: string;
962
+ /** Event type (`law.created`, `law.updated`, `law.repealed`, ...). */
963
+ readonly type: string;
964
+ /** Server-side timestamp (ISO-8601 string). */
965
+ readonly createdAt: string;
966
+ /** Event-specific payload body. */
967
+ readonly data: Record<string, unknown>;
968
+ /** The full decoded JSON body — useful for fields the SDK doesn't type. */
969
+ readonly raw: Record<string, unknown>;
970
+ }
971
+ interface WebhookVerifyOptions {
972
+ /** The raw request body bytes. Do NOT pass a re-serialized JSON string. */
973
+ payload: Buffer | Uint8Array | string;
974
+ /** The `X-Legalize-Signature` header value. May contain several `vN=<hex>` pairs. */
975
+ sigHeader: string;
976
+ /** The `X-Legalize-Timestamp` header value (Unix seconds, decimal string). */
977
+ timestamp: string;
978
+ /** The endpoint's signing secret. */
979
+ secret: string;
980
+ /** Seconds of clock skew accepted. Default 300. */
981
+ tolerance?: number;
982
+ /** Unit-test hook: override the reference wall clock (Unix seconds). */
983
+ now?: number;
984
+ }
985
+ declare class Webhook {
986
+ /** Default anti-replay tolerance. */
987
+ static readonly TOLERANCE = 300;
988
+ /** Compute the canonical `v1=<hex>` signature for (payload, timestamp). */
989
+ static computeSignature(secret: string, payload: Buffer | Uint8Array | string, timestamp: string): string;
990
+ /**
991
+ * Verify a webhook delivery and return the parsed event.
992
+ *
993
+ * Signature verification happens BEFORE JSON parsing, to protect the
994
+ * process from resource-exhaustion on unauthenticated bodies.
995
+ *
996
+ * Throws WebhookVerificationError on any failure. The `.reason`
997
+ * field identifies which check tripped for server-side logging,
998
+ * while the user-facing message stays generic.
999
+ */
1000
+ static verify(options: WebhookVerifyOptions): WebhookEvent;
1001
+ }
1002
+
1003
+ /**
1004
+ * SDK version — MUST be kept in sync with the "version" field in package.json.
1005
+ *
1006
+ * The Node runtime doesn't resolve JSON imports cleanly across ESM/CJS dual
1007
+ * output without extra wiring, so we duplicate the literal here. The test
1008
+ * suite asserts they match so drift is caught immediately.
1009
+ */
1010
+ declare const SDK_VERSION = "0.1.0";
1011
+
1012
+ export { APIConnectionError, APIError, type APIErrorOptions, APITimeoutError, type ApiValidationError, AuthenticationError, type Commit, type CommitsResponse, Countries, type CountryInfo, DEFAULT_API_VERSION, DEFAULT_BACKOFF_FACTOR, DEFAULT_BASE_URL, DEFAULT_INITIAL_DELAY, DEFAULT_MAX_DELAY, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, DEFAULT_TOLERANCE_SECONDS, type FetchImpl, ForbiddenError, type HTTPValidationError, IDEMPOTENT_METHODS, InvalidRequestError, type JurisdictionInfo, Jurisdictions, KEY_PREFIX, type LawAtCommitResponse, type LawDetail, type LawFilterOptions, type LawIterOptions, type LawListOptions, type LawMeta, type LawSearchOptions, type LawSearchResult, type LawSort, LawTypes, Laws, Legalize, LegalizeError, type LegalizeOptions, NotFoundError, OffsetIterator, PAGE_MAX, PageIterator, type PaginatedLaws, RETRY_STATUSES, RateLimitError, type Reform, type ReformIterOptions, type ReformListOptions, Reforms, type ReformsResponse, type RequestOptions, RetryPolicy, type RetryPolicyOptions, SDK_VERSION, ServerError, ServiceUnavailableError, Stats, type StatsOptions, type StatsResponse, ValidationError, Webhook, type WebhookCreateOptions, type WebhookDeliveriesOptions, type WebhookDeliveriesPage, type WebhookDelivery, type WebhookDeliveryStatus, type WebhookEndpoint, type WebhookEndpointCreate, type WebhookEndpointUpdate, type WebhookEvent, type WebhookUpdateOptions, WebhookVerificationError, type WebhookVerificationReason, type WebhookVerifyOptions, Webhooks, buildQueryString, defaultUserAgent, parseRetryAfter, resolveApiKey, resolveApiVersion, resolveBaseUrl, sleep };