{"id":51317905,"url":"https://github.com/vllnt/convex-tokens","last_synced_at":"2026-07-01T09:31:18.405Z","repository":{"id":364821509,"uuid":"1269289024","full_name":"vllnt/convex-tokens","owner":"vllnt","description":"Hashed-secret token primitive with mint-once, TTL, revoke and scope, as a Convex component","archived":false,"fork":false,"pushed_at":"2026-06-14T15:47:20.000Z","size":124,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T17:23:38.160Z","etag":null,"topics":["convex","convex-component","tokens","vllnt"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@vllnt/convex-tokens","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vllnt.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"bntvllnt"}},"created_at":"2026-06-14T14:29:38.000Z","updated_at":"2026-06-14T15:55:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vllnt/convex-tokens","commit_stats":null,"previous_names":["vllnt/convex-tokens"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/vllnt/convex-tokens","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-tokens","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-tokens/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-tokens/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-tokens/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vllnt","download_url":"https://codeload.github.com/vllnt/convex-tokens/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-tokens/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35001648,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["convex","convex-component","tokens","vllnt"],"created_at":"2026-07-01T09:31:15.399Z","updated_at":"2026-07-01T09:31:18.394Z","avatar_url":"https://github.com/vllnt.png","language":"TypeScript","funding_links":["https://github.com/sponsors/bntvllnt"],"categories":[],"sub_categories":[],"readme":"\u003c!-- Badges --\u003e\n[![Convex Component](https://img.shields.io/badge/convex-component-EE342F.svg)](https://www.convex.dev/components)\n[![npm](https://img.shields.io/npm/v/@vllnt/convex-tokens.svg)](https://www.npmjs.com/package/@vllnt/convex-tokens)\n[![CI](https://github.com/vllnt/convex-tokens/actions/workflows/ci.yml/badge.svg)](https://github.com/vllnt/convex-tokens/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@vllnt/convex-tokens.svg)](./LICENSE)\n\n# @vllnt/convex-tokens\n\nHashed-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.\n\n```ts\nconst tokens = new Tokens(components.tokens);\nconst { token, id } = await tokens.mint(ctx, { resourceRef }); // show `token` once\nconst result = await tokens.validate(ctx, rawToken); // { valid: true, resourceRef? } | { valid: false }\n```\n\n## Features\n\n- **Mint-once** — the raw token is returned a single time and never persisted; only its digest rests in the component.\n- **Hash-at-rest** — a leaked DB row can't be replayed; digest is `SHA-256` by default, `SHA-512` configurable, fixed per mount.\n- **Server-sourced time** — `expiresAt` and validate-time `now` come from the server clock; expiry can't be spoofed.\n- **TTL with clamp** — every token expires; a requested `ttlMs` is clamped server-side into `[MIN_TTL_MS, maxTtlMs]`.\n- **Revoke** — invalidate a token before it expires, via a transition contract.\n- **Scopes** — global by default, or namespace per tenant / purpose / locale.\n- **Self-pruning cron + safe management surface** — sweeps expired/stale-revoked tokens; `list`/`getMetadata` never return the hash.\n\n## Installation\n\n```bash\npnpm add @vllnt/convex-tokens\n```\n\nPeer dependency: `convex@^1.41.0`.\n\n## Usage\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport tokens from \"@vllnt/convex-tokens/convex.config\";\n\nconst app = defineApp();\napp.use(tokens);\nexport default app;\n```\n\n```ts\n// convex/api-keys.ts — host owns auth; pass an opaque resourceRef in.\nimport { components } from \"./_generated/api\";\nimport { mutation, query } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { Tokens } from \"@vllnt/convex-tokens\";\n\nconst tokens = new Tokens(components.tokens);\n\nexport const issue = mutation({\n  args: { userId: v.string() },\n  // returns { token, id }; show `token` to the user once — it is never recoverable.\n  handler: (ctx, { userId }) =\u003e tokens.mint(ctx, { resourceRef: userId }),\n});\n\nexport const check = query({\n  args: { token: v.string() },\n  handler: (ctx, { token }) =\u003e tokens.validate(ctx, token),\n});\n```\n\n## API Reference\n\n| Method | Kind | Result |\n|--------|------|--------|\n| `mint(ctx, { scope?, resourceRef?, ttlMs? })` | mutation | `{ token, id }` (raw shown once; TTL clamped server-side) |\n| `validate(ctx, rawToken, scope?)` | query | `{ valid: true, resourceRef? } \\| { valid: false }` |\n| `revoke(ctx, rawToken, scope?)` | mutation | `boolean` (`true` = transitioned to revoked this call) |\n| `list(ctx, { scope?, resourceRef?, limit? })` | query | `TokenMetadata[]` (never the hash; `limit` clamped to 1000) |\n| `getMetadata(ctx, id)` | query | `TokenMetadata \\| null` (never the hash) |\n| `prune(ctx, before?)` | mutation | `number` (deleted in a bounded batch) |\n\nFull reference: [docs/API.md](docs/API.md) — including client options (`defaultScope`, `defaultTtlMs`, `maxTtlMs`, `hashAlgo`, …) and the hash-guard error code.\n\n## React\n\nOptional, 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.\n\n```tsx\nimport { useTokens } from \"@vllnt/convex-tokens/react\";\nimport { api } from \"./convex/_generated/api\";\n\nconst tokens = useTokens(api.apiKeys.list, { resourceRef }); // TokenMetadata[] | undefined\n```\n\n| Hook | Wraps | Returns |\n|------|-------|---------|\n| `useTokens(listRef, { scope?, resourceRef })` | host's `list` query | `TokenMetadata[] \\| undefined` (metadata only) |\n\n## Security\n\n- Auth-agnostic — the host resolves identity and passes an opaque `resourceRef`; the component never authenticates.\n- 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.\n- Time is server-sourced (expiry can't be forged), `validate` returns a discriminated union, and the management surface (`list`, `getMetadata`) never returns the hash.\n\nSee [docs/API.md](docs/API.md).\n\n## Testing\n\n```bash\npnpm test           # single run\npnpm test:coverage  # enforced 100% on covered files\n```\n\nTests run against the real component runtime via `convex-test` (`@edge-runtime/vm`), not mocks.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Author\n\nBuilt by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt)\n\nPart of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com)\n\nIf this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt).\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvllnt%2Fconvex-tokens","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvllnt%2Fconvex-tokens","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvllnt%2Fconvex-tokens/lists"}