npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@legiscore/sdk

v0.4.1

Published

Node SDK for the LegiScore partner API — legal opinion reports and government record searches.

Readme

@legiscore/sdk

Node SDK for the LegiScore partner API — legal opinion reports and government record searches on Indian property.

Node 20+. Zero runtime dependencies. ESM and CommonJS both work. Nothing is imported from node: at load time, so the same package runs unchanged in a Next.js 15 App Router app (Server Components, Route Handlers, Server Actions, Middleware), on Vercel Edge, in a Cloudflare Worker, in Deno and in Bun.

npm install @legiscore/sdk
import { LegiScore } from "@legiscore/sdk";        // ESM
const { LegiScore } = require("@legiscore/sdk");   // CommonJS

Quick start

import { LegiScore } from "@legiscore/sdk";

const client = new LegiScore({ apiKey: process.env.LEGISCORE_API_KEY });

const created = await client.createReport({
  propertyFocus: "Sy. No. 123, Example Village, Telangana",
  files: ["sale-deed.pdf", "encumbrance-certificate.pdf"],
});
const status = await client.waitForCase(created.case_id);

if (status.state === "completed") {
  const report = await client.reports.getCaseResult(created.case_id);
} else if (status.state === "awaiting_review") {
  const missing = await client.reports.getMissingDocuments(created.case_id);
  await client.reports.continueCase(created.case_id, { new_document_ids: [] });
}

Endpoints are grouped by module: client.core, client.reports, client.search, client.translate, client.extraction, client.webhooks. Every method takes an optional { signal } as its last argument, merged with the client timeout.

client.search is the government-record search product: its own credit balance, its own flat per-search price from getSearchCatalog, and its own host, which the client already points at. Submit a search, then poll getSearch until its status is succeeded, failed or cancelled, then pull each file with getSearchDocument, which resolves to bytes. Override the host with searchBaseUrl only if you have been told to.

Karnataka places go as the portal's own codes; resolve them with getSearchLookups (kaveri_* dims for the encumbrance certificate, bhoomi_* for RTC, Akarband and Village Map):

// Encumbrance certificate (Kaveri 2.0), 2004 onwards.
await client.search.submitSearch({
  state: 'karnataka',
  search_type: 'ec',
  params: {
    district: '1', taluk: '193', hobli: '1085', village: '28867', // Kudlu
    property_kind: 'agricultural', // or 'non_agricultural' + property_number_type + property_number
    survey_number: '178',
  },
});

// RTC / Pahani (Bhoomi): Bengaluru Rural / Nelamangala / Kasaba / Gollahalli, survey 59.
await client.search.submitSearch({
  state: 'karnataka',
  search_type: 'rtc',
  params: { district: '21', taluk: '1', hobli: '1', village: '44', survey_no: '59' },
});

A case can pause and wait for you — awaiting_review means it needs missing documents, a document review, or risk acknowledgements before it can finish. Each pause has a matching resume method. Polling with waitForCase is the fallback; configure a webhook and the transitions are pushed to you instead.

Your organisation can require every item at a pause to be actioned before the case may advance, which turns a resume into a 422 with the code PAUSE_GATE_UNMET. readPauseGateRefusal reads the reasons. At the document-review pause those reasons are findings: getDocumentReview returns them as review_findings, readReviewFindings parses them, and the ones a person accepted go back as document_review_annotations on the next submitDocumentReview. Never compute a fingerprint yourself, and never tick one in a loop.

Next.js 15

The API key is a server-side secret. Keep every call in a Route Handler, a Server Action or a Server Component — never in a Client Component.

Webhooks — app/api/legiscore/webhook/route.ts

import { verifyWebhook, InvalidSignature } from "@legiscore/sdk";

export async function POST(request: Request) {
  // The raw body, byte for byte. Parsing and re-serialising changes it and the check fails.
  const rawBody = await request.text();

  try {
    const event = await verifyWebhook(rawBody, request.headers, {
      secret: process.env.LEGISCORE_WEBHOOK_SECRET!,
    });
    if (event.isPause) await handlePause(event.caseId);
    return Response.json({ received: true });
  } catch (error) {
    if (error instanceof InvalidSignature) return new Response(null, { status: 400 });
    throw error;
  }
}

