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

@fennecstudio/x-tkn-js

v2.0.0

Published

SDK for the X-TKN API — stateful tokens as a service

Readme

@fennecstudio/x-tkn-js

TypeScript/JavaScript client for X-TKN — stateful tokens as a service. Mint a token, hand the code to someone, and control how many times and for how long it can be redeemed.

Get an API key by signing up at x-tkn.com.

npm install @fennecstudio/x-tkn-js

Node 18+, or any runtime with a global fetch — browsers, Deno, Bun, Cloudflare Workers, Vercel Edge. No dependencies.

Upgrading from 1.x? The package moved — 1.x was published as unscoped x-tkn, and v2 lives here under @fennecstudio/x-tkn-js. Bumping a version will not find it; you have to change the dependency name. v2 is also a breaking change, and 1.x does not work against the current API either way. See Migrating from v1.

Quickstart

import { xtkn } from '@fennecstudio/x-tkn-js'

const token = await xtkn.createToken({
  type: 'password-reset',
  refId: user.id,
  maxUses: 1,
  expiresIn: { minutes: 30 },
})

// token.code is returned once and never again. Persist or send it now.
await sendEmail(user.email, `https://app.example.com/reset?c=${token.code}`)

…and on the other side:

import { xtkn, XTknGoneError, XTknNotFoundError } from '@fennecstudio/x-tkn-js'

try {
  const token = await xtkn.redeemToken(code, { refId: user.id, type: 'password-reset' })
  await resetPassword(user, token.payload)
} catch (err) {
  if (err instanceof XTknGoneError) return render('That link has already been used.')
  if (err instanceof XTknNotFoundError) return render('That link is not valid.')
  throw err
}

Authentication

The key is read on every request, so it can be set after import.

X_TKN_API_KEY=your_key_here

Or pass it explicitly — necessary if you talk to more than one account:

import { XTkn } from '@fennecstudio/x-tkn-js'

const client = new XTkn({ apiKey: process.env.MY_KEY })

Keep the key server-side. It grants full token CRUD for your account, so a key in a browser bundle is a published key. v1 read REACT_APP_- and VUE_APP_-prefixed variables, whose entire purpose is to inline a value into a browser bundle; v2 does not.

Options

| Option | Default | | | ----------- | ---------------------------------------- | ---------------------------------------------------------- | | apiKey | X_TKN_API_KEY, then X_TKN_API_KEY_ID | Resolved per request | | baseUrl | https://api.x-tkn.com | | | timeoutMs | 30000 | 0 disables | | fetch | globalThis.fetch | For tests or a custom agent | | headers | — | Merged into every request; cannot override Authorization |

Methods

Every method throws on failure — see Errors. code is the token's public identifier, returned by createToken.

createToken(input?)

const token = await xtkn.createToken({
  type: 'handoff', // [a-z0-9_-], ≤64 chars, defaults to 'generic'
  refId: 'user_123', // your own identifier, ≤256 chars
  payload: { role: 'admin' }, // JSON-encoded, ≤64 KB
  maxUses: 1, // 1–1,000,000. omit for unlimited
  description: 'Admin invite', // operator-facing note
  expiresIn: { hours: 2 }, // or expiresAt: new Date(...) — not both
})

token.code is the only copy. The server stores sha256(code) and cannot return it later; every subsequent read leaves the field undefined.

With no expiresAt or expiresIn, the server expires the token 30 days after creation.

readToken(code)

Returns the token without consuming a use. Check isActive, isExpired, isUsed.

redeemToken(code, options?)

Consumes one use and returns the token. Throws XTknGoneError if it is revoked, expired or exhausted; XTknNotFoundError if the code is wrong.

await xtkn.redeemToken(code, { refId: user.id, type: 'handoff' })

Pass both refId and type where you can. refId confines the lookup to the identity the code was issued for, which is what bounds guessing; type stops the redemption consuming a different kind of code held by the same identity.

updateToken(code, updates)

Changes type, refId, payload, maxUses or the expiry. description is not updatable.

extendExpiration(code, duration)

