From ea4f83ae3c5484d23e22c066540baf1ddf0dbf77 Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:54:50 +0200 Subject: [PATCH] docs: drop llms-full.txt, keep only llms.txt Remove the generated llms-full.txt artifact and its generate:llms generator; the curated llms.txt index remains the single AI-discovery file. Claude-Session: https://claude.ai/code/session_01RmQ1xbKKSpjsEEMdMRZ8ye --- AGENTS.md | 2 +- CLAUDE.md | 2 +- llms-full.txt | 808 -------------------------------------- llms.txt | 1 - package.json | 3 +- scripts/generate-llms.mjs | 31 -- 6 files changed, 3 insertions(+), 844 deletions(-) delete mode 100644 llms-full.txt delete mode 100644 scripts/generate-llms.mjs diff --git a/AGENTS.md b/AGENTS.md index 4f44d0d..94b2923 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,5 +95,5 @@ When any of these change, update the corresponding docs in the same commit: | New feature or breaking change | `CHANGELOG.md`, `README.md` Features | | Input validation rules changed | `docs/API.md` Error codes | -Always run `pnpm lint && pnpm build && pnpm test && pnpm generate:llms` before committing docs +Always run `pnpm lint && pnpm build && pnpm test` before committing docs changes. diff --git a/CLAUDE.md b/CLAUDE.md index 4f44d0d..94b2923 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,5 +95,5 @@ When any of these change, update the corresponding docs in the same commit: | New feature or breaking change | `CHANGELOG.md`, `README.md` Features | | Input validation rules changed | `docs/API.md` Error codes | -Always run `pnpm lint && pnpm build && pnpm test && pnpm generate:llms` before committing docs +Always run `pnpm lint && pnpm build && pnpm test` before committing docs changes. diff --git a/llms-full.txt b/llms-full.txt deleted file mode 100644 index 6c953b0..0000000 --- a/llms-full.txt +++ /dev/null @@ -1,808 +0,0 @@ -# @vllnt/convex-permissions — Full Source - -Auto-generated by `pnpm generate:llms`. Do not edit manually. - ---- - -## README.md - - -[![Convex Component](https://img.shields.io/badge/convex-component-EE342F.svg)](https://www.convex.dev/components) -[![npm version](https://img.shields.io/npm/v/@vllnt/convex-permissions.svg)](https://www.npmjs.com/package/@vllnt/convex-permissions) -[![CI](https://github.com/vllnt/convex-permissions/actions/workflows/ci.yml/badge.svg)](https://github.com/vllnt/convex-permissions/actions/workflows/ci.yml) -[![License](https://img.shields.io/npm/l/@vllnt/convex-permissions.svg)](./LICENSE) - -# @vllnt/convex-permissions - -Role-based access control as a Convex component — typed, sandboxed, -runtime-editable roles and grants keyed by opaque refs. - -```ts -// Define a role, assign it, enforce it — all from host functions. -await permissions.defineRole(ctx, { name: "editor", grants: ["doc.read", "doc.edit"] }); -await permissions.assign(ctx, { subjectRef: userId, role: "editor" }); -await permissions.require(ctx, { subjectRef: userId, action: "doc.edit" }); // throws on deny -``` - -## Features - -- **Stored, runtime-editable RBAC** — roles, grants, and assignments live in sandboxed tables; change them with a mutation, no redeploy. -- **Typed end to end** — `Permissions` is generic over your role and action unions. -- **Wildcard grants** — `"doc.*"` grants every `doc.` action; `"*"` grants everything. -- **Scoped / multi-tenant** — assign within an opaque `scopeRef`; unscoped assignments apply globally. -- **Agnostic by construction** — opaque `subjectRef` / `scopeRef`, no auth library, no domain model, no vendor. -- **`require` throws** — `ConvexError` the host maps to a 403. -- **Default-deny** — unknown subjects and unmatched actions are denied. -- **Runs anywhere** — identical on Convex Cloud and self-hosted `convex-backend`. - -## Installation - -```bash -npm install @vllnt/convex-permissions -``` - -Peer dependency: `convex@^1.36.1`. - -## Usage - -```ts -// convex/convex.config.ts -import { defineApp } from "convex/server"; -import permissions from "@vllnt/convex-permissions/convex.config"; - -const app = defineApp(); -app.use(permissions); -export default app; -``` - -```ts -// convex/permissions.ts — instantiate the typed client, then define / assign / check. -import { components } from "./_generated/api"; -import { Permissions } from "@vllnt/convex-permissions"; - -type Role = "admin" | "editor" | "viewer"; -type Action = "doc.read" | "doc.edit" | "doc.delete"; - -export const permissions = new Permissions(components.permissions); - -// from any host mutation/query (gate management behind your own admin auth): -await permissions.defineRole(ctx, { name: "editor", grants: ["doc.read", "doc.edit"] }); -await permissions.assign(ctx, { subjectRef: userId, role: "editor", scopeRef: orgId }); -const allowed = await permissions.check(ctx, { subjectRef: userId, action: "doc.edit" }); -await permissions.require(ctx, { subjectRef: userId, action: "doc.edit" }); // throws on deny -``` - -## API Reference - -| Method | Kind | Result | -|--------|------|--------| -| `defineRole(ctx, { name, grants, description? })` | mutation | Upsert a role (runtime-editable) | -| `removeRole(ctx, name)` | mutation | Delete a role by name | -| `assign(ctx, { subjectRef, role, scopeRef? })` | mutation | Grant a role to a subject | -| `revoke(ctx, { subjectRef, role, scopeRef? })` | mutation | Remove a role from a subject | -| `check(ctx, { subjectRef, action, scopeRef? })` | query | Boolean permission check (default-deny) | -| `require(ctx, { subjectRef, action, scopeRef? })` | query | Enforce access — throws on deny | -| `rolesFor(ctx, { subjectRef, scopeRef? })` | query | List role names for a subject | -| `permissionsFor(ctx, { subjectRef, scopeRef? })` | query | List distinct grants for a subject | -| `listRoles(ctx)` | query | All role definitions | - -Full reference: [docs/API.md](docs/API.md). - -## Security - -- **Host owns auth** — it authenticates the caller, resolves identity to an opaque `subjectRef`, and gates the management methods behind its own admin authorization. -- **Opaque refs only** — `subjectRef`, `scopeRef`, and action keys are arbitrary strings; tables are sandboxed (reached only via the client). -- **Default-deny** — no matching grant ⇒ denied; boundary validation rejects refs not matching `^[A-Za-z0-9_.:-]{1,128}$`. - -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). - - ---- - -## CHANGELOG.md - -# Changelog - -All notable changes to this project are documented here. The format is based on -[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to -[Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [0.1.0] - 2026-06-12 - -### Added - -- Initial release of `@vllnt/convex-permissions` — stored, runtime-editable - role-based access control as a Convex component. -- `Permissions` client class, generic over the host's role and - action unions: `defineRole`, `removeRole`, `assign`, `revoke`, `check`, - `require`, `rolesFor`, `permissionsFor`, `listRoles`. -- Sandboxed `roles` and `assignments` tables keyed by opaque `subjectRef` / - `scopeRef`; the host never reads them directly. -- Wildcard grants (`"doc.*"`, `"*"`), scoped / multi-tenant assignments, - default-deny semantics, and a structured `ConvexError` from - `require`. -- Boundary validation of role names and subject refs. -- 100% end-to-end test coverage via `convex-test` against the real component - runtime. - - ---- - -## docs/API.md - -# API Reference — @vllnt/convex-permissions - -**Compatibility:** `convex@^1.36.1` - -The public surface is the `Permissions` client class (`src/client/index.ts`), -generic over the host's role and action unions: - -```ts -import { Permissions } from "@vllnt/convex-permissions"; - -type Role = "admin" | "editor" | "viewer"; -type Action = "doc.read" | "doc.edit" | "doc.delete"; - -const permissions = new Permissions(components.permissions); -``` - -Every method takes the host `ctx` first. The mutating methods (`defineRole`, -`removeRole`, `assign`, `revoke`) need a mutation `ctx`; the read methods -(`check`, `require`, `rolesFor`, `permissionsFor`, `listRoles`) need a query (or -mutation) `ctx`. - -## Mutations - -Require a mutation `ctx`. - -| Method | Args | Returns | Notes | -|--------|------|---------|-------| -| `defineRole(ctx, { name, grants, description? })` | role name + grant list | `Promise` | Upsert by name — runtime-editable | -| `removeRole(ctx, name)` | role name | `Promise` | `true` if a role was removed | -| `assign(ctx, { subjectRef, role, scopeRef? })` | subject + role (+ scope) | `Promise` | `true` if newly created (idempotent) | -| `revoke(ctx, { subjectRef, role, scopeRef? })` | subject + role (+ scope) | `Promise` | `true` if an assignment was removed | - -## Queries - -Accept a query or mutation `ctx`. - -| Method | Args | Returns | Notes | -|--------|------|---------|-------| -| `check(ctx, { subjectRef, action, scopeRef? })` | subject + action (+ scope) | `Promise` | Default-deny | -| `require(ctx, { subjectRef, action, scopeRef? })` | subject + action (+ scope) | `Promise` | Throws `ConvexError` on deny | -| `rolesFor(ctx, { subjectRef, scopeRef? })` | subject (+ scope) | `Promise` | Role names, sorted | -| `permissionsFor(ctx, { subjectRef, scopeRef? })` | subject (+ scope) | `Promise` | Distinct grants, sorted | -| `listRoles(ctx)` | — | `Promise` | All role definitions | - -`RoleDoc = { _id; _creationTime; name; grants; description?; updatedAt }`. - -## Error codes - -| Code | Thrown by | Description | -|------|-----------|-------------| -| `FORBIDDEN` | `require` | Subject does not hold a matching grant for the requested action | - -`require` throws `ConvexError<{ code: "FORBIDDEN"; subjectRef: string; action: string; scopeRef?: string }>`. - -Boundary validation throws a plain `Error` (not `ConvexError`) when a role name or -subject ref does not match `^[A-Za-z0-9_.:-]{1,128}$`. - -## Grant matching - -- **exact** — `"doc.edit"` authorizes `"doc.edit"` -- **prefix wildcard** — `"doc.*"` authorizes `"doc.read"`, `"doc.edit"`, … -- **global wildcard** — `"*"` authorizes everything (a super-role is just a role - granted `"*"`) - -## Scopes (multi-tenant) - -An assignment with no `scopeRef` is **global** — it applies in every scope. A -scoped assignment applies only when `check` / `require` is called with the same -`scopeRef`. A scoped check therefore considers the subject's global **and** -in-scope roles; an unscoped check considers only global roles. - -## Out of scope - -This component is a **decision engine** (can subject X do action Y?). It does not -answer "list every resource X can see" — relationship/listing queries belong with -your resource data and indexes. Relationship-based access control (ReBAC) is a -candidate for a future major version. - - ---- - -## src/shared.ts - -```ts -/** - * Shared constants, types, and pure utilities for `@vllnt/convex-permissions`. - * Imported by both the component backend and the client wrapper. Pure — no - * Convex `ctx`, no Node APIs — so it runs identically on Cloud and self-hosted. - */ - -/** Component id used by `defineComponent` and the default mount / register name. */ -export const COMPONENT_NAME = "permissions"; - -/** Opaque, host-supplied references. The component never assumes their shape or source. */ -export type SubjectRef = string; -export type ScopeRef = string; - -/** Role names, subject refs, and scope refs must match this token shape (1..128 chars). */ -const TOKEN_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/; - -/** - * Validate an opaque token (role name / subject ref) at the component boundary. - * Throws on invalid input so callers fail fast and loud. - */ -export function assertToken(label: string, value: string): void { - if (!TOKEN_PATTERN.test(value)) { - throw new Error(`${label} must match ${String(TOKEN_PATTERN)}`); - } -} - -/** - * Does a single stored grant authorize an action? - * - exact: `"doc.edit"` authorizes `"doc.edit"` - * - global: `"*"` authorizes everything - * - prefix: `"doc.*"` authorizes `"doc.edit"`, `"doc.read"`, … - */ -export function grantMatchesAction(grant: string, action: string): boolean { - if (grant === action || grant === "*") { - return true; - } - if (grant.endsWith(".*")) { - return action.startsWith(grant.slice(0, -1)); - } - return false; -} - -/** Does any grant in the set authorize the action? */ -export function anyGrantMatches(grants: readonly string[], action: string): boolean { - for (const grant of grants) { - if (grantMatchesAction(grant, action)) { - return true; - } - } - return false; -} - -``` - ---- - -## src/client/types.ts - -```ts -/** Public TypeScript surface for `@vllnt/convex-permissions`. */ - -/** A stored role definition, as returned by `Permissions.listRoles`. */ -export interface RoleDoc { - _id: string; - _creationTime: number; - name: string; - grants: string[]; - description?: string; - updatedAt: number; -} - -``` - ---- - -## src/client/index.ts - -```ts -import { ConvexError } from "convex/values"; -import type { - FunctionArgs, - FunctionReference, - FunctionReturnType, -} from "convex/server"; -import type { RoleDoc } from "./types.js"; - -/** - * The component's function references, as exposed on the host via - * `components.permissions`. Hand-written so the client typechecks without the - * component's generated code — the host supplies the mounted ref. - */ -export interface PermissionsComponent { - mutations: { - defineRole: FunctionReference< - "mutation", - "internal", - { name: string; grants: string[]; description?: string }, - null - >; - removeRole: FunctionReference<"mutation", "internal", { name: string }, boolean>; - assign: FunctionReference< - "mutation", - "internal", - { subjectRef: string; role: string; scopeRef?: string }, - boolean - >; - revoke: FunctionReference< - "mutation", - "internal", - { subjectRef: string; role: string; scopeRef?: string }, - boolean - >; - }; - queries: { - check: FunctionReference< - "query", - "internal", - { subjectRef: string; action: string; scopeRef?: string }, - boolean - >; - rolesFor: FunctionReference< - "query", - "internal", - { subjectRef: string; scopeRef?: string }, - string[] - >; - permissionsFor: FunctionReference< - "query", - "internal", - { subjectRef: string; scopeRef?: string }, - string[] - >; - listRoles: FunctionReference< - "query", - "internal", - Record, - RoleDoc[] - >; - }; -} - -interface RunQueryCtx { - runQuery>( - reference: Q, - args: FunctionArgs, - ): Promise>; -} - -interface RunMutationCtx { - runMutation>( - reference: M, - args: FunctionArgs, - ): Promise>; -} - -/** Structured payload thrown by `Permissions.require` on an authorization failure. */ -export interface PermissionDenied { - code: "FORBIDDEN"; - subjectRef: string; - action: string; - scopeRef?: string; -} - -/** - * Consumer-facing client for the permissions component. Construct with the - * mounted component ref, then call from host queries / mutations. The host owns - * auth — it resolves identity and passes an opaque `subjectRef` in. Generic over - * the host's role + action unions so checks are typed end to end. - * - * @example - * ```ts - * type Role = "admin" | "editor" | "viewer"; - * type Action = "doc.read" | "doc.edit" | "doc.delete"; - * const permissions = new Permissions(components.permissions); - * await permissions.require(ctx, { subjectRef: userId, action: "doc.edit" }); - * ``` - */ -export class Permissions< - TRole extends string = string, - TAction extends string = string, -> { - constructor(private readonly component: PermissionsComponent) {} - - /** Create or update a role definition (upsert by name). */ - defineRole( - ctx: RunMutationCtx, - role: { name: TRole; grants: readonly (TAction | string)[]; description?: string }, - ): Promise { - return ctx.runMutation(this.component.mutations.defineRole, { - name: role.name, - grants: [...role.grants], - description: role.description, - }); - } - - /** Delete a role definition. Resolves true if a role was removed. */ - removeRole(ctx: RunMutationCtx, name: TRole): Promise { - return ctx.runMutation(this.component.mutations.removeRole, { name }); - } - - /** Assign a role to a subject. Resolves true if newly created. */ - assign( - ctx: RunMutationCtx, - args: { subjectRef: string; role: TRole; scopeRef?: string }, - ): Promise { - return ctx.runMutation(this.component.mutations.assign, args); - } - - /** Remove a role assignment. Resolves true if an assignment was removed. */ - revoke( - ctx: RunMutationCtx, - args: { subjectRef: string; role: TRole; scopeRef?: string }, - ): Promise { - return ctx.runMutation(this.component.mutations.revoke, args); - } - - /** Is the subject authorized to perform the action? */ - check( - ctx: RunQueryCtx, - args: { subjectRef: string; action: TAction; scopeRef?: string }, - ): Promise { - return ctx.runQuery(this.component.queries.check, args); - } - - /** - * Enforce an action. Throws `ConvexError` when the subject is - * not authorized — the host can map this to a 403. - */ - async require( - ctx: RunQueryCtx, - args: { subjectRef: string; action: TAction; scopeRef?: string }, - ): Promise { - const allowed = await this.check(ctx, args); - if (!allowed) { - // `ConvexError` data must be a Convex `Value`; an inline object literal - // satisfies that (a named interface would not, lacking an index signature). - // The shape matches `PermissionDenied`, which consumers use to type catches. - throw new ConvexError({ - code: "FORBIDDEN", - subjectRef: args.subjectRef, - action: args.action, - scopeRef: args.scopeRef, - }); - } - } - - /** Role names the subject holds in the optional scope (sorted). */ - rolesFor( - ctx: RunQueryCtx, - args: { subjectRef: string; scopeRef?: string }, - ): Promise { - return ctx.runQuery(this.component.queries.rolesFor, args); - } - - /** Distinct grants the subject has in the optional scope (sorted). */ - permissionsFor( - ctx: RunQueryCtx, - args: { subjectRef: string; scopeRef?: string }, - ): Promise { - return ctx.runQuery(this.component.queries.permissionsFor, args); - } - - /** All role definitions. */ - listRoles(ctx: RunQueryCtx): Promise { - return ctx.runQuery(this.component.queries.listRoles, {}); - } -} - -export type { RoleDoc }; - -``` - ---- - -## src/component/schema.ts - -```ts -import { defineSchema, defineTable } from "convex/server"; -import { v } from "convex/values"; - -/** - * Sandboxed tables — the component's own concern only. Subjects, scopes, and - * actions are opaque host-supplied strings; the component never models the - * host's domain or reaches into host / sibling tables. - */ -export default defineSchema({ - // Runtime-editable role definitions: a role name → the grants it confers. - roles: defineTable({ - name: v.string(), - grants: v.array(v.string()), - description: v.optional(v.string()), - updatedAt: v.number(), - }).index("by_name", ["name"]), - - // Who holds which role, optionally within an opaque scope (multi-tenant). - // A row with no scopeRef is global (applies in every scope). - assignments: defineTable({ - subjectRef: v.string(), - role: v.string(), - scopeRef: v.optional(v.string()), - createdAt: v.number(), - }) - .index("by_subject", ["subjectRef"]) - .index("by_role", ["role"]) - .index("by_subject_role_scope", ["subjectRef", "role", "scopeRef"]), -}); - -``` - ---- - -## src/component/validators.ts - -```ts -import { v } from "convex/values"; - -/** Public shape of a stored role definition, returned by the `listRoles` query. */ -export const roleDoc = v.object({ - _id: v.id("roles"), - _creationTime: v.number(), - name: v.string(), - grants: v.array(v.string()), - description: v.optional(v.string()), - updatedAt: v.number(), -}); - -``` - ---- - -## src/component/mutations.ts - -```ts -import { v } from "convex/values"; -import { mutation } from "./_generated/server.js"; -import { assertToken } from "../shared.js"; - -/** Create or update a role definition (upsert by name). Idempotent. */ -export const defineRole = mutation({ - args: { - name: v.string(), - grants: v.array(v.string()), - description: v.optional(v.string()), - }, - returns: v.null(), - handler: async (ctx, args) => { - assertToken("role name", args.name); - const existing = await ctx.db - .query("roles") - .withIndex("by_name", (q) => q.eq("name", args.name)) - .unique(); - if (existing !== null) { - await ctx.db.patch(existing._id, { - grants: args.grants, - description: args.description, - updatedAt: Date.now(), - }); - return null; - } - await ctx.db.insert("roles", { - name: args.name, - grants: args.grants, - description: args.description, - updatedAt: Date.now(), - }); - return null; - }, -}); - -/** Delete a role definition. Returns true if a role was removed. */ -export const removeRole = mutation({ - args: { name: v.string() }, - returns: v.boolean(), - handler: async (ctx, args) => { - const existing = await ctx.db - .query("roles") - .withIndex("by_name", (q) => q.eq("name", args.name)) - .unique(); - if (existing === null) { - return false; - } - await ctx.db.delete(existing._id); - return true; - }, -}); - -/** Assign a role to a subject, optionally scoped. Returns true if newly created. */ -export const assign = mutation({ - args: { - subjectRef: v.string(), - role: v.string(), - scopeRef: v.optional(v.string()), - }, - returns: v.boolean(), - handler: async (ctx, args) => { - assertToken("subjectRef", args.subjectRef); - assertToken("role", args.role); - const existing = await ctx.db - .query("assignments") - .withIndex("by_subject_role_scope", (q) => - q - .eq("subjectRef", args.subjectRef) - .eq("role", args.role) - .eq("scopeRef", args.scopeRef), - ) - .unique(); - if (existing !== null) { - return false; - } - await ctx.db.insert("assignments", { - subjectRef: args.subjectRef, - role: args.role, - scopeRef: args.scopeRef, - createdAt: Date.now(), - }); - return true; - }, -}); - -/** Remove a role assignment. Returns true if an assignment was removed. */ -export const revoke = mutation({ - args: { - subjectRef: v.string(), - role: v.string(), - scopeRef: v.optional(v.string()), - }, - returns: v.boolean(), - handler: async (ctx, args) => { - const existing = await ctx.db - .query("assignments") - .withIndex("by_subject_role_scope", (q) => - q - .eq("subjectRef", args.subjectRef) - .eq("role", args.role) - .eq("scopeRef", args.scopeRef), - ) - .unique(); - if (existing === null) { - return false; - } - await ctx.db.delete(existing._id); - return true; - }, -}); - -``` - ---- - -## src/component/queries.ts - -```ts -import { v } from "convex/values"; -import { query, type QueryCtx } from "./_generated/server.js"; -import { roleDoc } from "./validators.js"; -import { anyGrantMatches } from "../shared.js"; - -/** Role names assigned to a subject that apply in the given scope. */ -async function applicableRoleNames( - ctx: QueryCtx, - subjectRef: string, - scopeRef: string | undefined, -): Promise> { - const assignments = await ctx.db - .query("assignments") - .withIndex("by_subject", (q) => q.eq("subjectRef", subjectRef)) - .collect(); - const names = new Set(); - for (const assignment of assignments) { - // Global (unscoped) assignments apply everywhere; scoped ones only in-scope. - if (assignment.scopeRef === undefined || assignment.scopeRef === scopeRef) { - names.add(assignment.role); - } - } - return names; -} - -/** All grants conferred by a set of role names. */ -async function grantsForRoleNames( - ctx: QueryCtx, - roleNames: Set, -): Promise { - if (roleNames.size === 0) { - return []; - } - const roles = await ctx.db.query("roles").collect(); - const grants: string[] = []; - for (const role of roles) { - if (roleNames.has(role.name)) { - grants.push(...role.grants); - } - } - return grants; -} - -/** Is the subject authorized to perform the action (in the optional scope)? */ -export const check = query({ - args: { - subjectRef: v.string(), - action: v.string(), - scopeRef: v.optional(v.string()), - }, - returns: v.boolean(), - handler: async (ctx, args) => { - const roleNames = await applicableRoleNames(ctx, args.subjectRef, args.scopeRef); - if (roleNames.size === 0) { - return false; // default-deny - } - const grants = await grantsForRoleNames(ctx, roleNames); - return anyGrantMatches(grants, args.action); - }, -}); - -/** Role names a subject holds in the optional scope (sorted). */ -export const rolesFor = query({ - args: { subjectRef: v.string(), scopeRef: v.optional(v.string()) }, - returns: v.array(v.string()), - handler: async (ctx, args) => { - const names = await applicableRoleNames(ctx, args.subjectRef, args.scopeRef); - return [...names].sort(); - }, -}); - -/** Distinct grants a subject has in the optional scope (sorted). */ -export const permissionsFor = query({ - args: { subjectRef: v.string(), scopeRef: v.optional(v.string()) }, - returns: v.array(v.string()), - handler: async (ctx, args) => { - const names = await applicableRoleNames(ctx, args.subjectRef, args.scopeRef); - const grants = await grantsForRoleNames(ctx, names); - return [...new Set(grants)].sort(); - }, -}); - -/** All role definitions. */ -export const listRoles = query({ - args: {}, - returns: v.array(roleDoc), - handler: async (ctx) => { - return await ctx.db.query("roles").collect(); - }, -}); - -``` - ---- - -## src/test.ts - -```ts -/// -import type { TestConvex } from "convex-test"; -import schema from "./component/schema.js"; - -const modules = import.meta.glob("./component/**/*.ts"); - -/** - * Register this component with a `convex-test` instance so consuming apps can - * test integration: `import { register } from "@vllnt/convex-permissions/test"`. - */ -export function register(t: TestConvex, name = "permissions"): void { - t.registerComponent(name, schema, modules); -} - -``` diff --git a/llms.txt b/llms.txt index f511701..5495fc0 100644 --- a/llms.txt +++ b/llms.txt @@ -31,4 +31,3 @@ library, no domain model, no vendor — and runs identically on Convex Cloud and - [Security Policy](SECURITY.md): Vulnerability reporting policy - [Code of Conduct](CODE_OF_CONDUCT.md): Community participation guidelines - [License](LICENSE): MIT license -- [Full source bundle](llms-full.txt): Aggregated README, changelog, API docs, and key source files diff --git a/package.json b/package.json index 2ef9e62..86c3cc9 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,7 @@ "test": "vitest run --passWithNoTests", "test:watch": "vitest --typecheck --clearScreen false", "test:coverage": "vitest run --coverage --coverage.reporter=text", - "generate:llms": "node scripts/generate-llms.mjs", - "preversion": "pnpm install --frozen-lockfile && pnpm build && pnpm test:coverage && pnpm typecheck && pnpm generate:llms", + "preversion": "pnpm install --frozen-lockfile && pnpm build && pnpm test:coverage && pnpm typecheck", "prepublishOnly": "npm whoami || npm login", "alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags", "release": "npm version patch && npm publish && git push --follow-tags" diff --git a/scripts/generate-llms.mjs b/scripts/generate-llms.mjs deleted file mode 100644 index f4cdaf9..0000000 --- a/scripts/generate-llms.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; - -// Files bundled into llms-full.txt, in order. Keep in sync with the public surface. -const files = [ - "README.md", - "CHANGELOG.md", - "docs/API.md", - "src/shared.ts", - "src/client/types.ts", - "src/client/index.ts", - "src/component/schema.ts", - "src/component/validators.ts", - "src/component/mutations.ts", - "src/component/queries.ts", - "src/test.ts", -]; - -let out = `# @vllnt/convex-permissions — Full Source\n\nAuto-generated by \`pnpm generate:llms\`. Do not edit manually.\n`; - -for (const f of files) { - out += `\n---\n\n## ${f}\n\n`; - if (f.endsWith(".md")) { - out += readFileSync(f, "utf8"); - } else { - out += "```ts\n" + readFileSync(f, "utf8") + "\n```"; - } - out += "\n"; -} - -writeFileSync("llms-full.txt", out); -console.log(`Wrote llms-full.txt (${files.length} files)`);