verifyWebhook is async — it uses Web Crypto, which is what makes it work on the Edge runtime. Deliveries older than five minutes are rejected as replays, and an empty secret fails closed rather than trusting the delivery.

A Server Action that opens a case

"use server";

import { LegiScore } from "@legiscore/sdk";

export async function createTitleReport(formData: FormData): Promise<string> {
  const client = new LegiScore({ apiKey: process.env.LEGISCORE_API_KEY });
  const file = formData.get("deed") as File;

  const { case_id } = await client.createReport({
    propertyFocus: formData.get("address") as string,
    files: [file], // a File from the form — no filesystem involved
  });
  return case_id;
}

Edge runtime

Everything works on Edge except one thing: passing a filesystem path to an upload. That branch needs node:fs, so on Edge pass a Blob, a File, a Uint8Array/ArrayBuffer, or { data, fileName, contentType } instead. Uploads accept all five forms on every runtime.

Custom fetch

Pass your own fetch to pick up Next.js caching and revalidation semantics, or to instrument the calls:

const client = new LegiScore({
  apiKey: process.env.LEGISCORE_API_KEY,
  fetch: (url, init) => fetch(url, { ...init, next: { revalidate: 60 } }),
});

Errors and retries

Failures throw LegiScoreError, which carries .status, .body and, when the API sent one, .code — a stable string such as insufficient_credits. Branch on .code, not on the message.

Do not log error.body raw. It is the API's own response, which on a case route contains property, borrower and document details. Log error.status and error.message. The API key and the request headers are never attached to an error.

Retries are deliberately asymmetric, because replaying a write can cost money:

| Call | Replayed on | |---|---| | Reads (GET) | 429, 502, 503, 504 | | Writes carrying an Idempotency-Key (case creation, upload completion) | 429, 502, 503, 504 | | Every other write | 429 only — the one status that proves the server refused the request before running it | | rotateWebhookSecret | never |

Retry-After is honoured when the server sends one, clamped to 60 seconds; otherwise the backoff is exponential with jitter. baseUrl and searchBaseUrl must both be https (or localhost).

Redirects are never followed on an authenticated request — following one would forward your key to wherever Location points, because fetch carries custom headers across a cross-origin redirect even though it drops Authorization. getSearchDocument is the one route that answers with a redirect by design: the SDK reads the signed URL and fetches it on a second request with no key attached, and refuses a target that is not https.

Verifying webhooks outside Next.js

import { verifyWebhook, InvalidSignature } from "@legiscore/sdk";

try {
  const event = await verifyWebhook(rawBody, request.headers, { secret: WEBHOOK_SECRET });
  if (event.isPause) await handlePause(event.caseId);
} catch (error) {
  if (error instanceof InvalidSignature) return reply.code(400).send();
  throw error;
}

rawBody may be a string, a Uint8Array or an ArrayBuffer, and must be the raw request bytes. In Express, that means express.raw({ type: "application/json" }) on this route.

Setting the webhook up. Either client.webhooks.createWebhook({ name, url, events }) or the LegiScore dashboard with auth type HMAC. The secret comes back exactly once, in the create response as data.secret, and is never returned by a later list or get — that value is WEBHOOK_SECRET. If you lose it, or if we registered the webhook for you, mint a fresh one with client.webhooks.rotateWebhookSecret(webhookId) and update your verifier. A webhook created with any other auth type sends no X-LegiScore-Signature at all, so verification will reject every delivery.

Leave "Allowed domains" empty on a server-side key. A non-empty list is checked against the Origin or Referer header, which a server-to-server call does not send, so every request returns 403 no matter how valid the key is.

Checking the connection

const report = await client.checkConnection();   // resolves, never throws
if (!report.ok) throw new Error(report.problem);
if (!report.search.ok) console.warn(`Searches unavailable: ${report.search.problem}`);

The two products run on two hosts and fail independently, usually because a network allows one and not the other. ok is the reports host; search.ok is the search host. Gate on the one you are about to use.

License

Apache-2.0. Copyright 2026 LawyerDesk Advocacy Pvt Ltd.