{"id":51317920,"url":"https://github.com/vllnt/convex-consent","last_synced_at":"2026-07-01T09:31:21.325Z","repository":{"id":364821443,"uuid":"1268769656","full_name":"vllnt/convex-consent","owner":"vllnt","description":"Append-only consent ledger (GDPR Art. 6/7) — record, withdraw, and gate consent per subject + purpose as a Convex component","archived":false,"fork":false,"pushed_at":"2026-06-14T15:46:49.000Z","size":108,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T17:23:34.018Z","etag":null,"topics":["consent","convex","convex-component","gdpr","typescript","vllnt"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@vllnt/convex-consent","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-13T23:14:10.000Z","updated_at":"2026-06-14T15:55:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vllnt/convex-consent","commit_stats":null,"previous_names":["vllnt/convex-consent"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/vllnt/convex-consent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-consent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-consent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-consent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-consent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vllnt","download_url":"https://codeload.github.com/vllnt/convex-consent/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-consent/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":["consent","convex","convex-component","gdpr","typescript","vllnt"],"created_at":"2026-07-01T09:31:17.903Z","updated_at":"2026-07-01T09:31:21.313Z","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-consent.svg)](https://www.npmjs.com/package/@vllnt/convex-consent)\n[![CI](https://github.com/vllnt/convex-consent/actions/workflows/ci.yml/badge.svg)](https://github.com/vllnt/convex-consent/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@vllnt/convex-consent.svg)](./LICENSE)\n\n# @vllnt/convex-consent\n\nAn append-only consent ledger (GDPR Art. 6/7), as a Convex component.\n\n```ts\nconst consent = new Consent(components.consent);\nawait consent.record(ctx, subjectRef, \"analytics\", \"granted\");\nconst gate = await consent.check(ctx, subjectRef, \"analytics\"); // { granted, stale, ... }\n```\n\nThe host records a subject's decision for a purpose (`granted` / `denied` /\n`withdrawn`) — each call appends an immutable ledger event (the legal proof) and\nupdates an O(1) current-state projection — then gates processing with `check`. When\nthe policy version changes, a prior grant goes `stale` so the host re-prompts.\nDomain-neutral: a cookie gate, a marketing opt-in, a device-tied consent.\n\n## Features\n\n- **Record-and-gate** — `record` appends an immutable event and updates the projection; `check` is the O(1) runtime gate before processing.\n- **Append-only ledger** — every decision is a `consentEvents` row, never mutated; `history` pages the full trail for an audit or DSR.\n- **Easy withdrawal** — `withdraw` (Art. 7(3)) revokes a held grant; withdrawing something never granted throws a coded error.\n- **Version staleness** — `check` returns `stale: true` when a grant is for an older policy version than `requiredVersion`.\n- **Server-sourced time** — every event's `at` is stamped from the server clock; a caller can never supply a timestamp.\n- **Typed, opaque proof** — `Consent\u003cTProof\u003e` with an optional `proofValidator` narrows the stored evidence at the boundary.\n- **Bounded prune + cron** — a daily cron sweeps superseded ledger events past retention; the live projection is never pruned.\n- **Mount-safe** — correct under multiple named `app.use` mounts (web + marketing consent on one backend), each an isolated sandbox.\n\n## Installation\n\n```bash\npnpm add @vllnt/convex-consent\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 consent from \"@vllnt/convex-consent/convex.config\";\n\nconst app = defineApp();\napp.use(consent);\nexport default app;\n```\n\n```ts\n// convex/consent.ts — host owns auth; pass an opaque subjectRef in.\nimport { components } from \"./_generated/api\";\nimport { mutation, query } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { Consent } from \"@vllnt/convex-consent\";\n\nconst consent = new Consent\u003c{ policyHash: string }\u003e(components.consent, {\n  defaultVersion: \"2024-policy\",\n  proofValidator: v.object({ policyHash: v.string() }).parse,\n});\n\n// Record a decision, gate processing on it, and withdraw — host resolves identity → subjectRef.\nexport const setConsent = mutation({\n  args: { purpose: v.string(), granted: v.boolean(), policyHash: v.string() },\n  handler: async (ctx, { purpose, granted, policyHash }) =\u003e {\n    const subjectRef = await resolveSubject(ctx);\n    await consent.record(ctx, subjectRef, purpose, granted ? \"granted\" : \"denied\", { proof: { policyHash } });\n  },\n});\n\nexport const mayProcess = query({\n  args: { purpose: v.string() },\n  handler: async (ctx, { purpose }) =\u003e {\n    const subjectRef = await resolveSubject(ctx);\n    const gate = await consent.check(ctx, subjectRef, purpose, \"2024-policy\");\n    return gate.granted; // false (gate.stale === true) if they consented to an older policy\n  },\n});\n\nexport const revoke = mutation({\n  args: { purpose: v.string() },\n  handler: async (ctx, { purpose }) =\u003e consent.withdraw(ctx, await resolveSubject(ctx), purpose),\n});\n```\n\nClient options: `new Consent(component, { defaultVersion?, proofValidator? })`.\n\n## API Reference\n\n| Method | Kind | Result |\n|--------|------|--------|\n| `record(ctx, subjectRef, purpose, decision, opts?)` | mutation | `{ at }` (`decision`: `\"granted\" \\| \"denied\" \\| \"withdrawn\"`; `opts`: `{ version?; proof? }`) |\n| `withdraw(ctx, subjectRef, purpose, proof?)` | mutation | `{ at }` |\n| `check(ctx, subjectRef, purpose, requiredVersion?)` | query | `ConsentCheck` (`{ granted; stale; decision; version; at }`) |\n| `getState(ctx, subjectRef, purpose)` | query | `ConsentState \\| null` |\n| `getStatesForSubject(ctx, subjectRef)` | query | `ConsentState[]` |\n| `history(ctx, subjectRef, purpose, paginationOpts)` | query | `PaginationResult\u003cConsentEvent\u003e` |\n| `prune(ctx, opts?)` | mutation | `number` (ledger events removed in the first bounded pass) |\n\nFull reference: [docs/API.md](docs/API.md).\n\n## React\n\nBackend-only — no `./react` entry. Consent state is read through an ordinary\nreactive `useQuery` over the host's re-exported `check` / `getState` refs; consent\nproof is sensitive and gating is server-side, so there is no safe client surface.\n\n## Security\n\n- **Auth-agnostic** — the host authenticates the caller, decides who may record/read a subject's consent, and passes an opaque `subjectRef`; tables are sandboxed.\n- **Append-only** — a recorded decision and its proof can never be silently rewritten; withdrawal is a new event, not an edit.\n- **Server-sourced time** — every event's `at` comes from `Date.now()` in the handler, never the caller; the `proof` is opaque, narrowed by the host validator.\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-consent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvllnt%2Fconvex-consent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvllnt%2Fconvex-consent/lists"}