Skip to main content
The planned permission parity check inventories API paths from mobile Dart clients and satellite request calls, follows shared client imports, and records the matching handlers in scripts/ci/permission-parity.snapshot.json. Each entry identifies the route, exported HTTP method, consumers, owning application, membership helper, named permissions, permission denial expressions and candidate UI pages. Source hashes make changes to consumers and analyzed authorization code visible.

Implementation status

The checker, tests and snapshot described here are pending PR #7019. They are not included in this documentation commit. Run the commands below only after that implementation lands.

Planned commands (pending #7019)

After #7019 lands, use the repository resource queue: ttr resources run — node —test scripts/ci/permission-parity.test.js After adoption, the existing Root script tests (24) job in .github/workflows/turbo-unit-tests.yaml will discover this file through scripts/run-script-tests.js. No separate workflow or deployment is required. The check is subject to the existing workflow admission rules. Regenerate the snapshot after an intentional change: ttr resources run — node scripts/ci/permission-parity.js —write Writing the snapshot does not clear authorization failures. The command returns a nonzero status while unresolved paths or workspace permission checks remain. Review the diff before committing; never regenerate solely to turn CI green. For complete diagnostics from the repository root: node -e “const p = require(’./scripts/ci/permission-parity’); console.log(p.checkInventory(p.buildInventory(process.cwd())).join(‘\n’))“

Interpret failures

The gate rejects an unresolved consumed route, missing membership evidence, missing named permission evidence, missing UI policy evidence, differing API and UI denial expressions, or snapshot drift. An unrecognized authorization pattern is a failed proof, not evidence that the endpoint is exploitable. Membership evidence currently recognizes an awaited getPermissions result with an unconditional return or throw when it is absent. Named permissions must appear in a top-level conditional denial using withoutPermission. Boolean AND and OR remain distinct. Shared route re-exports are followed; returned helpers and helper results with a subsequent rejection branch can contribute evidence. Unused imports, unused functions, ignored helper results and checks inside a nested conditional cannot supply evidence for an otherwise unguarded method. GET evidence cannot authorize POST. Client discovery treats Dart endpoint definitions as possible consumers and checks every exported method of each matching route. Satellite discovery follows static imports, barrel dependencies and literal dynamic imports from source request calls. Shared internal API endpoint builders are included. Tests, generated registries, server route trees and routing configuration are excluded from client discovery. Dynamic values are matched conservatively against route segments; unresolved expressions need review. Workspace checks apply to routes containing a workspace parameter. Public, authentication and user-only routes remain in the inventory but do not receive an invented workspace permission requirement.

Review and adoption

The initial inventory is a strict audit candidate. Do not merge it as a required gate until its unresolved paths, helper contracts and UI policies have been reviewed. It carries no baseline exemptions for missing checks. UI candidates are selected by the longest matching feature path across active app pages. This is a review aid: shared layouts, action-specific controls, ownership rules, board visibility, approval roles, wallet restrictions and parameterized helper policies can require explicit endpoint contracts. Verify those contracts against first-party callers and add behavioral denial and success tests before treating a static match as authorization acceptance. Do not add broad exemptions or remove a client simply to satisfy this checker. Mobile does not send every request to web. api_origins.dart selects owning satellites for several domains, while web retains other contracts. The inventory therefore includes matching satellite implementations and shared handler packages alongside web’s current and retained legacy routes. Matching a path does not prove that a deployment routes traffic to that owner. This check does not execute handlers or prove control-flow dominance, returned HTTP status, row scoping, RLS behavior, deployment identity or complete Dart and TypeScript data flow. A green static check cannot replace runtime authorization tests or security review. During adoption, distinguish missing enforcement from unsupported static analysis and intentional contextual policies.