npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

aegiss-token

v2.0.0

Published

Security-first, self-defending token library for Node.js. Powered by Rust, Ed25519, ChaCha20-Poly1305, CBOR, context binding. Zero JS dependencies.

Readme

Aegiss

Security-first, self-defending token library for Node.js. Fixed algorithms (Ed25519, ChaCha20-Poly1305), mandatory context binding (IP + User-Agent), constant-time checks, and built-in rate limiting. Zero dependencies — uses only Node.js built-in crypto.


Table of contents


Overview

| Aspect | Description | |--------|-------------| | Purpose | Issue and verify cryptographically signed tokens bound to client context (IP + User-Agent). | | Token type | Public only: signed, not encrypted. Payload is CBOR-encoded and base64url. | | Algorithms | Ed25519 (signature), SHA-256 (context fingerprint), ChaCha20-Poly1305 (separate encrypt/decrypt helpers). | | Context binding | Every token is tied to the ip and userAgent used at sign time. Verification fails if the request’s IP or User-Agent does not match. | | Dependencies | None. Node.js >= 18 only. |

Comparison with JWT / PASETO

| Feature | JWT | PASETO | Aegiss | |---------|-----|--------|--------| | Algorithm choice | User picks (risk of misuse) | Version-locked | Hardcoded: Ed25519 & ChaCha20 | | Payload format | JSON | JSON | CBOR (binary, compact) | | Context binding | None | None | Required IP + User-Agent fingerprint | | Attack mitigation | External | External | Built-in (timing-safe, rate limit, revocation) | | Verification error | Can leak reason | Can leak reason | Single generic message |


Requirements & install

  • Runtime: Node.js >= 18.
  • Install: npm install aegiss-token
  • Import: const aegiss = require('aegiss-token'); (CommonJS). TypeScript types are provided via src/index.d.ts.

Token format & behavior

  • Structure: v1.public.<payload_base64url>.<signature_base64url>
    • v1 = protocol version (only this version is accepted).
    • public = token type (signed, not encrypted).
    • Payload and signature are base64url-encoded; payload is CBOR.
  • Payload contents (always present): iat (issued-at), exp (expiry), jti (unique id), fingerprint (hash of ip|userAgent). Plus any claims you pass to sign().
  • Reserved claims: The library always sets iat, exp, jti, and fingerprint. User payload cannot override these; they are merged after your payload.
  • Verification: Checks in order: format → signature → expiry → optional minIat → optional revokedJtis → fingerprint (constant-time). Any failure throws VerificationError with message "Invalid token" (no detail leaked).

Quick start

const { generateKeys, sign, verify, getClientInfo } = require('aegiss-token');

// 1. Generate key pair (do once; store securely)
const { publicKey, privateKey } = generateKeys();

// 2. When user logs in: sign a token with current request context
const clientInfo = getClientInfo(req);  // { ip, userAgent }
const token = sign(
  { userId: '123', role: 'admin' },
  privateKey,
  { clientInfo, expiresInSeconds: 3600 }
);
// Return token to client (e.g. in JSON body).

// 3. On protected routes: verify using same request’s context
const clientInfoNow = getClientInfo(req);
const payload = verify(token, publicKey, clientInfoNow);
// payload.userId, payload.role, etc. Use payload; do not trust client-sent claims without this verify.

Important for AI/implementers:

  • clientInfo at sign must come from the same logical client (IP + User-Agent) as at verify. If the client uses a different IP (e.g. new network) or User-Agent, verification will fail.
  • Always use getClientInfo(req) (or equivalent) for both sign and verify so the fingerprint is consistent.

API reference

Key and token

| Function | Input | Output | Throws | |----------|--------|--------|--------| | generateKeys() | none | { publicKey: string, privateKey: string } (base64url) | — | | sign(payload, privateKey, options) | See below | Token string v1.public.... | Error if invalid payload or missing clientInfo | | verify(token, publicKey, currentClientInfo, options?) | See below | Decoded payload object | VerificationError on any verification failure |

sign(payload, privateKey, options)

  • payload: Plain object. Any JSON-serializable claims (numbers, strings, etc.). Must not be null/array. Reserved names iat, exp, jti, fingerprint are overwritten by the library.
  • privateKey: String (base64url Ed25519 private key from generateKeys()).
  • options: Object.
    • clientInfo: Required. { ip: string, userAgent: string }. Usually from getClientInfo(req).
    • expiresInSeconds: Optional. Positive number. Default 3600. Token validity in seconds from issue time.

verify(token, publicKey, currentClientInfo, options?)

  • token: String. The token returned by sign().
  • publicKey: String (base64url Ed25519 public key).
  • currentClientInfo: Required. { ip: string, userAgent: string }. Must match the context used at sign time (use getClientInfo(req)).
  • options: Optional.
    • minIat: Optional. Number (Unix timestamp). If set, tokens with iat < minIat are rejected (replay prevention).
    • revokedJtis: Optional. Set<string> or (jti: string) => boolean. If the token’s jti is in the set or the function returns true, verification fails (revocation / logout).

Request context

| Function | Input | Output | |----------|--------|--------| | getClientInfo(req) | Request-like object with headers, optional socket.remoteAddress | { ip: string, userAgent: string } |

req must have:

  • headers['x-forwarded-for'] or headers['x-real-ip'] or socket.remoteAddress (used for IP).
  • headers['user-agent'] (defaults to '' if missing).

Middleware

| Function | Input | Output | |----------|--------|--------| | createVerifyMiddleware(publicKey, options?) | publicKey string, optional options | Express middleware function |

