Skip to content

feat: add dependency-free permission-expression evaluator - #362

Merged
Lakes41 merged 1 commit into
Adamantine-guild:mainfrom
angelraph:feat/permission-expression-evaluator
Aug 26, 2026
Merged

feat: add dependency-free permission-expression evaluator#362
Lakes41 merged 1 commit into
Adamantine-guild:mainfrom
angelraph:feat/permission-expression-evaluator

Conversation

@angelraph

Copy link
Copy Markdown
Contributor

Description

Implements a dependency-free, pure evaluator for compound boolean permission expressions, as an isolated primitive independent of the broader GuildPass policy engine.

Adds @guildpass/permission-expression with:

  • A strongly typed discriminated-union AST: permission (leaf), all, any, not.
  • parsePermissionExpression(input, limits?) — validates untrusted runtime input (not just compile-time-typed input) against the grammar: rejects unknown node types, non-object nodes, empty/non-string permission values, and non-array children. Enforces configurable maxDepth and maxNodes limits, checked before recursing into children, so oversized fan-out or cyclic/self-referential input is rejected with bounded work instead of exhausting memory or the call stack.
  • evaluatePermissionExpression(expression, grantedPermissions, { limits?, explain? }) — pure, deterministic, side-effect free. Accepts a Set, array, or any iterable of granted permission strings. No eval, no Function constructor, no dynamic code execution.
  • explain: true returns the specific leaves responsible for a denial, including through not (the granted-but-forbidden leaf that triggered the negation).
  • Empty all is vacuously true; empty any is vacuously false — both explicitly defined and unit tested.
  • Zero runtime dependencies; no knowledge of memberships, Prisma models, Fastify requests, or Redis.

Linked Issue

Closes #348

Type of Change

  • 🐛 Bug fix (API or policy engine)
  • ✨ New feature / endpoint
  • 📝 Documentation / OpenAPI spec update
  • 🔧 Chore / refactor / dependency update
  • 🧪 Tests only
  • ⛓️ Smart contract change (requires extra review)

Changes Made

  • packages/permission-expression/src/index.ts — AST types, parsePermissionExpression, evaluatePermissionExpression, PermissionExpressionError.
  • packages/permission-expression/src/index.test.ts — 36 unit tests: leaf checks, all/any/not semantics, empty-collection semantics, nested combinations, malformed-input rejection, depth/node-count limits (including a 200k-wide fan-out and a self-referential cyclic node), and explain mode.
  • packages/permission-expression/package.json, tsconfig.json — package scaffolding matching existing packages (quorum-engine, rate-limit).
  • pnpm-lock.yaml — workspace registration for the new package.

Test Evidence

pnpm --filter @guildpass/permission-expression test

▶ evaluatePermissionExpression - leaf permissions (2/2)
▶ evaluatePermissionExpression - all (2/2)
▶ evaluatePermissionExpression - any (2/2)
▶ evaluatePermissionExpression - not (2/2)
▶ evaluatePermissionExpression - nesting (3/3)
▶ parsePermissionExpression - malformed input rejection (15/15)
▶ parsePermissionExpression - limits (5/5)
▶ evaluatePermissionExpression - explain mode (5/5)

ℹ tests 36
ℹ pass 36
ℹ fail 0

Full workspace (pnpm typecheck, pnpm build, pnpm test) also verified green across all existing packages and apps/api — no regressions.

General Checklist

  • I have read CONTRIBUTING.md
  • This PR is linked to an open issue
  • pnpm typecheck passes
  • pnpm build passes
  • pnpm test passes — all tests green
  • Prisma schema changes include a migration file (N/A — no schema changes)
  • New API endpoints are documented in OpenAPI (N/A — no API endpoints)
  • No secrets, keys, or wallet addresses introduced
  • .env.example updated (N/A — no new env vars)
  • Documentation updated if new behaviour was introduced (doc comments in source)

Additional Notes

Deliberately scoped to stay independent of the wider policy engine: no membership lookups, database queries, or access API. Callers supply a set of granted permissions and an expression and receive a result.

Adds @guildpass/permission-expression, a pure evaluator for a
constrained boolean permission-expression AST (permission/all/any/not).

- Runtime-validates untrusted input against the AST grammar, rejecting
  malformed nodes and enforcing configurable max depth / max node count
  before any evaluation happens, bounding pathological and cyclic input.
- No eval, no Function constructor, no dynamic code execution.
- Deterministic, side-effect free evaluation with an explain mode that
  reports which leaves caused a denial.
- No knowledge of memberships, Prisma, Fastify or Redis.

Closes Adamantine-guild#348
@angelraph

Copy link
Copy Markdown
Contributor Author

@Lakes41 PR is ready for review — closes #348. All acceptance criteria are met: typed discriminated-union AST (permission/all/any/not), runtime validation with configurable maxDepth/maxNodes limits, deterministic side-effect-free evaluation with explain mode, no eval/Function usage, zero dependencies. pnpm typecheck, pnpm build, and pnpm test all pass workspace-wide (36 new tests, no regressions).

@angelraph

Copy link
Copy Markdown
Contributor Author

@Lakes41 Following up — PR #362 (closes #348) has been open with green CI and no review activity yet. Happy to address any feedback whenever you get a chance to take a look.

@angelraph

Copy link
Copy Markdown
Contributor Author

@Lakes41 Checking in again — PR #362 (closes #348) is still open with green CI and no review yet. Let me know if anything needs changes.

@Lakes41
Lakes41 merged commit 83f9b50 into Adamantine-guild:main Aug 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build a dependency-free permission expression evaluator for GuildPass rules

2 participants