@systemix/token
A cryptographically secure token generator and signed-token module for API keys, session tokens, CSRF tokens, and auth. Zero external dependencies, built on Node.js built-ins. Requires Node.js 24.7+. HMAC and RSA work in both environments.
Installation
pnpm add @systemix/token
npm install @systemix/token
Package Structure
| Subpath | Contents |
|---|---|
@systemix/token | Main entry — re-exports token, signed, rsa, common |
@systemix/token/token | Token generator, encoding utils, validation |
@systemix/token/signed | Signed token encode/decode/verify |
@systemix/token/rsa | RSA sign/verify (Node) |
@systemix/token/common | Crypto, enums, types, errors, utils |
Token Generator
generateToken(props?)
Generates cryptographically secure random tokens. Returns a single string or array of strings.
import { generateToken } from '@systemix/token'; // Default: 32 bytes, hex encoding const token = generateToken(); // → "a1b2c3d4e5f6789..." // With options const apiKey = generateToken({ byteLength: 32, charset: 'hex', }); const sessionToken = generateToken({ byteLength: 24, charset: 'base64url', }); const batch = generateToken({ byteLength: 16, charset: 'alphanumeric', count: 5, }); // → ["Ab3xYz...", "Mn7pQr...", ...]
Props
| Property | Type | Default | Description |
|---|---|---|---|
byteLength | number | 32 | Number of random bytes (1–1024). Output length depends on charset. |
charset | 'hex' | 'base64' | 'base64url' | 'alphanumeric' | 'hex' | Encoding format. |
count | number | 1 | Number of tokens to generate (1–10). Returns string[] when > 1. |
Charset behavior
| Charset | Output length | Use case |
|---|---|---|
hex | byteLength × 2 | API keys, opaque IDs |
base64 | ~byteLength × 4/3 | Compact tokens |
base64url | ~byteLength × 4/3, URL-safe | URLs, cookies |
alphanumeric | byteLength | Human-readable, no special chars |
generateTokenPropValidation(props)
Validates props before generation. Throws if invalid.
import { generateTokenPropValidation } from '@systemix/token/token'; try { generateTokenPropValidation({ byteLength: 64, charset: 'hex' }); } catch (e) { console.error(e.message); }
Encoding Utilities
Use when you need to encode raw bytes yourself:
import { bytesToHex, bytesToBase64, bytesToBase64Url, bytesToAlphanumeric, } from '@systemix/token/token'; const bytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); bytesToHex(bytes); // → "48656c6c6f" bytesToBase64(bytes); // → "SGVsbG8=" bytesToBase64Url(bytes); // → "SGVsbG8" bytesToAlphanumeric(bytes); // → "NkF2bB2"
| Function | Input | Output |
|---|---|---|
bytesToHex(bytes) | Uint8Array | Hex string |
bytesToBase64(bytes) | Uint8Array | Base64 string |
bytesToBase64Url(bytes) | Uint8Array | URL-safe base64 |
bytesToAlphanumeric(bytes) | Uint8Array | A–Z, a–z, 0–9 |
Signed Tokens
Signed tokens are compact, URL-safe strings with a header, payload, and signature. Use for auth, sessions, or any signed claims. Supports HS/RS/PS/ES/EdDSA with node:crypto.
encodeSigned(payload, secret, options?)
Creates a signed token. Returns a Promise<string>. Supports HMAC (shared secret) and RSA (PEM private key).
import { encodeSigned } from '@systemix/token/signed'; // HMAC (shared secret of 32+ bytes) const token = await encodeSigned( { userId: '123', role: 'admin' }, 'my-secret-key', { algorithm: 'HS256', expiresIn: 3600, issuer: 'my-app', audience: 'api', subject: 'user-123', tokenId: true, }, ); // RSA (PEM private key, 2048+ bits) const tokenRsa = await encodeSigned({ userId: '123' }, privateKeyPem, { algorithm: 'RS256', expiresIn: 3600, });
Encode options
| Option | Type | Default | Description |
|---|---|---|---|
algorithm | SignedAlgorithm | 'HS256' | Signing algorithm. |
typ | string | 'ST' | Token type in header. |
kid | string | — | Key ID for key rotation. |
cty | string | — | Content type. |
expiresIn | number | — | Expiration in seconds from now. |
notBefore | number | — | Not-before in seconds from now. |
issuedAt | number | now | Issued-at timestamp. |
issuer | string | — | iss claim. |
subject | string | — | sub claim. |
audience | string | string[] | — | aud claim. |
tokenId | string | true | — | jti claim. true = auto-generate. |
decodeSigned(token)
Decodes a token without verifying the signature.
import { decodeSigned } from '@systemix/token/signed'; const { header, payload, signature } = decodeSigned<{ userId: string }>(token); console.log(header.alg); // "HS256" console.log(payload.userId); // "123"
verifySigned(token, secret, options)
Decodes and verifies a token. algorithms is required to prevent algorithm confusion.
import { verifySigned } from '@systemix/token/signed'; const payload = await verifySigned<{ userId: string }>(token, 'my-secret', { algorithms: ['HS256'], issuer: 'my-app', audience: 'api', clockTolerance: 60, });
Verify options
| Option | Type | Required | Description |
|---|---|---|---|
algorithms | SignedAlgorithm[] | Yes | Allowed algorithms. Prevents algorithm-swap attacks. |
typ | string | No | Required typ header (case-insensitive). |
maxAge | number | No | Reject tokens older than N seconds since iat. |
issuer | string | string[] | No | Expected iss. |
audience | string | string[] | No | Expected aud. |
subject | string | No | Expected sub. |
clockTolerance | number | No | Seconds of skew for exp/nbf. |
ignoreExpiration | boolean | No | Skip exp check. |
ignoreNotBefore | boolean | No | Skip nbf check. |
Algorithms
| Algorithm | Type | Key |
|---|---|---|
| HS256, HS384, HS512 | HMAC (symmetric) | Secret of at least 32 / 48 / 64 bytes (RFC 7518 §3.2) |
| RS256, RS384, RS512 | RSASSA-PKCS1-v1_5 | PEM private key (encode) / public key (verify), ≥ 2048 bits |
| PS256, PS384, PS512 | RSASSA-PSS | Same as RS*; preferred for new RSA systems |
| ES256, ES384, ES512 | ECDSA | EC key on P-256 / P-384 / P-521; raw r||s signatures |
| EdDSA | Ed25519 / Ed448 | Ed25519 or Ed448 key |
Keys are PEM strings or node:crypto KeyObjects. The library refuses keys that don't fit the algorithm: a PEM can't be an HMAC secret (the RS256→HS256 downgrade), RSA keys under 2048 bits and EC keys on the wrong curve are rejected, and so are short HMAC secrets. These throw InvalidKeyError.
Standard claims
| Claim | Type | Description |
|---|---|---|
iss | string | Issuer |
sub | string | Subject |
aud | string | string[] | Audience |
exp | number | Expiration (Unix seconds) |
nbf | number | Not before (Unix seconds) |
iat | number | Issued at (Unix seconds) |
jti | string | Token ID |
Common Utilities
Low-level crypto primitives and constant-time comparison:
import { getRandomBytes, getRandomInt, bytesEqual, secureCompare, } from '@systemix/token/common'; const bytes = getRandomBytes(32); const n = getRandomInt(100); // [0, 100) bytesEqual(a, b); // constant-time byte comparison secureCompare(sig1, sig2); // constant-time string comparison
| Function | Description |
|---|---|
getRandomBytes(length) | Returns Uint8Array of random bytes. |
getRandomInt(max) | Returns random integer in [0, max). |
bytesEqual(a, b) | Constant-time byte comparison (timing-safe). |
secureCompare(a, b) | Constant-time string comparison (timing-safe). |
Common Module
For advanced use: enums, types, errors, and utils.
import { CHARSETS, HMAC_ALGORITHMS, RSA_ALGORITHMS, TokenPropsEnum, type Charset, type SignedAlgorithm, } from '@systemix/token/common';
Error Handling
Token generator errors
Throws generic Error with messages like:
Invalid byteLength. Must be a positive number.Invalid charset. Must be one of: hex, base64, base64url, alphanumeric.
Signed token errors
All extend SignedTokenError:
| Error | When |
|---|---|
InvalidTokenError | Malformed token, invalid encoding |
InvalidSignatureError | Signature mismatch, wrong secret |
TokenExpiredError | exp in the past |
NotBeforeError | nbf in the future |
AudienceMismatchError | aud does not match |
IssuerMismatchError | iss does not match |
import { verifySigned, TokenExpiredError, InvalidSignatureError, } from '@systemix/token/signed'; try { const payload = await verifySigned(token, secret, { algorithms: ['HS256'] }); } catch (e) { if (e instanceof TokenExpiredError) { console.log('Expired at', e.expiredAt); } else if (e instanceof InvalidSignatureError) { console.log('Invalid signature'); } }
Usage Examples
API key generation
const apiKey = generateToken({ byteLength: 32, charset: 'hex', });
Session token with signed payload
const sessionToken = await encodeSigned( { userId: user.id, email: user.email }, process.env.SESSION_SECRET!, { expiresIn: 86400, issuer: 'my-app' }, ); const payload = await verifySigned(sessionToken, process.env.SESSION_SECRET!, { algorithms: ['HS256'], issuer: 'my-app', });
RSA for distributed verification
const token = await encodeSigned({ sub: 'user-1' }, privateKey, { algorithm: 'RS256', expiresIn: 3600, }); const payload = await verifySigned(token, publicKey, { algorithms: ['RS256'], });
Key rotation with kid
const keys = { '2026-10': currentSecret, '2026-04': previousSecret }; const token = await encodeSigned({ sub: 'u1' }, currentSecret, { kid: '2026-10', }); await verifySigned(token, (header) => keys[header.kid], { algorithms: ['HS256'], typ: 'ST', });
Compared with jose
jose is the full JOSE toolkit. This package is a small, opinionated JWS signer and verifier: use it when you want a compact API with safe defaults. Use jose when you need more.
@systemix/token | jose | |
|---|---|---|
| Signed tokens (JWS compact) | Yes | Yes |
| Encrypted tokens (JWE) | No | Yes |
| JWK / JWKS handling and remote JWKS | No | Yes |
| Runtimes | Node.js 24.7+ | Node, browsers, edge runtimes |
| Dependencies | None | None |
Tokens produced here are standard compact JWS, so jose and other libraries can verify them.
Security Notes
- Algorithm whitelist:
algorithmsis required, and the token's ownalgis never trusted. - Key confusion: a key only works with an algorithm of its own type (see Algorithms) — an RSA public key can't be used as an HMAC secret.
- Secret strength: HMAC secrets must be at least as long as the hash output (32 / 48 / 64 bytes); shorter ones are rejected.
- Explicit typing: pass
typtoverifySignedso a token minted for one purpose can't be accepted for another (RFC 8725 §3.11). - Hardening: tokens with a
critheader are rejected, payloads must be JSON objects, signatures must be canonical base64url, and a token is expired at its exactexpsecond. - Key rotation: pass a function as the key to pick it by
kid(see below). Treat the header as untrusted — use it only to look a key up in a set you control. - Clock skew: Use
clockTolerancewhen servers may have time drift. - Sensitive data: Signed tokens are signed, not encrypted. Do not put secrets in the payload.
Try it
Open the Token Generator demo to experiment interactively.