agentleFS
JoviDeCroockJoviDeCroock/pracht/skills/audit-auth/SKILL.md

audit-auth

Skill106 starsChanged 7 months agoMITView on GitHub

Find pracht routes and API mutations that look protected but are not: missing or non-gating auth middleware and client-only checks. Use for "audit auth", "check route protection", "find unauthenticated routes", "review middleware coverage".

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 1 is distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Manifest router: read the name→file map from src/routes.ts (or the configured manifest), where middleware is registered through defineApp({ …

    line 53
  2. Very specific:

    Pages router: when the Vite config sets pagesDir, the generated name "pages" resolves to the root <pagesDir>/_middleware.ts (or .tsx, .js, …

    line 56
  3. Very specific:

    If only an Augmenter is present, mark as augmented-only.

    line 116
  4. Very specific:

    Treat context.agent as framework-owned, read-only verified identity.

    line 144
  5. Very specific:

    When a custom adapter supplies a frozen or sealed context, flag authorization helpers that read agent or middleware-added fields through …

    line 147
  6. Very specific:

    Every module in src/server-islands/ (or pracht({ serverIslandsDir })) is reachable at GET /__pracht/server-island with the visitor's …

    line 174
  7. Distinctive:

    Do not auto-add middleware.

    line 254
  8. Very specific:

    Treat allowed composed capability reachability as transitive.

    line 255

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Pracht Audit Auth

What the framework guarantees: middleware runs wrap-around style — every middleware must return a Response (the runtime throws if it doesn't), either return next() to continue the chain or a short-circuit Response to stop it. The redirect() helper from @pracht/core returns a scheme/CRLF-validated redirect Response. What the framework does NOT decide is which routes get an auth gate — that is app wiring, and this skill audits it.

The pracht auth pattern (see https://pracht.resynapse.dev/docs/recipes/auth): middleware loads the session onto context.session and short-circuits with a redirect when there is no user; loaders downstream read context.session.

@pracht/session is the first-party implementation. Two of its exports map straight onto the Gate/Augmenter classification below — requireSession() is a Gate, sessionMiddleware() is an Augmenter — so a project using it can be classified from the factory name without reading the middleware body. A project that hand-rolls its session instead is not automatically wrong, but check it for the things the package handles: an expiry inside the signed payload (not only Max-Age), a constant-time or crypto.subtle.verify signature check, HttpOnly/Secure/SameSite, and encryption if the cookie carries anything beyond an opaque id.

Prerequisites: pracht inspect requires a vite config that registers the pracht plugin.

Step 1: Identify the auth middleware(s)

pracht inspect routes --json

MCP: if pracht dev-mcp is registered, prefer its inspect_*, doctor, and verify tools to the CLI.

inspect reports middleware names, not files. Resolve them according to the app's router mode, then read each middleware file and classify it:

  • Manifest router: read the name→file map from src/routes.ts (or the configured manifest), where middleware is registered through defineApp({ middleware: { auth: () => import("./middleware/auth.ts") } }).
  • Pages router: when the Vite config sets pagesDir, the generated name "pages" resolves to the root <pagesDir>/_middleware.ts (or .tsx, .js, or .jsx). It applies to every page route and never wraps API routes. There is no src/routes.ts manifest to inspect unless the app has ejected. Follow imports and re-exports into underscore-reserved helpers such as <pagesDir>/_server/auth.ts; pracht excludes those helpers from client route/shell registries, but a direct import from client code still bundles them and should be treated as a server-code leak.

Then classify each resolved middleware module:

  • Gate — on auth failure, returns a short-circuit Response (redirect("/login", { request }), or a 401/403 Response) WITHOUT calling next(); on success, return next(). requireSession() from @pracht/session is one.
  • Augmenter — puts user info on context (or, in older code, on request headers), then always returns next(). Never short-circuits. sessionMiddleware() is one.
  • Other — non-auth middleware (rate limit, logging, CORS, etc.).

Flag any middleware that writes identity onto args.request.headers: the client controls request headers, so a loader reading x-user-id back out is trusting attacker-supplied input, and the write throws outright on Cloudflare Workers where the incoming Request is immutable. Identity belongs on context.

The "Augmenter" category is the silent killer: it makes loaders think auth is enforced because request.headers.get('x-user-id') returns a value when present, but unauthenticated requests just get null and the loader has to handle it. Flag every loader downstream of an Augmenter that doesn't.

A shell loader that redirects anonymous users is not a gate either. A client that already holds the shell's data sends x-pracht-shell-data: <shell> on route-state requests and the server skips the shell loader, so the route's data comes back without it running. Flag any shell whose loader is the only thing between an anonymous request and the routes under it; the gate belongs in middleware.

Step 2: Identify protected routes

A route is "expected protected" if any of:

  • It has auth/session/requireUser/similar middleware applied.
  • Its loader reads context.session, getSession, or (legacy) x-user-id/x-user-email.
  • It lives under conventional protected paths: /dashboard*, /admin*, /account*, /settings*, /app* (ask the user to confirm the convention if unclear).
  • The user has flagged it explicitly.

Build a list of expected-protected routes.

Step 3: Check coverage per protected route

For each expected-protected route:

  1. From pracht inspect routes --json, read the resolved middleware array.
  2. Confirm at least one Gate middleware is present.
  3. Confirm the gate runs before any other middleware that depends on identity (order matters).
  4. If only an Augmenter is present, mark as augmented-only.
  5. For pages-router routes, inspect render too. A root pages Gate on an ssg or isg route does not protect the static document per visitor: document middleware runs during build/revalidation with a sanitized request, while later route-state requests are separate live requests. Unless an independently verified platform/CDN edge gate protects the document, report the route as error / unprotected-static and recommend ssr/spa for session-gated pages.

Step 4: Check the API surface

Mutation endpoints (POST, PUT, PATCH, DELETE) are the highest-impact target. From pracht inspect api --json:

  • Each API route reports path, file, methods, and hasDefaultHandler (the last requires a current @pracht/cli). A default-export handler serves ALL methods but reports methods: [] — treat hasDefaultHandler: true as "every method exposed". On older CLIs where the field is missing, grep the handler file for export default instead.
  • For each mutation handler (named method export or default handler) and each HTTP- or remote-MCP-exposed capability, check whether defineApp({ api: { middleware } }) applies a Gate, OR the handler/capability reads and validates a session itself. App-level API middleware wraps generated capability endpoints before capability-specific middleware.
  • For remote MCP, cookie-bearing transport requests are rejected before capability dispatch and only Authorization is forwarded. Flag MCP-exposed capabilities whose gate depends on a browser session cookie or a custom credential header that the projection does not carry.
  • Treat context.agent as framework-owned, read-only verified identity. Flag middleware or capability code that attempts to mutate or replace it instead of deriving application authorization state on a separate context field.
  • When a custom adapter supplies a frozen or sealed context, flag authorization helpers that read agent or middleware-added fields through this. The framework binds private-field methods to the immutable source receiver, which cannot observe fields added on its extensible overlay. Callable fields keep their own API and arrays keep their brand. Application-defined Symbol.toStringTag branding does not affect whether an ordinary context can be overlaid, but immutable native built-ins such as Map and Date fail closed because an overlay cannot preserve their internal slots. Use a fresh mutable wrapper when a context needs native built-ins or when receiver-bound helpers depend on request state.
  • Inspect every HTTP-, WebMCP-, or MCP-exposed capability body for invokeCapability(). Direct composition never re-applies app-level API middleware. Remote MCP additionally re-applies the callee's agentPolicy and refuses destructive callees unless the tool being served is itself a destructive capability that already cleared prepare/commit — a request-scoped grant over every destructive callee, private ones included, so audit a confirmed destructive tool's body the way you would a confirmed HTTP endpoint's. Private non-destructive capabilities stay composable and rely on their named middleware for authorization. For HTTP/WebMCP composition, flag sensitive callees whose required transport authorization or approval is absent from the composing capability and the callee's named middleware.
  • Common bug: dashboard route is protected by middleware, but POST /api/items is not — attacker bypasses the UI entirely.

Server islands

Every module in src/server-islands/ (or pracht({ serverIslandsDir })) is reachable at GET /__pracht/server-island with the visitor's cookies, for any page whose route or shell lists it in export const serverIslands = [...] — with that route's middleware (see https://pracht.resynapse.dev/docs/server-islands#security). Map each server island to the routes that list it, directly or through their shell.

  • Flag a server island listed by both gated and ungated routes whose loader returns user-specific data without checking context itself — error: the ungated route serves it to anyone.
  • Flag pages that gate a server island only at render time ({isAdmin && <AdminStats />} behind a loader check, not middleware) — error: the endpoint runs it for anyone who passes that route's middleware.
  • Flag server island loaders that pick whose data to load from props (userId, orderId, accountId) — the caller controls the query string (error, IDOR). Identity must come from context.
  • Flag server island loaders with side effects (writes, sends) — the endpoint is a GET (warn).

Step 5: Client/server enforcement parity

Grep client components for patterns like if (!user) return <Login />. For each occurrence, confirm that the data path is also gated server-side. Client-side gating without a server gate is purely cosmetic and a common source of "I see the data flash before redirect" or worse, leaked data via SPA route loaders.

Step 6: Session cookie sanity

With @pracht/session, check the configuration rather than the mechanics: cookie.secrets read from serverEnv (never a literal), the storage built inside a function rather than at module scope (Workers env is request-scoped), a __Host- cookie name unless subdomain sharing is required, and sameSite matching the app's embedding needs. A store is required for logout to invalidate a session anywhere other than the browser that asked.

Then check the login path for session.regenerate(), called after credentials verify and before the user is written onto the session. Its absence is session fixation: with a store, an attacker who can plant a cookie for the host keeps a valid pointer to the session that becomes authenticated. Flag it as error/fixation on any store-backed app; on a cookie-only app it is info (the cookie carries the sealed data, not a pointer), but still worth adding before the app grows a store. Audit every other privilege change the same way — 2FA completion, role assumption, impersonation.

Cross-reference with audit-csrf: the same cookies that authorize the user are the CSRF target. Recommend running audit-csrf after this skill.

Step 7: Report

Route/APIExpectedResolved middlewareGate present?SeverityVerdict

Severity is the primary scale; the verdict is a secondary domain label:

  • error / unprotected — no auth middleware on a route the user expects protected.
  • error / inconsistent — UI route is gated; sibling API is not.
  • error / unprotected-static — a pages-router SSG/ISG document relies on request/session middleware without an independent per-request edge gate.
  • warn / augmented-only — middleware reads session but never blocks; loader must handle null user.
  • warn / client-only — server allows; client hides UI.
  • error / fixation — store-backed session, no regenerate() at login.
  • info / protected — gate confirmed.
  • info / public-by-design — deliberately exposed (login, signup, marketing).

Rules

  1. The framework's pracht inspect routes --json and pracht inspect api --json are the source of truth — group inheritance is already resolved.
  2. Recognize Gates by behavior (short-circuits with a Response without calling next() on failure), not by filename — projects use auth.ts, requireUser.ts, session.ts, etc.
  3. An Augmenter is a valid pattern when paired with a separate Gate or a loader that explicitly handles the unauthenticated case. Flag it; don't condemn it.
  4. Public routes deliberately exposed (login, signup, marketing) should be listed but not flagged.
  5. Do not auto-add middleware. Auth wiring is policy.
  6. Treat allowed composed capability reachability as transitive. MCP blocks destructive callees unless the served tool already cleared its own confirmation gate, and re-applies agentPolicy; named middleware remains the authorization seam for private non-destructive composition. Audit events identify every nested attempt with transport: "server" and trusted request provenance in via, but observability is not an authorization gate.

$ARGUMENTS

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.