@moon-x/node-sdk
v0.3.0
Published
MoonX server-side SDK for Node.js. Verify MoonX-issued access + identity tokens with zero runtime dependencies.
Downloads
395
Maintainers
Readme
@moon-x/node-sdk
MoonX server-side SDK for Node.js. Verify MoonX-issued access and identity tokens with zero runtime dependencies.
Works in Node 18+, Cloudflare Workers, Vercel Edge, Bun, and Deno — anywhere globalThis.crypto.subtle and fetch are available.
Install
npm i @moon-x/node-sdk
# or
pnpm add @moon-x/node-sdkQuick start
import { MoonXClient } from "@moon-x/node-sdk";
const moonx = new MoonXClient({
publishableKey: process.env.MOONX_PUBLISHABLE_KEY!,
issuer: process.env.MOONX_AUTH_ISSUER, // optional, defaults to https://api.moonx-dev.com
});
// In a Next.js route / Express handler / etc.
const session = await moonx.auth.verifySession({
accessToken: req.headers["x-moonx-access-token"]!,
identityToken: req.headers["x-moonx-identity-token"],
});
// session.access.sub — user id
// session.access.sid — session id
// session.identity?.email — OIDC profile claim (present when identity token forwarded)
// session.identity?.name
// session.identity?.pictureWhat it does
When a MoonX-authenticated user calls your backend, the request carries one or both of:
| Header | Token | Carries |
|---|---|---|
| X-MoonX-Access-Token | Access token | Minimal authorization claims (sub, sid, aud, iss, exp) |
| X-MoonX-Identity-Token | Identity token | OIDC profile claims (email, name, picture, etc.) |
verifySession() runs the full canonical check on both:
- Pin audience to your app id (rejects tokens from other MoonX apps even if cryptographically valid).
- Pin issuer to your configured MoonX deployment.
- Fetch the per-app public JWKS from MoonX (cached 5 min by default).
- Verify the ES256 signature against the fetched key.
- Check the token hasn't expired.
- (Identity token path) Verify the identity token's
submatches the access token'ssub— closes the swap-in-someone-else's-identity-token attack.
No shared secret with MoonX is required. The verification runs entirely against the public JWKS.
API
new MoonXClient(config)
Construct once per process. Holds in-memory caches; reuse across requests.
| Field | Type | Required | Default |
|---|---|---|---|
| publishableKey | string | Yes | — |
| secretKey | string | No (v0.1 unused) | — |
| issuer | string | No | https://api.moonx-dev.com |
| baseUrl | string | No | Same as issuer |
| jwksTtlMs | number | No | 300_000 (5 min) |
| appResolveTtlMs | number | No | 3_600_000 (1 hour) |
| fetch | typeof fetch | No | globalThis.fetch |
The publishable key (moon_pk_*) is the same one your iframe / browser SDK uses. It's safe to put in any env var, but in node-sdk it typically reads from a server-only env var alongside the secret key. The secret key (moon_sk_*) field is reserved for v0.2 (data + swaps modules).
client.auth.verifyAccessToken(token)
Returns Promise<AccessTokenClaims>. Throws a MoonXError subclass on any failure.
client.auth.verifyIdentityToken(token)
Returns Promise<IdentityTokenClaims>. Throws a MoonXError subclass on any failure.
client.auth.verifySession({ accessToken, identityToken? })
Returns Promise<{ access: AccessTokenClaims, identity: IdentityTokenClaims | null }>. Identity is null when the caller didn't forward an identity token.
Canonical entry point — use this from request handlers. Cross-checks the two tokens' subjects when both are present.
Errors
All thrown errors extend MoonXError, which carries a .code property for programmatic handling:
import {
MoonXError,
ExpiredTokenError,
InvalidTokenError,
AudienceMismatchError,
SubjectMismatchError,
// ...
} from "@moon-x/node-sdk";
try {
await moonx.auth.verifySession({ accessToken, identityToken });
} catch (err) {
if (err instanceof ExpiredTokenError) {
return new Response("session expired", { status: 401 });
}
if (err instanceof MoonXError) {
return new Response(err.message, { status: 401 });
}
throw err;
}Error codes
| Class | .code | Meaning |
|---|---|---|
| MalformedTokenError | malformed_token | Not a well-formed JWT |
| InvalidTokenError | invalid_signature | Signature didn't verify against the JWKS |
| ExpiredTokenError | expired_token | exp claim is in the past |
| AudienceMismatchError | audience_mismatch | Token's aud ≠ this client's app id |
| IssuerMismatchError | issuer_mismatch | Token's iss ≠ configured issuer |
| SubjectMismatchError | subject_mismatch | Identity token's sub ≠ access token's sub |
| KidMismatchError | kid_mismatch | Token's kid ≠ current JWKS key id (likely rotation) |
| UnsupportedAlgorithmError | unsupported_algorithm | Token uses something other than ES256 |
| JwksFetchError | jwks_fetch_failed | Couldn't fetch the JWKS from MoonX |
| AppResolutionError | app_resolution_failed | Publishable-key → app-id lookup failed |
| ConfigurationError | configuration_error | Bad MoonXClient config (e.g. swapped pk/sk) |
Type imports
AccessTokenClaims, IdentityTokenClaims, BaseTokenClaims, and VerifiedSession are also exported from @moon-x/core/types — the single source of truth shared with the browser + RN SDKs. Either import path works:
// From node-sdk (convenience re-export):
import type { AccessTokenClaims } from "@moon-x/node-sdk";
// From core directly (matches the browser SDK call sites):
import type { AccessTokenClaims } from "@moon-x/core/types";Why no JWT library?
Auth providers ship server SDKs partly to abstract away jose-style JWT libraries, but every dependency is a supply-chain surface. This SDK uses only Web Crypto (globalThis.crypto.subtle) and fetch, both Node 18+ standard library. Result: a single npm install line on your audit.
Roadmap
- v0.1 (current) — auth:
verifyAccessToken,verifyIdentityToken,verifySession. - v0.2 (pending MX-124) — adds
client.tokens.*,client.wallets.*,client.swaps.*once the backend's secret-key middleware lands.
