@legalize-dev/sdk 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning][semver].
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.2.0] — 2026-06-17
13
+
14
+ ### Added
15
+
16
+ - `requestRaw(method, path, options?)` on `Legalize` — the low-level
17
+ escape hatch for content negotiation. It fetches any endpoint in a
18
+ non-JSON wire format and returns a `RawResponse` (`.statusCode`,
19
+ `.content` as raw bytes, `.text`, `.contentType`, `.headers`) without
20
+ JSON-decoding. `options.format` controls the `Accept` header:
21
+ `"xml"` (the default) sends `application/xml`, `"json"` sends
22
+ `application/json`, and any other value is used verbatim as the media
23
+ type. Same retry policy, `lastResponse` population, and typed error
24
+ hierarchy as `request`.
25
+ - `RawResponse` type, exported from the package, with a `.json()`
26
+ convenience parser. The SDK ships no XML parser and keeps zero runtime
27
+ dependencies — parse `.text`/`.content` with your own library.
28
+ - `RequestRawOptions` type and the `formatToAccept` helper are exported.
29
+
30
+ The typed resource methods still return JSON-parsed models; XML is opt-in
31
+ per call via `requestRaw`. See the "Response formats" API docs.
32
+
12
33
  ## [0.1.0] — 2026-04-20
13
34
 
14
35
  ### Added
package/README.md CHANGED
@@ -65,6 +65,31 @@ const past = await client.laws.atCommit("es", "ley_organica_3_2018", oldest);
65
65
  console.log(past.content_md); // Markdown at that revision
