@systemix/token

A cryptographically secure token generator and signed-token module for API keys, session tokens, CSRF tokens, and auth. Zero external dependencies, pure Node.js and browser built-ins. HMAC and RSA work in both environments.

Installation

pnpm add @systemix/token
npm install @systemix/token

Package Structure

SubpathContents
@systemix/tokenMain entry — re-exports token, signed, rsa, common
@systemix/token/tokenToken generator, encoding utils, validation
@systemix/token/signedSigned token encode/decode/verify
@systemix/token/rsaRSA sign/verify (Node + browser, auto-detect)
@systemix/token/commonCrypto, 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

PropertyTypeDefaultDescription
byteLengthnumber32Number of random bytes (1–1024). Output length depends on charset.
charset'hex' | 'base64' | 'base64url' | 'alphanumeric''hex'Encoding format.
countnumber1Number of tokens to generate (1–10). Returns string[] when > 1.

Charset behavior

CharsetOutput lengthUse case
hexbyteLength × 2API keys, opaque IDs
base64~byteLength × 4/3Compact tokens
base64url~byteLength × 4/3, URL-safeURLs, cookies
alphanumericbyteLengthHuman-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"
FunctionInputOutput
bytesToHex(bytes)Uint8ArrayHex string
bytesToBase64(bytes)Uint8ArrayBase64 string
bytesToBase64Url(bytes)Uint8ArrayURL-safe base64
bytesToAlphanumeric(bytes)Uint8ArrayA–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. HMAC and RSA work in both Node and browser (Web Crypto API; RSA auto-detects environment).

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) — works in browser and Node
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) — works in browser and Node
const tokenRsa = await encodeSigned({ userId: '123' }, privateKeyPem, {
  algorithm: 'RS256',
  expiresIn: 3600,
});

Encode options

OptionTypeDefaultDescription
algorithmSignedAlgorithm'HS256'Signing algorithm.
typstring'ST'Token type in header.
kidstringKey ID for key rotation.
ctystringContent type.
expiresInnumberExpiration in seconds from now.
notBeforenumberNot-before in seconds from now.
issuedAtnumbernowIssued-at timestamp.
issuerstringiss claim.
subjectstringsub claim.
audiencestring | string[]aud claim.
tokenIdstring | truejti 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

OptionTypeRequiredDescription
algorithmsSignedAlgorithm[]YesAllowed algorithms. Prevents algorithm-swap attacks.
issuerstring | string[]NoExpected iss.
audiencestring | string[]NoExpected aud.
subjectstringNoExpected sub.
clockTolerancenumberNoSeconds of skew for exp/nbf.
ignoreExpirationbooleanNoSkip exp check.
ignoreNotBeforebooleanNoSkip nbf check.

Algorithms

AlgorithmTypeSecret / key
HS256, HS384, HS512HMAC (symmetric)Shared secret string
RS256, RS384, RS512RSA (asymmetric)PEM private key (encode), PEM public key (verify)

Standard claims

ClaimTypeDescription
issstringIssuer
substringSubject
audstring | string[]Audience
expnumberExpiration (Unix seconds)
nbfnumberNot before (Unix seconds)
iatnumberIssued at (Unix seconds)
jtistringToken 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
FunctionDescription
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:

ErrorWhen
InvalidTokenErrorMalformed token, invalid encoding
InvalidSignatureErrorSignature mismatch, wrong secret
TokenExpiredErrorexp in the past
NotBeforeErrornbf in the future
AudienceMismatchErroraud does not match
IssuerMismatchErroriss 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'],
});

Security Notes

  • Algorithm whitelist: Always pass algorithms to verifySigned. Never trust the token header.
  • Secret strength: Use at least 256 bits (32 bytes) for HMAC secrets.
  • RSA keys: Use 2048+ bit keys.
  • Clock skew: Use clockTolerance when 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.