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

@lepsto/sdk

v0.6.0

Published

Client SDK for the Lepsto public edge — one credential, generated from the public catalog.

Readme

@lepsto/sdk

The client SDK for the Lepsto public edge (https://public.lepsto.com). One credential issued for your product reaches every Lepsto extension that product has published.

  • Zero runtime dependencies.
  • ESM and CommonJS, with types.
  • Node >= 20 and modern browsers (fetch comes from the platform).

Entry points

@lepsto/sdk publishes eight entry points. The root builds the whole catalog's client; the other seven are per-toolkit bundles, so importing one brings nothing else with it:

| subpath | what it is | |---|---| | @lepsto/sdk | the whole catalog in one client | | @lepsto/sdk/mail | Lepsto Mail | | @lepsto/sdk/realtime | Lepsto Realtime, browser and server-agnostic | | @lepsto/sdk/realtime/server | Realtime's server-credentialled half | | @lepsto/sdk/users | Lepsto Users | | @lepsto/sdk/users/react | the React bindings | | @lepsto/sdk/users/react/ui | the prebuilt React components | | @lepsto/sdk/users/server | Users' server-credentialled half |

A toolkit with no subpath of its own is reached through the root client, MCP or REST.

import { createRealtime } from '@lepsto/sdk/realtime';

const realtime = createRealtime({
  productId: 'prod_…',
  credential: { kind: 'publishableKey', value: 'lpk_…' },
});

await realtime.messages.publish({ channel: 'room:42', data: { hello: 'world' } });

The root entry (import { createLepsto } from '@lepsto/sdk') builds the whole catalog's client in one object. A subpath is what you want when bundle size matters, because the root materialises every toolkit eagerly.

…/server

A toolkit that has server-side operations publishes them separately:

import { createRealtimeServer } from '@lepsto/sdk/realtime/server';

These carry server credentials and Node-only code. They cannot be bundled for a browser: the package declares a browser export condition that resolves every */server subpath to a stub which throws on import, so a bundler targeting the browser fails loudly instead of shipping a server key to every visitor.

Credentials

Each credential travels in its own header, and the header is part of the credential rather than something the transport guesses:

| kind | header | |---|---| | publishableKey | x-publishable-key | | apiKey (server only) | x-api-key | | accessToken | authorization: Bearer and x-access-token | | bearer | authorization: Bearer |

Retries are off by default — most calls are non-idempotent and a blind repeat is a race — and are opted into per call or per client with a RetryPolicy. DEFAULT_RETRY is a policy you can opt into without inventing numbers; it is not applied unless you ask for it.

@lepsto/sdk/realtime/server is the exception, and it earns it: messages.publish generates an Idempotency-Key for every call, so a repeat of a publish the service already accepted is deduplicated rather than delivered twice — which is what makes retrying it safe enough to have on.

Retry-After reaches you on the error in every case, whether or not anything acts on it.

Contributing

Several teams share this package. CONTRIBUTING.md has the rules: where hand-written toolkit code goes, what a subpath may import, when a subpath may declare its own dependency, and why every hand-written change needs a changeset.

Install

npm install @lepsto/sdk

Use

import { createLepsto, LepstoApiError } from '@lepsto/sdk';

const lepsto = createLepsto({
  productId: 'prod_…',
  publicKey: process.env.LEPSTO_PUBLIC_KEY,
});

try {
  const { domains } = await lepsto.mail.domains.list({});
} catch (err) {
  if (err instanceof LepstoApiError) console.error(err.status, err.code, err.body);
}

Which namespaces the root client carries is decided by the catalog the version was generated from: namespaces, extensions and catalogVersion are exported so a caller can read that off the package itself rather than guess.

createLepsto is the only root entry point. It takes one options object:

createLepsto({ productId, publicKey?, baseUrl?, fetch?, WebSocket? })

baseUrl defaults to https://public.lepsto.com and is how you point the client somewhere else — a local gateway, or the edge addressed directly. fetch and WebSocket are escape hatches for non-standard runtimes. WebSocket is used only by the streaming accessors (see Streaming); fetch is used by everything, streaming included — a connect mints its ticket over HTTP.

One credential at the root

publicKey is the root client's credential, and there is no separate key per extension and no client secret: the public edge resolves the key to your product and applies the scope it was issued with. (The subpaths take the fuller credential / credentials form from Credentials above; publicKey is that same thing spelled for a surface that needs exactly one.) Every operation is declared in the catalog as either

  • mode: "open" — callable without a key, so publicKey may be omitted entirely, or
  • mode: "keyed" — requires the public key.

A keyed operation sends Authorization: Bearer <your public key>. An open one sends no credential header at all — not an empty one, not the key "just in case": sending a key where the edge does not ask for it leaks it into every log and proxy along the way. There are no cookies, no CSRF token and no X-Product-Id header; the product is in the path.

Calling a keyed operation on a client built without a publicKey fails locally, before any network call:

const lepsto = createLepsto({ productId: 'prod_…' });          // no key
await lepsto.realtime.messages.publish({ channel: 'c', payload: {} });
// LepstoApiError: status 0, code 'public_key_required'

status: 0 means the request never left the process. You get the actual mistake — "this operation needs a key and this client has none" — instead of a 403 from the edge that you would have to interpret.

productId is substituted into each route's {productId} placeholder at request time; the catalog never resolves it, because generation time does not know which product a caller holds a key for.

Errors

Every failure is a LepstoApiError:

class LepstoApiError extends Error {
  status: number;          // HTTP status, or 0 when the SDK refused to send the request
  code?: string;           // what to branch on
  body: unknown;           // the edge's response body, verbatim
  retryAfter?: number;     // seconds, from `Retry-After` on a 429
  stage?: 'preflight' | 'mint' | 'upgrade';  // streaming only — see below
}

A code the edge sent always wins. When it sent none, the status supplies the one its contract implies:

| status | code | what happened | | ------ | --------------------------- | ------------------------------------------------------ | | 0 | public_key_required | keyed operation, no key — refused locally, no request | | 401 | public_key_invalid | the key is unknown or revoked | | 403 | public_key_invalid | the key is not scoped for this operation (…_scope) | | 404 | operation_not_allowlisted | the operation is not published for this product | | 413 | body_too_large | over the extension's max_body_bytes | | 429 | rate_limited | rate limited; retryAfter carries Retry-After |

Anything else — a 500, a proxy's own error page — keeps code: undefined. Those come from below the edge, and naming them would claim we understood a failure we did not.

Nothing retries unless you asked for it (see Credentials for the one exception). retryAfter is reported so you can decide; a library that backs off on your behalf makes latency decisions it has no standing to make.

Published from production only

@lepsto/sdk is generated and published exclusively from the production catalog. Staging runs the exact same pipeline with --dry-run: it fetches, generates, type-checks, classifies the semver change and writes out/result.json, then stops without publishing.

This is deliberate. Staging carries toolkits that do not exist in production (the playground among them) and leads production by whatever has not been promoted yet. Publishing a staging build under any dist-tag would ship customers operations they cannot call. There is no prerelease channel, and the pipeline rejects any --channel value but latest.

Only kind: "sdk" operations are generated

The public catalog serves three surfaces, and every operation says which one it belongs to: sdk (callable from this client), embed (an embeddable widget route) and asset (a static asset route). The generator emits the sdk operations and skips the other two without complaining — they are not errors, they are someone else's operations riding the same catalog. The run's summary line counts both, so a client that shrank is always explainable from the log:

generated src/gen from …/public-catalog.json: 42 sdk operations, skipped 3 embed + 1 asset

An operation with no kind at all comes from a catalog older than spec v3 and is treated as sdk. An unrecognised kind is a hard error, exactly like an unrecognised mode: the contract moved upstream and we would otherwise guess.

Publishing a new version is a deliberate step

After a toolkit promotes a public operation to production, someone must run the release. There is no trigger. Run the Release @lepsto/sdk GitHub Actions workflow: Actions → Release @lepsto/sdkRun workflow, dry_run on for a rehearsal, off to publish. It builds the commit, publishes, and opens a pull request carrying the version bump, the changelog entry and the consumed changesets — that pull request is part of the release and has to be merged, or the next run announces the same changes again.

If nobody runs it, nothing tells you. The published package silently lags the live API: customers install an @lepsto/sdk that does not expose operations that exist, and get no error, no warning and no hint of why. No build fails and no alert fires — the gap just grows with every promotion until someone notices by hand. That is why this step also lives in the toolkit production-release checklist, not only here.

Running it twice is harmless: the pipeline compares the generated package against the last published one by content and publishes nothing when they match.

A new version can take a few minutes to appear in npm view, so retry before you call it missing.

docs/publishing.md has the full procedure — what to check first, how to read out/result.json, what each failure means, and the Cloud Build fallback for when the workflow is unavailable.

Streaming

An operation with a ws binding mounts next to its namespace's regular accessors, with a Connect suffix, and returns the platform socket for you to listen on:

const stream = await lepsto.realtime.channel.subscribeConnect({ channel: 'room-1' });
stream.socket.addEventListener('message', (e) => console.log(e.data));
stream.close();

Why these accessors are async

Opening a stream is two calls, not one. The SDK first mints a ticket over plain HTTP, sending your public key as an Authorization header, and only the ticket goes into the WebSocket URL:

POST https://public.lepsto.com/_lessly/ws-ticket/{productId}/{extension}{wsPath}
     Authorization: Bearer lpk_…            →  { "ticket": "v1.…", "expires_in": 30 }

wss://public.lepsto.com/{productId}/{extension}{wsPath}?ticket=v1.…

The key never enters a URL: URLs are written to every access log, proxy trace and CDN record between you and the edge, and those outlive the browser tab that held the key. The ticket is harmless there instead — it lives 30 seconds and opens exactly one operation, bound to that product, extension and path. A ticket for /rooms/lobby opens nothing else.

For the same reason the SDK never stores one: a fresh ticket is minted immediately before every connect. There is no ticket cache and no refresh-ahead — minting is one cheap call, and a stored ticket would be a credential lying around for no benefit.

The returned promise resolves once the edge has accepted the upgrade, so a stream you are handed is a stream you can send on. It rejects with a LepstoApiError otherwise, and the two failures are distinguishable:

  • the mint was refused — the error carries the edge's HTTP status and code (public_key_required, public_key_invalid, public_key_scope, not_found, upgrade_required, rate_limited, service_unavailable). A rate_limited mint (429, a per-IP budget on the extension) also fills err.retryAfter with the Retry-After seconds — reported, never acted on: this SDK does not retry for you;
  • the upgrade was refusedstatus: 0 and the gateway's close code (ws_ticket_expired, ws_ticket_invalid, ws_ticket_required, origin_not_allowed, …), with err.body carrying the raw { closeCode, reason }. A code this SDK has not seen before is reported as the edge sent it rather than translated; ws_upgrade_failed means the close frame named no code at all.

Every LepstoApiError from a stream also carries err.stage'preflight', 'mint' or 'upgrade' — which says where it failed, and that is the part that does not change when the edge renumbers a status. What to do with it: preflight is a mistake in your call (you asked for a keyed operation without a key), so retrying is pointless; mint is an ordinary HTTP failure, and a retry is meaningful when its code says so (service_unavailable, yes; public_key_invalid, no); upgrade depends on the code — ws_ticket_expired is fixed by connecting again and service_unavailable by retrying with backoff, while the rest mean the URL or the configuration is wrong. On a plain HTTP call stage is undefined: there is one call and nothing to disambiguate. Nothing retries on your behalf.

Two of the upgrade codes surprise people, because they arrive after a mint that succeeded:

  • origin_not_allowed — the browser's Origin is not in your product's public CORS policy. The SDK does not send Origin; the browser does. Add your site's domain to the product's allowed origins. This is the most likely first failure in a browser, and the credential is not the problem.
  • service_unavailable — the edge cannot sign or verify tickets right now. Transient, and not a statement about your path: the operation is published, the gateway is having a moment.

A keyed stream opened without a key hits the same local pre-flight as a keyed request, before any network call. Frames are delivered raw: their framing is protocol-specific and documented by the operation, so the SDK does not invent a shape for them.

The gateway side of this contract is in review and not yet deployed, so these accessors are built against the frozen contract rather than against a live edge.

Development

npm ci
npm run typecheck
npm test
npm run build

# regenerate the committed src/gen — the catalog is a required argument, name it
npm run generate -- fixtures/public-catalog.json

# see what a populated catalog generates, in out/gen-sample
npm run generate:sample

# exercise the whole pipeline against the fixture, without publishing
npm run ci -- --channel latest --catalog-url "file://$PWD/fixtures/public-catalog.sample.json" --dry-run

src/gen is generated output — do not edit it by hand. src/runtime is the hand-written, shipped runtime, and src/extras is the hand-written half of a namespace (see src/extras/README.md). The committed src/gen is a snapshot — today built from fixtures/public-catalog.json, so the tree typechecks — not the shipped surface: a release regenerates it from the live catalog. Which fixture is which is CONTRIBUTING.md § 8; see docs/publishing.md before changing what is committed.