Moves the expiry to duration from now, not from the existing expiry — so this revives an already-expired token rather than extending from a past date.

await xtkn.extendExpiration(sessionCode, { hours: 2 })

revokeToken(code) / revokeTokens(filter)

await xtkn.revokeToken(code)

// Log a user out everywhere. One call revokes a bounded batch.
let result
do {
  result = await xtkn.revokeTokens({ refId: user.id, type: 'session' })
} while (result.hasMore)

revokeTokens requires type or refId. An empty filter means "revoke everything", which is not allowed by omission.

deleteToken(code)

Permanent. Prefer revokeToken — a revoked token can still explain why a redemption failed, a deleted one is indistinguishable from one that never existed.

listTokens(options?)

const { tokens, count } = await xtkn.listTokens({
  where: { refId: 'user_123', isRevoked: false },
  sort: '-createdAt',
  page: 1,
  limit: 50, // capped at 100
})

count is the total matching the filter, not tokens.length.

Only type, refId and isRevoked are filterable, and the server silently drops anything else — so a typo widens the result rather than erroring. sort accepts createdAt, updatedAt, expiresAt, lastUsedAt or uses, each optionally prefixed with -; anything else is silently replaced with -createdAt, which the TokenSort type makes unreachable from TypeScript.

Codes are not exposed as a filter. The API accepts one and hashes it to match sha256(code), but it can only return the single token you already hold the code for — readToken(code) does that directly.

Errors

Every non-2xx response throws. Catch XTknError for all of them, or a subclass to tell them apart.

| Class | Status | Means | | --------------------- | -------- | ------------------------------------------------- | | XTknRequestError | 400 | Malformed request or failed validation | | XTknAuthError | 401, 403 | Key missing, unknown or revoked | | XTknNotFoundError | 404 | No such token on this account | | XTknGoneError | 410 | Exists but spent: revoked, expired or out of uses | | XTknRateLimitError | 429 | Hourly guard or monthly quota exhausted | | XTknServerError | 5xx | | | XTknConnectionError | — | Never reached the server: DNS, reset, timeout | | XTknConfigError | — | Bad arguments; thrown before any request |

Each carries status, and details when the API supplied a field-level map.

Payload encryption

The server never needs to read your payload, and accounts with requireEncryptedPayload set reject anything that is not already an xtkn.v1. or xtkn.v1r. envelope.

This SDK does not encrypt for you yet. Pass an already-encrypted string as payload if your account enforces it. Local encryption is planned; until it lands, the envelope format is documented in the X-TKN crypto contract.

Migrating from v1

1.x returns the HTTP envelope instead of the token, never throws on an error response, and calls routes with a parameter the API renamed — so it does not work against the current API. There is no compatibility shim; the changes are mechanical.

| v1 | v2 | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | npm install x-tkn | npm install @fennecstudio/x-tkn-js — a different package, not a version bump | | setup(apiKey) | new XTkn({ apiKey }), or X_TKN_API_KEY. v1's setup() assigned its argument to the base URL, so it never set a key at all | | import { createToken } from 'x-tkn' | import { xtkn } from '@fennecstudio/x-tkn-js'xtkn.createToken(...) | | Any tokenId argument | code | | Result was { success, data } | The token itself | | A 401/404/410 arrived as data | Throws a typed error | | createSecurityToken(refId, ttl) | createToken({ refId, maxUses: 1, expiresIn: ttl }) | | createSessionToken(refId, ttl, payload, type) | createToken({ refId, payload, type, expiresIn: ttl }) | | createShortCode(...) | Removed. The API no longer issues short codes | | redeemToken(id, shortCode) | redeemToken(code, { refId, type }) | | listTokens(where, orderBy, skip, take) | listTokens({ where, sort, page, limit }) | | revokeTokens(where) | revokeTokens({ type, refId }) | | listTokens filtering by code | Not wrapped. Use readToken(code) | | REACT_APP_ / VUE_APP_ key variables | Removed — see Authentication | | deleteToken returned the response body | Returns void |

ttl objects carry over unchanged as expiresIn, and seconds now works — v1 documented it and had no branch for it.

License

ISC