SDK
One package for identity, entitlements, usage and webhook verification. No dependencies.
@guiru/sdk is the official client. It is a thin wrapper over the same six endpoints — nothing is hidden from you — but it carries the decisions an integration should not have to make twice.
bash
npm install @guiru/sdk # pnpm add @guiru/sdk · yarn add @guiru/sdkGate a feature
entitlements
import { Guiru } from "@guiru/sdk";
const guiru = new Guiru({ apiKey: process.env.GUIRU_API_KEY! });
if (await guiru.entitlements.isActive({ userId })) {
// let them in
} else {
// your paywall, your words
}
// the whole answer, when you want to know why
const { active, type, ends_at } = await guiru.entitlements.check({ userId });Report that the session was real
usage
await guiru.usage.activity({ userId }); // they did the thing
await guiru.usage.heartbeat({ userId }); // still working
await guiru.usage.session({ userId }); // the app openedVerify a webhook
webhook handler
// rawBody must be the bytes that arrived, not a re-serialised object
const event = await guiru.webhooks.constructEvent({
secret: process.env.GUIRU_WEBHOOK_SECRET!,
rawBody: await req.text(),
signature: req.headers.get("guiru-signature")!,
previousSignature: req.headers.get("guiru-signature-previous"),
});
switch (event.event) {
case "purchase.completed": /* … */ break;
case "entitlement.refunded": /* close the door */ break;
}Sign in with Shin ID
oauth + pkce
const { verifier, challenge } = await guiru.oauth.createPkce();
// keep `verifier` for the exchange; send them here
const url = guiru.oauth.authorizeUrl({
clientId, redirectUri, state, codeChallenge: challenge,
});
// on your callback
const token = await guiru.oauth.exchangeCode({ code, redirectUri, clientId, codeVerifier: verifier });
const me = await guiru.oauth.me({ accessToken: token.access_token });What it decides for you
- Signatureswebhooks
- HMAC over the raw body, compared in constant time, inside the five-minute window, with the rotation header accepted while you redeploy.
- Retriestransport
- A 429 waits exactly the
retry-afterthe server asked for; a 5xx backs off exponentially; a 4xx is never retried, because it would fail identically. - Errorstypes
GuiruAuthError,GuiruRateLimitError(withretryAfter),GuiruRequestError,GuiruServerError— branch on the class instead of parsing a string.- PKCEoauth
- Verifier from a real CSPRNG and the S256 challenge, rather than whatever
Math.randomgives you. - Mistakesguards
- Passing your own user id instead of the Shin ID, or an API key where a user token belongs, fails before it costs a request.