create-pracht 0.2.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/package.json +3 -2
- package/skills/add-auth/SKILL.md +346 -0
- package/skills/add-db/SKILL.md +316 -0
- package/skills/add-i18n/SKILL.md +239 -0
- package/skills/add-observability/SKILL.md +270 -0
- package/skills/audit-a11y/SKILL.md +170 -0
- package/skills/audit-auth/SKILL.md +144 -0
- package/skills/audit-bundles/SKILL.md +167 -0
- package/skills/audit-csrf/SKILL.md +179 -0
- package/skills/audit-deps/SKILL.md +138 -0
- package/skills/audit-headers/SKILL.md +186 -0
- package/skills/audit-islands/SKILL.md +144 -0
- package/skills/audit-loaders/SKILL.md +135 -0
- package/skills/audit-redirects/SKILL.md +146 -0
- package/skills/audit-secrets/SKILL.md +150 -0
- package/skills/audit-seo/SKILL.md +164 -0
- package/skills/audit-shells/SKILL.md +142 -0
- package/skills/configure-isg/SKILL.md +172 -0
- package/skills/migrate-nextjs/SKILL.md +519 -0
- package/skills/pracht-debug/SKILL.md +146 -0
- package/skills/pracht-deploy/SKILL.md +208 -0
- package/skills/pracht-scaffold/SKILL.md +191 -0
- package/skills/pracht-test-api/SKILL.md +165 -0
- package/skills/pre-deploy/SKILL.md +164 -0
- package/skills/scaffold-e2e/SKILL.md +189 -0
- package/skills/scaffold-tests/SKILL.md +283 -0
- package/skills/tune-render-mode/SKILL.md +168 -0
- package/skills/typed-routes/SKILL.md +203 -0
- package/skills/upgrade-pracht/SKILL.md +151 -0
- package/src/index.js +472 -31
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-observability
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Wire error tracking, request tracing, and Web Vitals into a pracht app.
|
|
6
|
+
Supports Sentry or OpenTelemetry on the server side (loader/middleware
|
|
7
|
+
boundaries, API routes), and client-side Web Vitals reporting via the
|
|
8
|
+
`web-vitals` package.
|
|
9
|
+
Use when asked to "add observability", "wire Sentry", "set up tracing",
|
|
10
|
+
"add OpenTelemetry", "monitor Web Vitals", or "track errors".
|
|
11
|
+
allowed-tools:
|
|
12
|
+
- Bash
|
|
13
|
+
- Read
|
|
14
|
+
- Write
|
|
15
|
+
- Edit
|
|
16
|
+
- Grep
|
|
17
|
+
- Glob
|
|
18
|
+
- AskUserQuestion
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Pracht Add Observability
|
|
22
|
+
|
|
23
|
+
Three layers, each opt-in:
|
|
24
|
+
|
|
25
|
+
1. **Server error tracking** — capture loader/middleware/API exceptions.
|
|
26
|
+
2. **Request tracing** — span per request with child spans per loader/db call.
|
|
27
|
+
3. **Web Vitals (LCP/CLS/INP/FCP/TTFB)** — client-side, posted to a beacon
|
|
28
|
+
endpoint.
|
|
29
|
+
|
|
30
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
31
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
|
|
32
|
+
`generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
|
|
33
|
+
config with the pracht plugin registered.
|
|
34
|
+
|
|
35
|
+
## Step 1: Pick the stack
|
|
36
|
+
|
|
37
|
+
Use `AskUserQuestion`:
|
|
38
|
+
|
|
39
|
+
- **Sentry** — easiest end-to-end (errors + traces + Web Vitals).
|
|
40
|
+
- **OpenTelemetry + your backend** (Honeycomb, Grafana, Datadog, Jaeger).
|
|
41
|
+
- **Custom beacon** — minimal `fetch('/api/telemetry')` setup, no SaaS.
|
|
42
|
+
|
|
43
|
+
The skill below shows Sentry and OTel patterns. Custom beacon is mentioned
|
|
44
|
+
but trivial.
|
|
45
|
+
|
|
46
|
+
## Step 2: Server error tracking
|
|
47
|
+
|
|
48
|
+
### Sentry path (Node adapter)
|
|
49
|
+
|
|
50
|
+
The pattern below uses `@sentry/node` and works on the **Node adapter only**
|
|
51
|
+
— see the caveat box below for Cloudflare and Vercel Edge before installing
|
|
52
|
+
anything.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm add @sentry/node # Node adapter only
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Create `src/server/observability.ts`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { serverEnv } from "@pracht/core/env/server";
|
|
62
|
+
import * as Sentry from "@sentry/node";
|
|
63
|
+
|
|
64
|
+
let initialized = false;
|
|
65
|
+
export function initObservability() {
|
|
66
|
+
if (initialized) return;
|
|
67
|
+
initialized = true;
|
|
68
|
+
// serverEnv is read INSIDE this function, not at module scope — module-level
|
|
69
|
+
// env reads break on runtimes where env arrives per request (docs/ENV.md).
|
|
70
|
+
Sentry.init({
|
|
71
|
+
dsn: serverEnv.SENTRY_DSN,
|
|
72
|
+
tracesSampleRate: Number(serverEnv.SENTRY_TRACES_SAMPLE_RATE ?? 0.1),
|
|
73
|
+
environment: serverEnv.NODE_ENV,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Add a global middleware that calls `initObservability()` once (from inside
|
|
79
|
+
the handler, never at module scope) and wraps the downstream call:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// src/middleware/observability.ts
|
|
83
|
+
import type { MiddlewareFn } from "@pracht/core";
|
|
84
|
+
import * as Sentry from "@sentry/node";
|
|
85
|
+
import { initObservability } from "../server/observability";
|
|
86
|
+
|
|
87
|
+
export const middleware: MiddlewareFn = async ({ request, route }, next) => {
|
|
88
|
+
initObservability();
|
|
89
|
+
return Sentry.startSpan(
|
|
90
|
+
{
|
|
91
|
+
name: `${request.method} ${route.path}`,
|
|
92
|
+
op: "http.server",
|
|
93
|
+
},
|
|
94
|
+
() => next(),
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> **Cloudflare / Vercel Edge caveat — be honest here.** `@sentry/cloudflare`
|
|
100
|
+
> requires wrapping the worker's fetch handler with `withSentry()`, but
|
|
101
|
+
> pracht's Cloudflare adapter owns that handler — there is no user hook to
|
|
102
|
+
> wrap it today, so the middleware-init pattern above **cannot work** with
|
|
103
|
+
> `@sentry/cloudflare`, and `@sentry/node` does not run on Workers at all.
|
|
104
|
+
> Do not scaffold a pattern that can't work. Options on Cloudflare:
|
|
105
|
+
> 1. Plain fetch-based event forwarding: catch errors in a wrap-around
|
|
106
|
+
> middleware and `fetch` them to Sentry's store/envelope endpoint (or any
|
|
107
|
+
> HTTP sink) yourself. Read the DSN via `serverEnv` inside the middleware.
|
|
108
|
+
> 2. Wait for pracht to expose a handler-wrap hook for the adapter, then use
|
|
109
|
+
> Sentry's Cloudflare SDK properly.
|
|
110
|
+
>
|
|
111
|
+
> The same applies to `@sentry/vercel-edge`: verify how the init hooks into
|
|
112
|
+
> the runtime before installing; if it needs to own the handler, fall back to
|
|
113
|
+
> option 1. (This mirrors the OTel-edge honesty note below.)
|
|
114
|
+
|
|
115
|
+
Pracht middleware is wrap-around: `await next()` invokes the rest of the
|
|
116
|
+
request and resolves to the final `Response`, so the span naturally covers
|
|
117
|
+
the loader/handler and ends when they finish.
|
|
118
|
+
|
|
119
|
+
Register it in `defineApp({ middleware: { observability: "./..." } })` (the
|
|
120
|
+
top-level `middleware` field is a *registry* keyed by name — not an ordered
|
|
121
|
+
chain). To actually wrap requests, place `"observability"` first in every
|
|
122
|
+
chain that should cover them:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
defineApp({
|
|
126
|
+
middleware: { observability: "./middleware/observability.ts", auth: "./middleware/auth.ts" },
|
|
127
|
+
api: { middleware: ["observability"] }, // all API routes
|
|
128
|
+
routes: [
|
|
129
|
+
group({ middleware: ["observability"] }, [ // all pages
|
|
130
|
+
group({ middleware: ["auth"] }, [ /* protected routes */ ]),
|
|
131
|
+
]),
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Ordering lives in these `middleware: [...]` arrays — always place
|
|
137
|
+
observability first so it spans the rest of the chain.
|
|
138
|
+
|
|
139
|
+
### OpenTelemetry path
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pnpm add @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/auto-instrumentations-node
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Create a SDK init module that runs at server entry — for Node, use the
|
|
146
|
+
`--require ./otel.cjs` flag; for Cloudflare/Vercel edge, OTel is more limited
|
|
147
|
+
(use HTTP exporter directly). Surface this trade-off; don't pretend OTel
|
|
148
|
+
edge is plug-and-play.
|
|
149
|
+
|
|
150
|
+
## Step 3: Loader/API tracing
|
|
151
|
+
|
|
152
|
+
For each loader and API handler, wrap the body in a span.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import * as Sentry from "@sentry/node";
|
|
156
|
+
|
|
157
|
+
export async function loader({ request }) {
|
|
158
|
+
return Sentry.startSpan({ name: "loader: dashboard", op: "function" }, async () => {
|
|
159
|
+
return { /* ... */ };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Auto-injection is out of scope; provide a snippet, recommend wrapping the 5-10
|
|
165
|
+
slowest loaders (cross-reference with `audit-bundles` perf hotspots).
|
|
166
|
+
|
|
167
|
+
## Step 4: Web Vitals on the client
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pnpm add web-vitals
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Create `src/client/vitals.ts` — export a function, **no module-level
|
|
174
|
+
side effects**:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { onCLS, onINP, onLCP, onFCP, onTTFB, type Metric } from "web-vitals";
|
|
178
|
+
|
|
179
|
+
function send(metric: Metric) {
|
|
180
|
+
navigator.sendBeacon?.(
|
|
181
|
+
"/api/telemetry/vitals",
|
|
182
|
+
JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, path: location.pathname }),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function reportVitals() {
|
|
187
|
+
onCLS(send);
|
|
188
|
+
onINP(send);
|
|
189
|
+
onLCP(send);
|
|
190
|
+
onFCP(send);
|
|
191
|
+
onTTFB(send);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Do NOT import this statically from a shell: shells render on the **server**
|
|
196
|
+
too, so module-level `onCLS(...)` calls would execute during SSR. The primary
|
|
197
|
+
pattern is a lazy `import()` inside an effect, guarded by `useIsHydrated`
|
|
198
|
+
(exported from `@pracht/core`), placed in a shell or top-level component:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useIsHydrated } from "@pracht/core";
|
|
202
|
+
import { useEffect } from "preact/hooks";
|
|
203
|
+
|
|
204
|
+
export function Vitals() {
|
|
205
|
+
const hydrated = useIsHydrated();
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!hydrated) return;
|
|
208
|
+
void import("../client/vitals").then((m) => m.reportVitals());
|
|
209
|
+
}, [hydrated]);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This keeps `web-vitals` out of the critical bundle (lazy chunk) and only
|
|
215
|
+
starts observers after hydration has fully settled.
|
|
216
|
+
|
|
217
|
+
## Step 5: Beacon endpoint
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
// src/api/telemetry/vitals.ts
|
|
221
|
+
import type { ApiRouteArgs } from "@pracht/core";
|
|
222
|
+
|
|
223
|
+
export async function POST({ request }: ApiRouteArgs) {
|
|
224
|
+
const body = await request.text();
|
|
225
|
+
// Forward to your destination (Sentry, Honeycomb, custom store).
|
|
226
|
+
// Keep body small; do not block on the upstream.
|
|
227
|
+
console.log("vitals", body);
|
|
228
|
+
return new Response(null, { status: 204 });
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
For Sentry users, Sentry's browser SDK can capture Web Vitals natively —
|
|
233
|
+
prefer that over a custom beacon if you've gone the Sentry route.
|
|
234
|
+
|
|
235
|
+
## Step 6: Sampling and PII
|
|
236
|
+
|
|
237
|
+
- Set `SENTRY_TRACES_SAMPLE_RATE` to a small number (0.05–0.10) in
|
|
238
|
+
production.
|
|
239
|
+
- Scrub auth headers and cookies from breadcrumbs:
|
|
240
|
+
```ts
|
|
241
|
+
Sentry.init({ beforeSend(event) { delete event.request?.headers?.cookie; return event; } });
|
|
242
|
+
```
|
|
243
|
+
- Never send loader return values verbatim — they often contain user data.
|
|
244
|
+
|
|
245
|
+
## Step 7: Verify
|
|
246
|
+
|
|
247
|
+
- Trigger a deliberate error in dev and confirm it lands in Sentry/OTel.
|
|
248
|
+
- Open a route, check the Web Vitals beacon fires (Network tab).
|
|
249
|
+
- Confirm `pnpm test` and `pnpm e2e` still pass.
|
|
250
|
+
- Run `pracht typegen` if any routes were added (the beacon API route does
|
|
251
|
+
not affect page-route types, but re-run when in doubt).
|
|
252
|
+
- Run `pracht verify --json` and confirm no failures.
|
|
253
|
+
|
|
254
|
+
## Rules
|
|
255
|
+
|
|
256
|
+
1. Confirm adapter compatibility before installing the SDK package
|
|
257
|
+
(Sentry has separate packages per runtime), and never scaffold a pattern
|
|
258
|
+
the runtime can't actually run — see the Cloudflare/Vercel-edge caveat in
|
|
259
|
+
Step 2. Read `SENTRY_DSN` and friends via `serverEnv` inside functions,
|
|
260
|
+
never `process.env` at module scope.
|
|
261
|
+
2. Top-level `middleware` in `defineApp` is a name→path *registry*, not an
|
|
262
|
+
ordered chain. Place `"observability"` first in every `group({
|
|
263
|
+
middleware: [...] })` and in `api.middleware` so it wraps the rest.
|
|
264
|
+
3. Web Vitals only matter for SSR/SSG/ISG routes that hydrate; SPA-only
|
|
265
|
+
routes still benefit but the values reflect the post-bootstrap state.
|
|
266
|
+
4. Sample traces (≤ 10%) in production; full sampling in dev.
|
|
267
|
+
5. Never send raw cookies, auth headers, or full loader payloads to a
|
|
268
|
+
third-party SaaS.
|
|
269
|
+
|
|
270
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-a11y
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Per-route accessibility audit for a pracht app. Drives a headless browser
|
|
6
|
+
through every route in the manifest, runs axe-core, and reports issues
|
|
7
|
+
grouped by severity and route. Catches alt-text gaps, contrast failures,
|
|
8
|
+
missing landmarks, focus-order bugs, and form-label problems.
|
|
9
|
+
Use when asked to "audit a11y", "check accessibility", "axe my app",
|
|
10
|
+
"WCAG compliance", or "screen reader test".
|
|
11
|
+
allowed-tools:
|
|
12
|
+
- Bash
|
|
13
|
+
- Read
|
|
14
|
+
- Write
|
|
15
|
+
- Grep
|
|
16
|
+
- Glob
|
|
17
|
+
- AskUserQuestion
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Pracht Audit A11y
|
|
21
|
+
|
|
22
|
+
Static linting cannot prove a route is accessible — many issues only appear
|
|
23
|
+
in the rendered DOM. This skill renders each route in a real browser and
|
|
24
|
+
runs axe-core against the result.
|
|
25
|
+
|
|
26
|
+
## Step 1: Boot the app
|
|
27
|
+
|
|
28
|
+
Prefer the production build/runtime — production HTML is what users actually
|
|
29
|
+
receive. `pracht preview` builds and serves it for **Node and Cloudflare**
|
|
30
|
+
targets. It refuses Vercel targets (it prints guidance and exits nonzero):
|
|
31
|
+
for a Vercel app, run `vercel dev` yourself or point `BASE_URL` at a deployed
|
|
32
|
+
preview instead. Fall back to `pracht dev` only if the user can't build.
|
|
33
|
+
|
|
34
|
+
Run the server as a managed background process, wait for readiness, and clean
|
|
35
|
+
it up when the audit ends:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pracht preview & # run in the background (use the Bash tool's background mode)
|
|
39
|
+
# wait for readiness before auditing:
|
|
40
|
+
until curl -sf http://localhost:3000 > /dev/null; do sleep 1; done
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After Step 5 (or on any failure), kill the background process — do not leave
|
|
44
|
+
a stray server holding the port.
|
|
45
|
+
|
|
46
|
+
Or, if `BASE_URL` is set, target the deployed app and skip the local server
|
|
47
|
+
entirely.
|
|
48
|
+
|
|
49
|
+
## Step 2: Install runner
|
|
50
|
+
|
|
51
|
+
If Playwright is already wired (see `scaffold-e2e`), reuse it. Install the
|
|
52
|
+
axe adapter plus `tsx` (the Step 5 runner — it is not a pracht dependency):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm add -D @axe-core/playwright tsx
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If Playwright is not wired, scaffold a one-off script using `playwright`
|
|
59
|
+
directly. Prefer Playwright over Puppeteer for consistency with existing
|
|
60
|
+
project tooling.
|
|
61
|
+
|
|
62
|
+
## Step 3: Enumerate routes
|
|
63
|
+
|
|
64
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
65
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
66
|
+
shelling out.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pracht inspect routes --json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
|
|
73
|
+
wired up.
|
|
74
|
+
|
|
75
|
+
For dynamic-segment routes, ask the user once for example params (or skip
|
|
76
|
+
with a note in the report).
|
|
77
|
+
|
|
78
|
+
## Step 4: Generate the audit script
|
|
79
|
+
|
|
80
|
+
`scripts/audit-a11y.ts`:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { chromium } from "playwright";
|
|
84
|
+
import AxeBuilder from "@axe-core/playwright";
|
|
85
|
+
|
|
86
|
+
const BASE = process.env.BASE_URL ?? "http://localhost:3000";
|
|
87
|
+
const ROUTES: string[] = [/* injected from pracht inspect routes */];
|
|
88
|
+
|
|
89
|
+
const browser = await chromium.launch();
|
|
90
|
+
const context = await browser.newContext();
|
|
91
|
+
const page = await context.newPage();
|
|
92
|
+
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const path of ROUTES) {
|
|
95
|
+
await page.goto(`${BASE}${path}`, { waitUntil: "networkidle" });
|
|
96
|
+
const axe = await new AxeBuilder({ page })
|
|
97
|
+
.withTags(["wcag2a", "wcag2aa", "wcag21aa", "best-practice"])
|
|
98
|
+
.analyze();
|
|
99
|
+
results.push({ path, violations: axe.violations });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await browser.close();
|
|
103
|
+
console.log(JSON.stringify(results, null, 2));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Step 5: Run and aggregate
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pnpm exec tsx scripts/audit-a11y.ts > a11y-report.json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then stop the background server from Step 1.
|
|
113
|
+
|
|
114
|
+
Aggregate by:
|
|
115
|
+
|
|
116
|
+
- **Per-route summary**: violation count, worst severity.
|
|
117
|
+
- **Per-rule summary**: which rule fires most across the app — usually
|
|
118
|
+
reveals a single component (header, footer, form) that authors every page.
|
|
119
|
+
|
|
120
|
+
## Step 6: Report
|
|
121
|
+
|
|
122
|
+
Report findings with a primary severity of `error` / `warn` / `info`,
|
|
123
|
+
mapping axe impacts: critical + serious → `error`, moderate → `warn`,
|
|
124
|
+
minor → `info`. Keep the raw axe impact as a secondary column.
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
## Per-route summary
|
|
128
|
+
|
|
129
|
+
| Route | Critical | Serious | Moderate | Minor |
|
|
130
|
+
| ----------- | -------- | ------- | -------- | ----- |
|
|
131
|
+
| / | 0 | 1 | 2 | 0 |
|
|
132
|
+
|
|
133
|
+
## Per-rule summary (top offenders)
|
|
134
|
+
|
|
135
|
+
- `color-contrast` — 12 violations across 8 routes
|
|
136
|
+
→ Likely source: shared Button component (src/components/Button.tsx)
|
|
137
|
+
- `image-alt` — 5 violations across 3 routes
|
|
138
|
+
→ Likely source: <img> in shells/marketing.tsx
|
|
139
|
+
|
|
140
|
+
## Detail
|
|
141
|
+
|
|
142
|
+
[per-route violations with selectors and help URLs]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Step 7: Targeted fixes
|
|
146
|
+
|
|
147
|
+
For the top 3 issues, propose concrete patches:
|
|
148
|
+
|
|
149
|
+
- `color-contrast` → suggest tokenized color pairs from existing CSS vars.
|
|
150
|
+
- `image-alt` → list every `<img>` missing `alt` and propose either a
|
|
151
|
+
description or `alt=""` (decorative).
|
|
152
|
+
- `landmark-one-main` → confirm shells render exactly one `<main>` element.
|
|
153
|
+
- `label` → list inputs without associated `<label>` and propose
|
|
154
|
+
`htmlFor`/`for` or wrapping pattern.
|
|
155
|
+
|
|
156
|
+
## Rules
|
|
157
|
+
|
|
158
|
+
1. Run against the production build/runtime if at all possible — minified production
|
|
159
|
+
markup is what real users hit.
|
|
160
|
+
2. axe with WCAG 2.1 AA + best-practice tags is the default; ask before
|
|
161
|
+
downgrading.
|
|
162
|
+
3. Aggregate by rule before per-route — the same component is usually
|
|
163
|
+
responsible for most violations.
|
|
164
|
+
4. Do not auto-fix. Suggest, then let the user review per component.
|
|
165
|
+
5. For SPA routes that need interaction before content appears, ask the user
|
|
166
|
+
for a setup hook (e.g., a script that logs in and lands on the dashboard).
|
|
167
|
+
6. Always clean up background servers you started, even when the audit fails
|
|
168
|
+
partway.
|
|
169
|
+
|
|
170
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-auth
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Find pracht routes that look protected but aren't — missing auth middleware,
|
|
6
|
+
middleware that augments context but never gates, client-side auth checks
|
|
7
|
+
with no server enforcement, and API mutations exposed without guards.
|
|
8
|
+
Use when asked to "audit auth", "check route protection", "is my dashboard
|
|
9
|
+
protected", "find unauthenticated routes", or "review middleware coverage".
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
- Grep
|
|
14
|
+
- Glob
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Pracht Audit Auth
|
|
18
|
+
|
|
19
|
+
What the framework guarantees: middleware runs wrap-around style — every
|
|
20
|
+
middleware must return a `Response` (the runtime throws if it doesn't), either
|
|
21
|
+
`return next()` to continue the chain or a short-circuit `Response` to stop it.
|
|
22
|
+
The `redirect()` helper from `@pracht/core` returns a scheme/CRLF-validated
|
|
23
|
+
redirect `Response`. What the framework does NOT decide is *which* routes get
|
|
24
|
+
an auth gate — that is app wiring, and this skill audits it.
|
|
25
|
+
|
|
26
|
+
The pracht auth pattern (see `examples/docs/src/routes/docs/recipes-auth.md`):
|
|
27
|
+
middleware checks the session, short-circuits with a redirect on absence, and
|
|
28
|
+
forwards user info via request headers; loaders downstream read the headers.
|
|
29
|
+
|
|
30
|
+
Prerequisites: `pracht inspect` requires a vite config that registers the
|
|
31
|
+
pracht plugin.
|
|
32
|
+
|
|
33
|
+
## Step 1: Identify the auth middleware(s)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pracht inspect routes --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If the pracht MCP server is registered (see `docs/MCP.md`), prefer its tools
|
|
40
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
41
|
+
shelling out.
|
|
42
|
+
|
|
43
|
+
Middleware is registered by name in the app manifest —
|
|
44
|
+
`defineApp({ middleware: { auth: () => import("./middleware/auth.ts") } })` —
|
|
45
|
+
and `inspect` reports those names, not files. Read the name→file map from
|
|
46
|
+
`src/routes.ts` (or the configured manifest) to resolve each name, then read
|
|
47
|
+
each middleware file and classify it:
|
|
48
|
+
|
|
49
|
+
- **Gate** — on auth failure, returns a short-circuit `Response`
|
|
50
|
+
(`redirect("/login", { request })`, or a 401/403 `Response`) WITHOUT calling
|
|
51
|
+
`next()`; on success, `return next()`.
|
|
52
|
+
- **Augmenter** — mutates request headers/context with user info, then always
|
|
53
|
+
returns `next()`. Never short-circuits.
|
|
54
|
+
- **Other** — non-auth middleware (rate limit, logging, CORS, etc.).
|
|
55
|
+
|
|
56
|
+
The "Augmenter" category is the silent killer: it makes loaders *think*
|
|
57
|
+
auth is enforced because `request.headers.get('x-user-id')` returns a value
|
|
58
|
+
when present, but unauthenticated requests just get `null` and the loader has
|
|
59
|
+
to handle it. Flag every loader downstream of an Augmenter that doesn't.
|
|
60
|
+
|
|
61
|
+
## Step 2: Identify protected routes
|
|
62
|
+
|
|
63
|
+
A route is "expected protected" if any of:
|
|
64
|
+
|
|
65
|
+
- It has `auth`/`session`/`requireUser`/similar middleware applied.
|
|
66
|
+
- Its loader reads `x-user-id`/`x-user-email`/`getSession`/equivalent.
|
|
67
|
+
- It lives under conventional protected paths: `/dashboard*`, `/admin*`,
|
|
68
|
+
`/account*`, `/settings*`, `/app*` (ask the user to confirm the
|
|
69
|
+
convention if unclear).
|
|
70
|
+
- The user has flagged it explicitly.
|
|
71
|
+
|
|
72
|
+
Build a list of expected-protected routes.
|
|
73
|
+
|
|
74
|
+
## Step 3: Check coverage per protected route
|
|
75
|
+
|
|
76
|
+
For each expected-protected route:
|
|
77
|
+
|
|
78
|
+
1. From `pracht inspect routes --json`, read the resolved `middleware` array.
|
|
79
|
+
2. Confirm at least one **Gate** middleware is present.
|
|
80
|
+
3. Confirm the gate runs **before** any other middleware that depends on
|
|
81
|
+
identity (order matters).
|
|
82
|
+
4. If only an Augmenter is present, mark as `augmented-only`.
|
|
83
|
+
|
|
84
|
+
## Step 4: Check the API surface
|
|
85
|
+
|
|
86
|
+
Mutation endpoints (`POST`, `PUT`, `PATCH`, `DELETE`) are the highest-impact
|
|
87
|
+
target. From `pracht inspect api --json`:
|
|
88
|
+
|
|
89
|
+
- Each API route reports `path`, `file`, `methods`, and `hasDefaultHandler`
|
|
90
|
+
(the last requires a current `@pracht/cli`). A `default`-export handler
|
|
91
|
+
serves ALL methods but reports `methods: []` — treat
|
|
92
|
+
`hasDefaultHandler: true` as "every method exposed". On older CLIs where
|
|
93
|
+
the field is missing, grep the handler file for `export default` instead.
|
|
94
|
+
- For each mutation handler (named method export or default handler), check
|
|
95
|
+
whether `defineApp({ api: { middleware } })` applies a Gate, OR the handler
|
|
96
|
+
reads/validates a session itself.
|
|
97
|
+
- Common bug: dashboard route is protected by middleware, but
|
|
98
|
+
`POST /api/items` is not — attacker bypasses the UI entirely.
|
|
99
|
+
|
|
100
|
+
## Step 5: Client/server enforcement parity
|
|
101
|
+
|
|
102
|
+
Grep client components for patterns like `if (!user) return <Login />`. For
|
|
103
|
+
each occurrence, confirm that **the data path is also gated server-side**.
|
|
104
|
+
Client-side gating without a server gate is purely cosmetic and a common
|
|
105
|
+
source of "I see the data flash before redirect" or worse, leaked data via
|
|
106
|
+
SPA route loaders.
|
|
107
|
+
|
|
108
|
+
## Step 6: Session cookie sanity
|
|
109
|
+
|
|
110
|
+
Cross-reference with `audit-csrf`: the same cookies that authorize the user
|
|
111
|
+
are the CSRF target. Recommend running `audit-csrf` after this skill.
|
|
112
|
+
|
|
113
|
+
## Step 7: Report
|
|
114
|
+
|
|
115
|
+
| Route/API | Expected | Resolved middleware | Gate present? | Severity | Verdict |
|
|
116
|
+
| --------- | -------- | ------------------- | ------------- | -------- | ------- |
|
|
117
|
+
|
|
118
|
+
Severity is the primary scale; the verdict is a secondary domain label:
|
|
119
|
+
|
|
120
|
+
- `error` / `unprotected` — no auth middleware on a route the user expects
|
|
121
|
+
protected.
|
|
122
|
+
- `error` / `inconsistent` — UI route is gated; sibling API is not.
|
|
123
|
+
- `warn` / `augmented-only` — middleware reads session but never blocks;
|
|
124
|
+
loader must handle null user.
|
|
125
|
+
- `warn` / `client-only` — server allows; client hides UI.
|
|
126
|
+
- `info` / `protected` — gate confirmed.
|
|
127
|
+
- `info` / `public-by-design` — deliberately exposed (login, signup,
|
|
128
|
+
marketing).
|
|
129
|
+
|
|
130
|
+
## Rules
|
|
131
|
+
|
|
132
|
+
1. The framework's `pracht inspect routes --json` and `pracht inspect api
|
|
133
|
+
--json` are the source of truth — group inheritance is already resolved.
|
|
134
|
+
2. Recognize Gates by behavior (short-circuits with a `Response` without
|
|
135
|
+
calling `next()` on failure), not by filename — projects use `auth.ts`,
|
|
136
|
+
`requireUser.ts`, `session.ts`, etc.
|
|
137
|
+
3. An Augmenter is a valid pattern when paired with a separate Gate or a
|
|
138
|
+
loader that explicitly handles the unauthenticated case. Flag it; don't
|
|
139
|
+
condemn it.
|
|
140
|
+
4. Public routes deliberately exposed (login, signup, marketing) should be
|
|
141
|
+
listed but not flagged.
|
|
142
|
+
5. Do not auto-add middleware. Auth wiring is policy.
|
|
143
|
+
|
|
144
|
+
$ARGUMENTS
|