npm.io
0.3.0 • Published yesterday

@legalize-dev/sdk

Licence
MIT
Version
0.3.0
Deps
0
Size
368 kB
Vulns
0
Weekly
0
Stars
1

@legalize-dev/sdk

npm Node License: MIT

Official Node client for the Legalize API — legal texts as structured, versioned data.

npm install @legalize-dev/sdk
import { Legalize } from "@legalize-dev/sdk";

const client = new Legalize({ apiKey: "leg_..." });

for await (const law of client.laws.iter("es", { lawType: "ley_organica" })) {
  console.log(law.id, law.title);
}

Why this SDK

  • Typed end-to-end. Types are generated from the canonical OpenAPI spec and shipped in the package. strict TypeScript friendly.
  • Zero runtime dependencies. Uses Node's built-in fetch, AbortController, and crypto. No undici, axios, or node-fetch.
  • Retries with backoff built in. Honors Retry-After, handles 429/5xx, exponential delay with full jitter, and never auto-retries POST/PATCH by default (no duplicate mutations).
  • Webhook verification is a one-liner. Constant-time HMAC compare, 5-minute anti-replay window, clock-skew tolerant.
  • Works in any Node 20+ environment. Lambda, Cloud Functions, Fly, Railway, plain node, tsx, ts-node, etc.
  • ESM and CJS dual build. "type": "module" with a proper exports map so import and require both resolve cleanly.

Quick tour

// One page
const page = await client.laws.list("es", { page: 1, perPage: 50 });
console.log(page.total, page.results.length);

// Auto-paginated async iterator (fetches pages as needed)
for await (const law of client.laws.iter("es", { status: "vigente" })) {
  // ...
}

// Full-text search
const results = await client.laws.search("es", "protección de datos");
Time-travel

Every law has a git-tracked history. Retrieve it at any past revision:

const commits = await client.laws.commits("es", "ley_organica_3_2018");
const oldest = commits.commits[commits.commits.length - 1]!.sha;
const past = await client.laws.atCommit("es", "ley_organica_3_2018", oldest);
console.log(past.content_md); // Markdown at that revision

Or skip the SHA lookup entirely and ask by date:

const at = await client.laws.atDate("es", "ley_organica_3_2018", "2019-05-13");
console.log(at.sha, at.version_date); // which version answered, so you can cite it

The rule is published on or before the date, not in force on it.

XML (and other raw formats)

The typed methods always return JSON-parsed models. When your app speaks XML, use requestRaw to fetch any endpoint in another wire format via content negotiation — it sets Accept and hands you the body untouched:

const res = await client.requestRaw("GET", "/api/v1/es/laws/BOE-A-1978-31229");
res.contentType;     // "application/xml; charset=utf-8"
const xmlText = res.text;   // the raw XML string
res.content;         // the raw bytes (Uint8Array)

// format: "json" or any explicit media type works the same way:
const data = (
  await client.requestRaw("GET", "/api/v1/countries", { format: "json" })
).json();

requestRaw defaults to format: "xml". The SDK has zero runtime dependencies and ships no XML parser — parse res.text (or res.content) with your own library (fast-xml-parser, @xmldom/xmldom, …). Errors raise the same typed exceptions as the JSON methods (the error body is in the negotiated format). See the Response formats docs.

Abort + timeout

Every method accepts a standard AbortSignal:

const ac = new AbortController();
setTimeout(() => ac.abort(), 5000);
const out = await client.laws.list("es", { signal: ac.signal });

Per-request timeouts are configured on the client:

const client = new Legalize({ apiKey: "leg_...", timeout: 10_000 });
Webhooks

Verify a signed delivery in one call. Pass the raw request bytes — re-serialized JSON will NOT verify:

import express from "express";
import { Webhook, WebhookVerificationError } from "@legalize-dev/sdk";

app.post(
  "/webhooks/legalize",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = Webhook.verify({
        payload: req.body as Buffer,
        sigHeader: req.header("X-Legalize-Signature") ?? "",
        timestamp: req.header("X-Legalize-Timestamp") ?? "",
        secret: process.env.LEGALIZE_WHSEC!,
      });
      if (event.type === "law.updated") {
        // ...
      }
      res.status(204).send();
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        res.status(400).send();
        return;
      }
      throw err;
    }
  },
);

Working Express and Fastify receivers in examples/.

Configuration

Set the environment and just instantiate:

export LEGALIZE_API_KEY=leg_live_...
# Optional:
export LEGALIZE_BASE_URL=https://legalize.dev
export LEGALIZE_API_VERSION=v1
import { Legalize } from "@legalize-dev/sdk";

const client = new Legalize(); // picks everything up from the environment
Explicit
import { Legalize, RetryPolicy } from "@legalize-dev/sdk";

const client = new Legalize({
  apiKey: "leg_...",
  baseUrl: "https://legalize.dev",
  apiVersion: "v1",         // negotiated via Legalize-API-Version header
  timeout: 30_000,          // milliseconds
  retry: new RetryPolicy({
    maxRetries: 5,
    initialDelay: 0.5,
    maxDelay: 10,
  }),
  defaultHeaders: { "X-Correlation-Id": "..." },
});

Precedence: explicit argument > environment variable > built-in default. The full cross-SDK contract is documented in ENVIRONMENT.md.

Read rate-limit headers from the last response:

await client.countries.list();
const resp = client.lastResponse;
console.log(resp?.headers.get("X-RateLimit-Remaining"));

The same lastResponse is populated when a call fails, so you can inspect X-Request-Id and rate-limit headers after an error too:

try {
  await client.laws.retrieve("es", "unknown");
} catch (err) {
  console.error(client.lastResponse?.headers.get("X-Request-Id"));
  throw err;
}
Cleanup

The client holds no persistent connection pool (Node's fetch manages sockets globally), but close() is exported for API symmetry across SDKs. TS 5.2+ await using is supported:

{
  await using client = new Legalize({ apiKey: "leg_..." });
  await client.countries.list();
} // client auto-disposed

Retries

Auto-retries on 429 + 5xx + transport errors, with exponential backoff and full jitter. Retry-After (integer seconds or HTTP-date form) is honored when present, capped at maxDelay.

POST and PATCH are NOT retried by default — they may be non-idempotent. Opt in per-policy with retryNonIdempotent: true, or send an Idempotency-Key and wrap your own retry loop.

Errors

All errors inherit from LegalizeError. Catch the specific one you care about and let the rest bubble:

import {
  AuthenticationError,     // 401 — bad/missing key
  ForbiddenError,          // 403
  NotFoundError,           // 404
  InvalidRequestError,     // 400
  ValidationError,         // 422
  RateLimitError,          // 429 — retried automatically by default
  ServerError,             // 5xx
  ServiceUnavailableError, // 503
  APIConnectionError,      // network failure
  APITimeoutError,         // timeout
  WebhookVerificationError,
} from "@legalize-dev/sdk";

Every APIError exposes .statusCode, .code, .body, .response, and .requestId.

Compatibility

  • Node 20, 22, 24
  • Linux, macOS, Windows
  • ESM and CommonJS consumers (dual package)

License

MIT

Keywords