Tokens
Hashed-secret token primitive with mint-once, TTL, revoke and scope, as a Bijection component
git clone https://github.com/bijectionhq/tokens.git
cd tokens
git checkout 12601a7a9cfb983b9f28181b0f8bd608726be735Bijection repository access is required. Request access.
Description
Hashed-secret token primitive with mint-once, TTL, revoke and scope, as a Bijection component
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/tokens. Based on vllnt/convex-tokens. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Hashed-secret token primitive with mint-once, TTL, revoke and scope, as a Convex component
Explore the demo ↗Upstream overview
From the upstream package documentation.
@vllnt/convex-tokens provides an expiring secret-token primitive backed by isolated Convex tables. Mint a token against an optional opaque resource reference, return the raw value once, and persist its digest instead of the secret.
mint returns the raw token and its ID. validate checks validity in a scope, and revoke invalidates a token. TTLs are enforced with server time. list and getMetadata expose management information without the token hash, while cleanup removes eligible expired records.
Your host owns authentication, delivery, access policy, and abuse protection. Mint-once means the raw secret is shown once; validation does not consume the token. Flows requiring single-use redemption need atomic host-side coordination. Never log or persist the raw token in your UI.
Maintained by bntvllnt at VLLNT. Find the author on GitHub and X.
Use cases
From the upstream package documentation.
- Resource sharing: issue a time-limited link to a host-authorized resource.
- Account flows: use a token as one part of an email-verification or recovery flow designed by your backend.
- Device or integration setup: associate a temporary token with a pending host-side setup record.
- Temporary access: validate a scoped token and revoke it before its expiry.
How it works
From the upstream package documentation.
mint returns the raw token and its ID. validate checks validity in a scope, and revoke invalidates a token. TTLs are enforced with server time. list and getMetadata expose management information without the token hash, while cleanup removes eligible expired records.
Your host owns authentication, delivery, access policy, and abuse protection. Mint-once means the raw secret is shown once; validation does not consume the token. Flows requiring single-use redemption need atomic host-side coordination. Never log or persist the raw token in your UI.
Upstream package FAQ
Is the raw token ever stored in the Convex database?
No. @vllnt/convex-tokens hashes the raw token client-side using Web Crypto (SHA-256 by default, SHA-512 configurable) before any database write. Only the digest is persisted. The raw token is returned exactly once from tokens.mint() and is never recoverable afterward, so a compromised database row cannot be replayed.
How does token expiry work and can a client extend it?
Expiry is controlled entirely by the server. When minting, a requested ttlMs is clamped server-side into the configured [MIN_TTL_MS, maxTtlMs] range. The expiresAt timestamp and the now value used during validation both come from the server clock, so a client cannot forge or extend a token's lifetime.
How do I install and register @vllnt/convex-tokens in a Convex app?
Run npm install @vllnt/convex-tokens (peer dependency: convex@^1.41.0). In convex/convex.config.ts, import the config with import tokens from '@vllnt/convex-tokens/convex.config' and register it with app.use(tokens). Then instantiate the client in your function files with const tokens = new Tokens(components.tokens).
What does the useTokens React hook return and does it expose the hash?
The useTokens hook from @vllnt/convex-tokens/react wraps the host application's own re-exported list query and returns TokenMetadata[] or undefined. It never returns a raw token or the stored hash. The hook accepts an optional scope and resourceRef for filtering.
Does @vllnt/convex-tokens handle authentication itself?
@vllnt/convex-tokens is auth-agnostic. The component never authenticates the caller. The host application is responsible for resolving identity and passing an opaque resourceRef string into mint and the management methods. This means it works alongside any auth system you already use with Convex.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Tokens with Bijection.
Read: https://bijection.com/components/tokens/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from vllnt/convex-tokens, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@vllnt/convex-tokens
Hashed-secret token primitive with mint-once, TTL, revoke and scope, as a Convex component — mint a secret against an opaque resourceRef, return the raw value once, and store only its SHA-256 hash.
const tokens = new Tokens(components.tokens);
const { token, id } = await tokens.mint(ctx, { resourceRef }); // show `token` once
const result = await tokens.validate(ctx, rawToken); // { valid: true, resourceRef? } | { valid: false }
Features
- Mint-once — the raw token is returned a single time and never persisted; only its digest rests in the component.
- Hash-at-rest — a leaked DB row can't be replayed; digest is
SHA-256by default,SHA-512configurable, fixed per mount. - Server-sourced time —
expiresAtand validate-timenowcome from the server clock; expiry can't be spoofed. - TTL with clamp — every token expires; a requested
ttlMsis clamped server-side into[MIN_TTL_MS, maxTtlMs]. - Revoke — invalidate a token before it expires, via a transition contract.
- Scopes — global by default, or namespace per tenant / purpose / locale.
- Self-pruning cron + safe management surface — sweeps expired/stale-revoked tokens;
list/getMetadatanever return the hash.
Installation
pnpm add @vllnt/convex-tokens
Peer dependency: convex@^1.41.0.
Usage
// convex/convex.config.ts
import { defineApp } from "convex/server";
import tokens from "@vllnt/convex-tokens/convex.config";
const app = defineApp();
app.use(tokens);
export default app;
// convex/api-keys.ts — host owns auth; pass an opaque resourceRef in.
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { Tokens } from "@vllnt/convex-tokens";
const tokens = new Tokens(components.tokens);
export const issue = mutation({
args: { userId: v.string() },
// returns { token, id }; show `token` to the user once — it is never recoverable.
handler: (ctx, { userId }) => tokens.mint(ctx, { resourceRef: userId }),
});
export const check = query({
args: { token: v.string() },
handler: (ctx, { token }) => tokens.validate(ctx, token),
});
API Reference
| Method | Kind | Result |
|---|---|---|
mint(ctx, { scope?, resourceRef?, ttlMs? }) | mutation | { token, id } (raw shown once; TTL clamped server-side) |
validate(ctx, rawToken, scope?) | query | { valid: true, resourceRef? } | { valid: false } |
revoke(ctx, rawToken, scope?) | mutation | boolean (true = transitioned to revoked this call) |
list(ctx, { scope?, resourceRef?, limit? }) | query | TokenMetadata[] (never the hash; limit clamped to 1000) |
getMetadata(ctx, id) | query | TokenMetadata | null (never the hash) |
prune(ctx, before?) | mutation | number (deleted in a bounded batch) |
Full reference: docs/API.md — including client options (defaultScope, defaultTtlMs, maxTtlMs, hashAlgo, …) and the hash-guard error code.
React
Optional, tree-shakeable front-tooling at @vllnt/convex-tokens/react; react and convex are optional peer deps. No-leak: hooks expose only token metadata — never a raw token or hash. Pass the host's own re-exported list query ref.
import { useTokens } from "@vllnt/convex-tokens/react";
import { api } from "./convex/_generated/api";
const tokens = useTokens(api.apiKeys.list, { resourceRef }); // TokenMetadata[] | undefined
| Hook | Wraps | Returns |
|---|---|---|
useTokens(listRef, { scope?, resourceRef }) | host's list query | TokenMetadata[] | undefined (metadata only) |
Security
- Auth-agnostic — the host resolves identity and passes an opaque
resourceRef; the component never authenticates. - Hash-at-rest — the raw token is hashed client-side (Web Crypto, SHA-256 by default); only the digest is stored, so a compromised DB yields no usable token.
- Time is server-sourced (expiry can't be forged),
validatereturns a discriminated union, and the management surface (list,getMetadata) never returns the hash.
See docs/API.md.
Testing
pnpm test # single run
pnpm test:coverage # enforced 100% on covered files
Tests run against the real component runtime via convex-test (@edge-runtime/vm), not mocks.
Contributing
See CONTRIBUTING.md.
Author
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
License
MIT — see LICENSE.