Middleware behavior:

  • Reads Authorization: Bearer <token>. If missing → 401, no block.
  • If present: runs verify(token, publicKey, getClientInfo(req), options). On success → req.aegiss = payload, next(). On failure → 401 and increments failure count for the IP; after maxFailedAttempts (default 10) failures, the IP is blocked for 15 minutes (subsequent requests get 429).
  • Successful verification resets the failure count for that IP.

Options: minIat, revokedJtis (same as verify), maxFailedAttempts (positive number; default 10).

Utilities and errors

| Function / export | Description | |-------------------|-------------| | encrypt(plaintext, key) | ChaCha20-Poly1305. key: 32-byte Buffer or equivalent. Returns string iv.ciphertext.authTag (base64url, dot-separated). | | decrypt(packed, key) | Inverse of encrypt. Returns Buffer. | | hashFingerprint(input) | SHA-256 of string, 64-char hex. Used internally for context binding. | | toBase64Url(buf) / fromBase64Url(str) | Encoding helpers. | | clearBlockList() | Clears in-memory block and failure-count stores (e.g. for tests). | | isBlocked(ip) | Returns whether the IP is currently blocked. | | VerificationError | Class. All verification failures throw this; message is always "Invalid token". | | CHACHA_KEY_LENGTH, CHACHA_IV_LENGTH, AUTH_TAG_LENGTH, BLOCK_DURATION_MS | Numeric constants. |


Express middleware

const { createVerifyMiddleware, getClientInfo, sign } = require('aegiss-token');

const { publicKey, privateKey } = generateKeys();  // or load from env

// Login: issue token
app.post('/login', (req, res) => {
  const clientInfo = getClientInfo(req);
  const token = sign(
    { userId: req.body.userId },
    privateKey,
    { clientInfo, expiresInSeconds: 3600 }
  );
  res.json({ token });
});

// Protected route: require valid Bearer token
app.get('/api/me', createVerifyMiddleware(publicKey), (req, res) => {
  res.json({ user: req.aegiss.userId });
});

// Optional: revocation (e.g. logout) — pass a Set or async store
const revokedJtis = new Set();
app.post('/logout', createVerifyMiddleware(publicKey, { revokedJtis }), (req, res) => {
  revokedJtis.add(req.aegiss.jti);
  res.json({ ok: true });
});
  • Missing Authorization or invalid token → 401.
  • After 10 failed verifications (same IP) → that IP gets 429 for 15 minutes. Override with maxFailedAttempts.

Encryption

Encryption is separate from tokens (no context binding). Use for symmetric secret data.

const { encrypt, decrypt } = require('aegiss-token');
const crypto = require('crypto');

const key = crypto.randomBytes(32);  // 32 bytes required
const packed = encrypt(Buffer.from('secret data'), key);
const plain = decrypt(packed, key);
  • Key must be 32 bytes.
  • Do not use the same key for tokens; tokens use Ed25519 key pairs.

Security design

  • Algorithms: Ed25519 (signing), SHA-256 (fingerprint), ChaCha20-Poly1305 (encryption helper).
  • Reserved claims: iat, exp, jti, fingerprint are always set by the library; user payload cannot override them.
  • Single error: All verification failures throw VerificationError with message "Invalid token" so attackers cannot distinguish signature, expiry, or context mismatch.
  • Constant-time: Signature and fingerprint comparison use constant-time logic where applicable.
  • Context binding: Token is valid only when the request’s IP and User-Agent match the sign-time context.
  • Replay: Use minIat (e.g. last logout or password change time) to reject older tokens.
  • Revocation: Use revokedJtis (Set or callback) to invalidate specific tokens (e.g. logout).
  • Rate limiting: After N failed verifications (default 10) per IP, that IP is blocked for 15 minutes. Successful verify resets the count for that IP.
  • Input limits: Token length, payload size, and CBOR structure are bounded to reduce DoS risk.

For more hardening ideas (key rotation, path binding, distributed block list, etc.) see SECURITY.md.


Error handling

  • Verification: Always catch VerificationError when calling verify() or when using the middleware (middleware catches it and returns 401). Do not rely on the message text for logic; it is intentionally generic.
  • Sign / encrypt / decrypt: Can throw generic Error for invalid arguments (e.g. missing clientInfo, wrong key length). Use try/catch and return a generic error to the client.

Example:

const { verify, VerificationError } = require('aegiss-token');
try {
  const payload = verify(token, publicKey, getClientInfo(req));
  // use payload
} catch (err) {
  if (err instanceof VerificationError) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  throw err;
}

Constants & limits

| Constant | Value | Meaning | |----------|--------|--------| | Default token TTL | 3600 | expiresInSeconds default (1 hour) | | Max token length | 4096 | Characters; longer tokens rejected | | Max payload size | 8192 | Bytes (CBOR); larger rejected | | Max CBOR map entries | 64 | Per payload | | Max string length (CBOR) | 1024 | Per value | | Block duration | 15 min | After max failed attempts | | Default max failed attempts | 10 | Before blocking IP | | CHACHA_KEY_LENGTH | 32 | Bytes for encrypt/decrypt key | | FINGERPRINT_HEX_LENGTH | 64 | SHA-256 hex length |


Further reading

  • SECURITY.md — Hardening ideas (key rotation, path binding, audit callbacks, distributed block list, threat model).
  • examples/basic-usage.js — Run with node examples/basic-usage.js for sign, verify, revocation, and reserved-claims behavior.
  • examples/express-middleware.js — Minimal Express server with login and protected route.

License

MIT