@gramota/sdk, @gramota/core, and one shape for every package
Today's release is structural. Runtime behaviour is unchanged; the public surface got tightened so that the fifteen packages now read as one product instead of fifteen separately-designed ones.
Two new packages, two reshaped clients, one shared base, and the old shapes removed in the same cycle. There's exactly one way to call each thing. Versions on npm right now:
@gramota/sdk@0.2.0— top-level facade (new)@gramota/core@0.2.1— shared primitives (new)@gramota/verifier@0.5.0— Stripe-shaped namespaces, flat methods removed@gramota/qr@0.4.0—QrClientclass, loose factories removed@gramota/jose@0.3.0—Fetcherre-export removed (import from@gramota/core)- everything else — patch bump for the
GramotaErrorretrofit
What changed and why
One client, one shape
Before today, the verifier was a flat surface:
verifier.verify(token, opts); // gone
verifier.response(rawBody, opts); // gone
verifier.request(opts); // gone
The holder + issuer were already namespaced (holder.credentials.*,
issuer.credentials.*), but the verifier predated that convention.
Inconsistent inside one product. Fixed:
verifier.presentations.verify(token, opts);
verifier.responses.verify(rawBody, opts);
verifier.requests.create(opts);
Every client now reads client. — same shape Stripe,
AWS SDK v3, and the modern OpenAI client all use. The flat methods
have been removed; this is the only public shape now.
One import for the common case
@gramota/sdk is the new top-level facade:
import { Gramota } from "@gramota/sdk";
const gramota = new Gramota({
verifier: { audience: "https://my-bank.com", trust },
qr: { errorCorrection: "H" },
});
await gramota.verifier.presentations.verify(token, { nonce });
const code = gramota.qr.fromAuthorizationRequest(req);
What you get over wiring four packages by hand:
- One config object.
audience,trust,issuerKeyflow to the verifier without you wiring them. Future shared options (telemetry, retry, request-id headers) plug in here once. - Lazy instantiation. Properties construct their underlying
client on first access. If you only use
.verifier, the issuer never loads. - One mental model. Mirrors
new Stripe(key).customers.create()— if you've used a modern API client, you already know how this works.
The individual packages still work standalone; the facade is additive.
One error to catch
Every per-package error class (VerifierError, IssuerError,
HolderError, QrError, JoseError, SdJwtError, etc. — thirteen of
them) now extends GramotaError from the new @gramota/core package:
import { isGramotaError } from "@gramota/core";
try {
await verifier.presentations.verify(token, opts);
} catch (err) {
if (isGramotaError(err)) {
telemetry.recordError(err.name, err.code); // type-safe across packages
}
throw err;
}
Existing instanceof VerifierError and error.code === "..." checks
still work — the per-package codes (SecurityCheckName,
IssuerErrorCode, etc.) are unchanged.
@gramota/core also hosts the Fetcher transport interface, which
used to live in @gramota/jose for historical reasons. @gramota/jose
re-exports it for back-compat through 1.0.
One QR client
@gramota/qr was originally three loose factory functions on a
namespace. It's now a class that you can configure once and reuse,
matching the rest of the SDK:
import { QrClient } from "@gramota/qr";
const qr = new QrClient({
renderer: customRenderer, // strategy override
errorCorrection: "H", // default for every code this client builds
width: 512,
});
const code = qr.fromUrl("openid4vp://…");
await code.toDataUrl(); // <img src=...>
The default qr singleton is now an instance of QrClient with the
default renderer — qr.fromUrl(...) works the same. The loose factory
re-exports (fromUrl, fromAuthorizationRequest, fromCredentialOffer)
that used to be available as named imports have been removed; pick
either qr.* or new QrClient({...}).*.
Test surface
666 passing tests across 66 test files in the monorepo. Up from 632 — the new packages and the namespace coverage added 34. Zero regressions.
What's next
verifier.trustedIssuers.*— namespace for trust-list operations (currently scattered between@gramota/trustand theVerifierconfig). Lands when the EU LoTL ingestion code does.verifier.statusLists.*— same for status-list resolution.- Idempotency keys + retry policy on
@gramota/sdk— Stripe-style. This is a 1.0 thing.
Migration
If you upgraded to 0.4.0/0.3.0 yesterday and saw deprecation warnings, fix the call sites the warnings pointed at — that's the only change. Runtime behaviour is identical; the type-checker tells you exactly what to change.
If you're upgrading from earlier (0.2 → today), the changes you'll hit are concentrated in three places:
verifier.verify(...)→verifier.presentations.verify(...)(and.response/.requestsimilarly).import { Fetcher } from "@gramota/jose"→from "@gramota/core".import { fromUrl } from "@gramota/qr"→ useqr.fromUrl(...)ornew QrClient({...}).fromUrl(...).
Per-package error classes still extend their own subclasses
(VerifierError, IssuerError, etc.) — those are unchanged. The
new bit is instanceof GramotaError works as a single catch.
— @gramota/sdk on npm
· GitHub
· Demo