66
66
  ```
67
67
 
68
+ ### XML (and other raw formats)
69
+
70
+ The typed methods always return JSON-parsed models. When your app speaks
71
+ XML, use `requestRaw` to fetch any endpoint in another wire format via
72
+ content negotiation — it sets `Accept` and hands you the body untouched:
73
+
74
+ ```ts
75
+ const res = await client.requestRaw("GET", "/api/v1/es/laws/BOE-A-1978-31229");
76
+ res.contentType; // "application/xml; charset=utf-8"
77
+ const xmlText = res.text; // the raw XML string
78
+ res.content; // the raw bytes (Uint8Array)
79
+
80
+ // format: "json" or any explicit media type works the same way:
81
+ const data = (
82
+ await client.requestRaw("GET", "/api/v1/countries", { format: "json" })
83
+ ).json();
84
+ ```
85
+
86
+ `requestRaw` defaults to `format: "xml"`. The SDK has **zero runtime
87
+ dependencies and ships no XML parser** — parse `res.text` (or
88
+ `res.content`) with your own library (`fast-xml-parser`,
89
+ `@xmldom/xmldom`, …). Errors raise the same typed exceptions as the JSON
90
+ methods (the error body is in the negotiated format). See the
91
+ [Response formats](https://legalize.dev/docs/formats) docs.
92
+
68
93
  ### Abort + timeout
69
94
 
70
95
  Every method accepts a standard `AbortSignal`:
package/dist/index.cjs CHANGED
@@ -744,7 +744,7 @@ var Webhooks = class {
744
744
  };
745
745
 
746
746
  // src/version.ts
747
- var SDK_VERSION = "0.1.0";
747
+ var SDK_VERSION = "0.2.0";
748
748
 
749
749
  // src/client.ts
750
750
  function defaultUserAgent() {
@@ -755,6 +755,22 @@ function stripTrailingSlashes(s) {
755
755
  while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
756
756
  return s.slice(0, end);
757
757
  }
758
+ var FORMAT_ALIASES = {
759
+ xml: "application/xml",
760
+ json: "application/json"
761
+ };
762
+ function formatToAccept(format) {
763
+ const key = (format ?? "").trim().toLowerCase();
764
+ if (!key) return "application/xml";
765
+ return FORMAT_ALIASES[key] ?? format;
766
+ }
767
+ function headersToRecord(headers) {
768
+ const out = {};
769
+ headers.forEach((value, key) => {
770
+ out[key] = value;
771
+ });
772
+ return out;
773
+ }
758
774
  var Legalize = class {
759
775
  countries;
760
776
  jurisdictions;
@@ -801,19 +817,100 @@ var Legalize = class {
801
817
  */
802
818
  async request(method, path, options = {}) {
803
819
  const upperMethod = method.toUpperCase();
804
- const url = this.buildUrl(path, options.params);
805
820
  const headers = { ...this._headers };
806
821
  if (options.extraHeaders) Object.assign(headers, options.extraHeaders);
807
822
  if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
808
823
  const hasJson = options.json !== void 0;
809
824
  if (hasJson) headers["Content-Type"] = "application/json";
825
+ const response = await this.sendWithRetry(upperMethod, path, headers, options, hasJson);
826
+ this._lastResponse = response;
827
+ if (response.status === 204) return null;
828
+ const text = await response.text();
829
+ if (!text) return null;
830
+ try {
831
+ return JSON.parse(text);
832
+ } catch (err) {
833
+ throw new APIError({
834
+ message: "Server returned non-JSON body",
835
+ statusCode: response.status,
836
+ body: text,
837
+ response,
838
+ cause: err
839
+ });
840
+ }
841
+ }
842
+ /**
843
+ * Execute a request and return the raw, non-JSON-decoded body.
844
+ *
845
+ * The escape hatch for content negotiation: the typed resource methods
846
+ * always return JSON models, but `requestRaw` lets you fetch any
847
+ * endpoint in another wire format. `format` controls the `Accept`
848
+ * header — `"xml"` (the default) requests `application/xml`, `"json"`
849
+ * requests `application/json`, and any other value is sent verbatim as
850
+ * the media type.
851
+ *
852
+ * The SDK has zero runtime deps and ships no XML parser — parse the
853
+ * returned `.text` (or `.content`) with your own library.
854
+ *
855
+ * Example:
856
+ *
857
+ * const res = await client.requestRaw("GET", "/api/v1/es/laws/BOE-A-1978-31229");
858
+ * res.contentType; // "application/xml; charset=utf-8"
859
+ * const xmlText = res.text; // already application/xml
860
+ *
861
+ * Throws the same APIError subclasses as {@link request} on a non-2xx
862
+ * response; the error body is in whatever format you negotiated.
863
+ */
864
+ async requestRaw(method, path, options = {}) {
865
+ const upperMethod = method.toUpperCase();
866
+ const headers = { ...this._headers };
867
+ headers["Accept"] = formatToAccept(options.format);
868
+ if (options.extraHeaders) Object.assign(headers, options.extraHeaders);
869
+ if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
870
+ const response = await this.sendWithRetry(upperMethod, path, headers, options, false);
871
+ this._lastResponse = response;
872
+ return rawFromResponse(response);
873
+ }
874
+ /** Release any resources held by the client. Kept for API symmetry. */
875
+ async close() {
876
+ }
877
+ /** TS 5.2+ `using` / `await using` support. */
878
+ async [Symbol.asyncDispose]() {
879
+ await this.close();
880
+ }
881
+ // ---- internals --------------------------------------------------------
882
+ buildUrl(path, params) {
883
+ let base;
884
+ if (path.startsWith("http://") || path.startsWith("https://")) {
885
+ base = path;
886
+ } else {
887
+ const p = path.startsWith("/") ? path : `/${path}`;
888
+ base = this._baseUrl + p;
889
+ }
890
+ if (!params) return base;
891
+ const query = buildQueryString(params);
892
+ if (!query) return base;
893
+ return base.includes("?") ? `${base}&${query}` : `${base}?${query}`;
894
+ }
895
+ /**
896
+ * Run the retry loop and return the raw 2xx `Response`.
897
+ *
898
+ * Shared by {@link request} (which then JSON-parses) and
899
+ * {@link requestRaw} (which builds a `RawResponse`). Applies the retry
900
+ * policy on transport errors and retryable statuses, populates
901
+ * `lastResponse` on the failing response before raising, and throws the
902
+ * typed `APIError` hierarchy on a final non-2xx. The returned response
903
+ * body is left unread for the caller to consume.
904
+ */
905
+ async sendWithRetry(method, path, headers, options, hasJson) {
906
+ const url = this.buildUrl(path, options.params);
810
907
  let attempt = 0;
811
908
  while (true) {
812
909
  let response;
813
910
  try {
814
- response = await this.sendOnce(url, upperMethod, headers, options, hasJson);
911
+ response = await this.sendOnce(url, method, headers, options, hasJson);
815
912
  } catch (err) {
816
- const shouldRetry2 = this._retry.shouldRetry(attempt, { method: upperMethod });
913
+ const shouldRetry2 = this._retry.shouldRetry(attempt, { method });
817
914
  if (!shouldRetry2) {
818
915
  throw wrapTransportError(err);
819
916
  }
@@ -823,25 +920,11 @@ var Legalize = class {
823
920
  continue;
824
921
  }
825
922
  if (response.status >= 200 && response.status < 300) {
826
- this._lastResponse = response;
827
- if (response.status === 204) return null;
828
- const text = await response.text();
829
- if (!text) return null;
830
- try {
831
- return JSON.parse(text);
832
- } catch (err) {
833
- throw new APIError({
834
- message: "Server returned non-JSON body",
835
- statusCode: response.status,
836
- body: text,
837
- response,
838
- cause: err
839
- });
840
- }
923
+ return response;
841
924
  }
842
925
  const shouldRetry = this._retry.shouldRetry(attempt, {
843
926
  status: response.status,
844
- method: upperMethod
927
+ method
845
928
  });
846
929
  if (!shouldRetry) {
847
930
  this._lastResponse = response;
@@ -857,27 +940,6 @@ var Legalize = class {
857
940
  attempt += 1;
858
941
  }
859
942
  }
860
- /** Release any resources held by the client. Kept for API symmetry. */
861
- async close() {
862
- }
863
- /** TS 5.2+ `using` / `await using` support. */
864
- async [Symbol.asyncDispose]() {
865
- await this.close();
866
- }
867
- // ---- internals --------------------------------------------------------
868
- buildUrl(path, params) {
869
- let base;
870
- if (path.startsWith("http://") || path.startsWith("https://")) {
871
- base = path;
872
- } else {
873
- const p = path.startsWith("/") ? path : `/${path}`;
874
- base = this._baseUrl + p;
875
- }
876
- if (!params) return base;
877
- const query = buildQueryString(params);
878
- if (!query) return base;
879
- return base.includes("?") ? `${base}&${query}` : `${base}?${query}`;
880
- }
881
943
  async sendOnce(url, method, headers, options, hasJson) {
882
944
  const controller = new AbortController();
883
945
  const timeoutMs = this._timeout;
@@ -955,6 +1017,21 @@ async function errorFromResponse(response) {
955
1017
  }
956
1018
  return APIError.fromResponse(response, text, data);
957
1019
  }
1020
+ async function rawFromResponse(response) {
1021
+ const buffer = await response.arrayBuffer();
1022
+ const content = new Uint8Array(buffer);
1023
+ const text = new TextDecoder().decode(content);
1024
+ return {
1025
+ statusCode: response.status,
1026
+ content,
1027
+ text,
1028
+ contentType: response.headers.get("content-type") ?? "",
1029
+ headers: headersToRecord(response.headers),
1030
+ json() {
1031
+ return JSON.parse(text);
1032
+ }
1033
+ };
1034
+ }
958
1035
  function wrapTransportError(err) {
959
1036
  if (err instanceof Error) {
960
1037
  const isAbort = err.name === "AbortError" || err.code === "ABORT_ERR";
@@ -1120,6 +1197,7 @@ exports.WebhookVerificationError = WebhookVerificationError;
1120
1197
  exports.Webhooks = Webhooks;
1121
1198
  exports.buildQueryString = buildQueryString;
1122
1199
  exports.defaultUserAgent = defaultUserAgent;
1200
+ exports.formatToAccept = formatToAccept;
1123
1201
  exports.parseRetryAfter = parseRetryAfter;
1124
1202
  exports.resolveApiKey = resolveApiKey;
1125
1203
  exports.resolveApiVersion = resolveApiVersion;