# Tokens Hashed-secret token primitive with mint-once, TTL, revoke and scope, as a Bijection component - Package: @bijectionhq/tokens - Category: Authentication - Status: Preview - License: MIT - Bijection source (repository access required): https://github.com/bijectionhq/tokens/tree/12601a7a9cfb983b9f28181b0f8bd608726be735 - Upstream: https://github.com/vllnt/convex-tokens/tree/1581eb60769ce391697bf1d5ae243a2a961f1040 - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/tokens ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/tokens.git cd tokens git checkout 12601a7a9cfb983b9f28181b0f8bd608726be735 ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes @vllnt/convex-tokens; check the Bijection adaptation separately. @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](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt). ### Use cases - 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 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 README # @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. ```ts 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-256` by default, `SHA-512` configurable, fixed per mount. - **Server-sourced time** — `expiresAt` and validate-time `now` come from the server clock; expiry can't be spoofed. - **TTL with clamp** — every token expires; a requested `ttlMs` is 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`/`getMetadata` never return the hash. ## Installation ```bash pnpm add @vllnt/convex-tokens ``` Peer dependency: `convex@^1.41.0`. ## Usage ```ts // 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; ``` ```ts // 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](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. ```tsx 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), `validate` returns a discriminated union, and the management surface (`list`, `getMetadata`) never returns the hash. See [docs/API.md](docs/API.md). ## Testing ```bash 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](CONTRIBUTING.md). ## Author Built by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt) Part of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com) If this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt). ## License MIT — see [LICENSE](LICENSE).