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,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-bundles
|
|
3
|
+
version: 1.2.0
|
|
4
|
+
description: |
|
|
5
|
+
Analyze a pracht production build. Report client bundle size per route,
|
|
6
|
+
flag fat vendor chunks, find route components that ship large dependencies,
|
|
7
|
+
and suggest dynamic `import()` and prefetch strategies based on observed
|
|
8
|
+
navigation patterns.
|
|
9
|
+
Use when asked to "audit bundles", "why is my JS so big", "bundle size per
|
|
10
|
+
route", "what's in my vendor chunk", or "tune prefetching".
|
|
11
|
+
allowed-tools:
|
|
12
|
+
- Bash
|
|
13
|
+
- Read
|
|
14
|
+
- Grep
|
|
15
|
+
- Glob
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Pracht Audit Bundles
|
|
19
|
+
|
|
20
|
+
Pracht performs route-level code splitting via the Vite plugin and emits a
|
|
21
|
+
manifest. This skill reads that manifest, sizes each route's client payload,
|
|
22
|
+
and surfaces the worst offenders.
|
|
23
|
+
|
|
24
|
+
## Step 1: Build with analysis
|
|
25
|
+
|
|
26
|
+
If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
|
|
27
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
28
|
+
shelling out. Prerequisites: `pracht inspect` needs a vite config with the
|
|
29
|
+
pracht plugin; `pracht inspect build` reads artifacts from a prior
|
|
30
|
+
`pracht build`.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pracht build --analyze --json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This is the native per-route payload report: for every route (pattern + render
|
|
37
|
+
mode) it emits the transitive client JS chunks with raw and gzip sizes
|
|
38
|
+
(`routes[].chunks`), route-specific and total sums (`routeGzipBytes`,
|
|
39
|
+
`totalGzipBytes`), and the shared entry chunks broken out (`shared`), sorted by
|
|
40
|
+
total gzip descending. A stale `dist/` produces misleading numbers — rebuild,
|
|
41
|
+
don't reuse.
|
|
42
|
+
|
|
43
|
+
Exit-code footgun: when configured budgets fail, `pracht build --analyze
|
|
44
|
+
--json` sets a nonzero exit code while still printing valid JSON on stdout.
|
|
45
|
+
Do not treat exit 1 as "no output" — parse stdout anyway, or run with
|
|
46
|
+
`--no-budget-fail` to keep the exit code clean.
|
|
47
|
+
|
|
48
|
+
For a human-readable table, use `pracht build --analyze` instead.
|
|
49
|
+
|
|
50
|
+
## Step 2: Pull supporting metadata
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pracht inspect build --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Captures the resolved adapter, client entry URL, and CSS/JS manifest. Cross
|
|
57
|
+
reference with `dist/client/.vite/manifest.json` for chunk metadata
|
|
58
|
+
(file, imports, dynamicImports, css, isEntry) — needed for vendor fan-in
|
|
59
|
+
analysis (Step 4) and CSS sizing, which the analyze report does not cover.
|
|
60
|
+
|
|
61
|
+
## Step 3: Interpret per-route payload
|
|
62
|
+
|
|
63
|
+
From the Step 1 JSON, report:
|
|
64
|
+
|
|
65
|
+
| Route | Hydration | Route JS (gz) | Shared (gz) | Total (gz) | CSS (gz) | LCP-class? |
|
|
66
|
+
| ----- | --------- | ------------- | ----------- | ---------- | -------- | ---------- |
|
|
67
|
+
|
|
68
|
+
`gz` = gzip size, taken directly from `routeGzipBytes` / `shared.gzipBytes` /
|
|
69
|
+
`totalGzipBytes`. Size CSS chunks (from `cssManifest`) with `zlib.gzipSync`.
|
|
70
|
+
|
|
71
|
+
Account for the per-route `hydration` field the analyze JSON emits (omitted
|
|
72
|
+
means `"full"`) before judging sizes:
|
|
73
|
+
|
|
74
|
+
- `hydration: "none"` routes ship **0 bytes** of JS — the report already
|
|
75
|
+
zeroes them out. Never flag them.
|
|
76
|
+
- `hydration: "islands"` routes never load the shared client entry; their
|
|
77
|
+
total is the islands bootstrap plus **every** island chunk in the app — an
|
|
78
|
+
upper bound, since which islands a page actually uses is only known at
|
|
79
|
+
render time. Treat their totals as pessimistic.
|
|
80
|
+
- Only `"full"` routes pay route JS + shared entry.
|
|
81
|
+
|
|
82
|
+
`LCP-class?` is `yes` when total gz exceeds 200 KB — that's the order of
|
|
83
|
+
magnitude where mid-tier mobile starts losing LCP budget. Apply the threshold
|
|
84
|
+
after the hydration adjustment above.
|
|
85
|
+
|
|
86
|
+
If the app declares `budgets` in the pracht plugin config, the Step 1 JSON also
|
|
87
|
+
contains a `budgets` section with per-route pass/fail — lead the report with any
|
|
88
|
+
failures. If no budgets are configured, suggest adding them (see
|
|
89
|
+
docs/PERFORMANCE.md), starting from the current sizes plus ~10% headroom.
|
|
90
|
+
|
|
91
|
+
## Step 4: Vendor chunk health
|
|
92
|
+
|
|
93
|
+
Identify chunks under `node_modules/`. For each:
|
|
94
|
+
- Size (raw and gz).
|
|
95
|
+
- Number of route chunks that import it (fan-in).
|
|
96
|
+
|
|
97
|
+
A vendor chunk imported by every route is the framework runtime — expected.
|
|
98
|
+
A vendor chunk imported by ONE route is a code-splitting opportunity (move
|
|
99
|
+
the import inside that route's component or lazy-load it).
|
|
100
|
+
|
|
101
|
+
A vendor chunk over 100 KB gz that is imported by every route is worth a
|
|
102
|
+
manual review — common offenders: date libraries, validation libraries,
|
|
103
|
+
icon sets, charting libraries.
|
|
104
|
+
|
|
105
|
+
## Step 5: Heavy dependencies in route components
|
|
106
|
+
|
|
107
|
+
For each route chunk over 50 KB gz, run `pracht inspect build --json` plus
|
|
108
|
+
`du`-style analysis on the chunk contents:
|
|
109
|
+
|
|
110
|
+
- Grep the chunk source for known heavy module headers (`moment`, `lodash`,
|
|
111
|
+
`chart.js`, `three`, `@stripe/stripe-js`, etc.).
|
|
112
|
+
- For each, recommend: (a) tree-shakeable alternative, (b) dynamic import
|
|
113
|
+
inside an event handler, (c) lazy-load via `lazy()` from `preact-suspense`.
|
|
114
|
+
|
|
115
|
+
## Step 6: Prefetch strategy
|
|
116
|
+
|
|
117
|
+
`pracht inspect routes --json` exposes `prefetch` per route on a current
|
|
118
|
+
`@pracht/cli`. If your CLI version predates the field (it's absent from the
|
|
119
|
+
JSON), fall back to grepping the route manifest source for `prefetch:`.
|
|
120
|
+
Route-level strategies are `"none"`, `"hover"`, `"intent"`, `"viewport"`
|
|
121
|
+
(`"render"` exists only as a per-link override — see below). Recommend:
|
|
122
|
+
|
|
123
|
+
- `"viewport"` for primary-nav links.
|
|
124
|
+
- `"hover"` for content links inside long pages.
|
|
125
|
+
- `"intent"` (default) is fine if you're not sure.
|
|
126
|
+
- `"none"` for routes that are large and rarely visited (admin, settings) so
|
|
127
|
+
hover doesn't preload them.
|
|
128
|
+
|
|
129
|
+
Cost model for `"viewport"`: the IntersectionObserver fires once per anchor —
|
|
130
|
+
it unobserves the anchor after the first prefetch, and prefetched route state
|
|
131
|
+
is cached for 30 seconds — so a link in a marketing footer costs at most one
|
|
132
|
+
prefetch per anchor per page view, not one per scroll. It's still wasteful
|
|
133
|
+
for a 300 KB route that footer visitors rarely click; prefer `"none"` or
|
|
134
|
+
`"hover"` there. Also note the per-link escape hatch: `<Link prefetch="...">`
|
|
135
|
+
overrides the route-level strategy for a single anchor (and accepts the extra
|
|
136
|
+
`"render"` value — prefetch as soon as the link renders, the most eager
|
|
137
|
+
option), so a route can stay on `"intent"` while its primary-nav link opts
|
|
138
|
+
into `"viewport"` or `"render"`.
|
|
139
|
+
|
|
140
|
+
## Step 7: Report
|
|
141
|
+
|
|
142
|
+
Three sections:
|
|
143
|
+
|
|
144
|
+
1. **Top 10 routes by total client payload** — sorted desc.
|
|
145
|
+
2. **Top 10 vendor chunks by size** — with fan-in.
|
|
146
|
+
3. **Suggestions** — ordered by impact (KB saved × routes affected).
|
|
147
|
+
|
|
148
|
+
Tag every finding with a primary severity — `error` (budget failure), `warn`
|
|
149
|
+
(LCP-class route, single-route vendor chunk), `info` (tuning opportunity) —
|
|
150
|
+
and keep the size numbers as supporting detail.
|
|
151
|
+
|
|
152
|
+
Include before/after estimates for each suggestion: "Lazy-load `chart.js`
|
|
153
|
+
inside `Component`: -180 KB gz off `/dashboard/analytics`."
|
|
154
|
+
|
|
155
|
+
## Rules
|
|
156
|
+
|
|
157
|
+
1. Always run `pracht build --analyze --json` first. A stale build is a source
|
|
158
|
+
of bad advice.
|
|
159
|
+
2. Use the Vite manifest as the source of truth — chunk names rotate per
|
|
160
|
+
build.
|
|
161
|
+
3. Report gzip size, not raw — the wire size is what users pay.
|
|
162
|
+
4. Distinguish "shared" code (entry + framework) from "route-specific" code
|
|
163
|
+
when reporting; users can only optimize the latter.
|
|
164
|
+
5. Do not auto-edit. Bundle changes have render-blocking implications;
|
|
165
|
+
surface and let the user choose.
|
|
166
|
+
|
|
167
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-csrf
|
|
3
|
+
version: 1.2.0
|
|
4
|
+
description: |
|
|
5
|
+
Inventory every form submission and mutation API in the project, then verify
|
|
6
|
+
the CSRF posture. Pracht enforces same-origin on mutation API requests by
|
|
7
|
+
default (`api.requireSameOrigin`); this skill checks that the default is
|
|
8
|
+
intact and that cookie strategy, middleware, or tokens cover whatever the
|
|
9
|
+
built-in check does not.
|
|
10
|
+
Use when asked to "audit CSRF", "check CSRF protection", "are forms safe",
|
|
11
|
+
"review session security", or after enabling cross-origin form usage.
|
|
12
|
+
allowed-tools:
|
|
13
|
+
- Bash
|
|
14
|
+
- Read
|
|
15
|
+
- Grep
|
|
16
|
+
- Glob
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Pracht Audit CSRF
|
|
20
|
+
|
|
21
|
+
What the framework guarantees: by default (`ApiConfig.requireSameOrigin`,
|
|
22
|
+
`true` unless explicitly disabled), the runtime rejects state-changing API
|
|
23
|
+
requests (`POST`/`PUT`/`PATCH`/`DELETE`) with a 403 unless the browser signals
|
|
24
|
+
an exact same-origin fetch (`Sec-Fetch-Site: same-origin`) or the request's
|
|
25
|
+
`Origin`/`Referer` matches the request URL's origin. `same-site` is not
|
|
26
|
+
accepted (sibling subdomains can be attacker-controlled). Requests with no
|
|
27
|
+
browser provenance headers at all (curl, server-to-server) are allowed — the
|
|
28
|
+
threat model is browser-form CSRF, which cannot strip those headers. Page
|
|
29
|
+
routes reject unsafe methods outright, so the API surface is where mutations
|
|
30
|
+
live.
|
|
31
|
+
|
|
32
|
+
The same check also covers **WebSocket upgrade requests** (any request carrying
|
|
33
|
+
an `Upgrade` header), even though they are `GET`. Browsers do not apply CORS to
|
|
34
|
+
WebSocket, so an upgrade is a cross-site-reachable, cookie-carrying request —
|
|
35
|
+
cross-site WebSocket hijacking. `requireSameOrigin: false` therefore opens
|
|
36
|
+
sockets as well as mutations.
|
|
37
|
+
|
|
38
|
+
The audit therefore targets the OPT-OUTS and the remaining layers, in order of
|
|
39
|
+
preference:
|
|
40
|
+
|
|
41
|
+
1. **Built-in same-origin enforcement** (`requireSameOrigin`, on by default).
|
|
42
|
+
2. **`SameSite=Lax` (or `Strict`) on session cookies** — defense in depth.
|
|
43
|
+
3. **Origin-check middleware** — only needed when `requireSameOrigin` is
|
|
44
|
+
disabled or for non-`/api` endpoints.
|
|
45
|
+
4. **Per-request tokens** — only when `SameSite=None` is required.
|
|
46
|
+
|
|
47
|
+
Prerequisites: `pracht inspect` requires a vite config that registers the
|
|
48
|
+
pracht plugin.
|
|
49
|
+
|
|
50
|
+
## Step 0: Check the built-in guard
|
|
51
|
+
|
|
52
|
+
Read `src/routes.ts` (the app manifest) for
|
|
53
|
+
`defineApp({ api: { requireSameOrigin } })`. Absent means `true` (the
|
|
54
|
+
default). An explicit `requireSameOrigin: false` is a top finding (`error`):
|
|
55
|
+
the project has opted out of the built-in CSRF protection and MUST show
|
|
56
|
+
compensating layers (origin-check middleware or token protocol) — demand them
|
|
57
|
+
in the report.
|
|
58
|
+
|
|
59
|
+
## Step 1: Inventory mutation surfaces
|
|
60
|
+
|
|
61
|
+
### Forms
|
|
62
|
+
|
|
63
|
+
Grep for `<Form ` across `src/`. For each occurrence:
|
|
64
|
+
- Capture `method` (default is `get` — only `post`/`put`/`patch`/`delete` are
|
|
65
|
+
CSRF-relevant).
|
|
66
|
+
- Capture `action`.
|
|
67
|
+
|
|
68
|
+
### API mutations
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pracht inspect api --json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If the pracht MCP server is registered (see `docs/MCP.md`), prefer its tools
|
|
75
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
76
|
+
shelling out.
|
|
77
|
+
|
|
78
|
+
For each API route, read the exported `methods`. Mutation methods: `POST`,
|
|
79
|
+
`PUT`, `PATCH`, `DELETE`. Caveat: a `default`-export handler serves ALL
|
|
80
|
+
methods but reports `methods: []` — check the `hasDefaultHandler` field
|
|
81
|
+
(requires a current `@pracht/cli`) or, on older CLIs, grep the handler file
|
|
82
|
+
for `export default`. A default handler counts as exposing all mutation
|
|
83
|
+
methods unless it gates on `request.method` itself.
|
|
84
|
+
|
|
85
|
+
### WebSocket upgrades
|
|
86
|
+
|
|
87
|
+
Grep API handlers for `upgrade`, `WebSocketPair`, and `status: 101`. A route
|
|
88
|
+
that returns a handshake is a mutation-equivalent surface: it is reachable
|
|
89
|
+
cross-site, carries cookies, and — once open — is not covered by any
|
|
90
|
+
per-request check. Treat it as a mutation surface in Step 5, and additionally
|
|
91
|
+
verify:
|
|
92
|
+
|
|
93
|
+
- The handler **authenticates the handshake** (session check in the handler or
|
|
94
|
+
in `api.middleware`). The built-in origin check stops other *websites*; it
|
|
95
|
+
does nothing about an unauthenticated client.
|
|
96
|
+
- Authorization is re-checked per message where messages carry authority. The
|
|
97
|
+
socket outlives the request that opened it, so a session revoked afterwards
|
|
98
|
+
does not close it.
|
|
99
|
+
|
|
100
|
+
## Step 2: Inspect the session cookie
|
|
101
|
+
|
|
102
|
+
Locate cookie issuance — typically `src/server/session.ts`, `src/api/auth/*`,
|
|
103
|
+
or anywhere `Set-Cookie` appears in a response. For every cookie set:
|
|
104
|
+
|
|
105
|
+
| Attribute | Required posture | Failure mode |
|
|
106
|
+
| ---------------- | ---------------------------- | --------------------------- |
|
|
107
|
+
| `HttpOnly` | Present | XSS can steal the session |
|
|
108
|
+
| `Secure` | Present in production | Sniffable on HTTP |
|
|
109
|
+
| `SameSite` | `Lax` or `Strict` | Cross-site sends the cookie |
|
|
110
|
+
| `Path` | Set (usually `/`) | Scope confusion |
|
|
111
|
+
|
|
112
|
+
Flag any cookie missing `HttpOnly`, missing `SameSite`, or with
|
|
113
|
+
`SameSite=None` without an accompanying token check. A missing
|
|
114
|
+
`Max-Age`/`Expires` is `info` only — session cookies are legitimate and
|
|
115
|
+
strictly shorter-lived, not a failure.
|
|
116
|
+
|
|
117
|
+
## Step 3: Origin-check middleware (only for opt-outs)
|
|
118
|
+
|
|
119
|
+
A manual origin-check middleware is only needed when `requireSameOrigin` is
|
|
120
|
+
disabled, or for mutation endpoints outside `/api` handled by custom code.
|
|
121
|
+
If Step 0 found `requireSameOrigin: false`, look for middleware that:
|
|
122
|
+
|
|
123
|
+
- Reads `request.headers.get('origin')`.
|
|
124
|
+
- Compares against `url.origin` and an allowlist.
|
|
125
|
+
- Rejects unsafe-method requests on mismatch.
|
|
126
|
+
|
|
127
|
+
The canonical shape is in `recipes-auth.md` (the `origin-check.ts` example).
|
|
128
|
+
|
|
129
|
+
Verify the wiring: the middleware name must appear in
|
|
130
|
+
`defineApp({ api: { middleware: [...] } })` — that single global list applies
|
|
131
|
+
to every API route. There is no per-group API middleware, and
|
|
132
|
+
`pracht inspect api --json` output has no middleware field, so the manifest is
|
|
133
|
+
the only place to check.
|
|
134
|
+
|
|
135
|
+
## Step 4: Look for token-based CSRF
|
|
136
|
+
|
|
137
|
+
Grep for: `csrf`, `csrfToken`, hidden form fields with token values, headers
|
|
138
|
+
like `x-csrf-token`. Verify both sides of the protocol exist (issue + verify).
|
|
139
|
+
A token issuer with no verifier (or vice versa) is a bug.
|
|
140
|
+
|
|
141
|
+
## Step 5: Score each mutation surface
|
|
142
|
+
|
|
143
|
+
For each `<Form>` and each mutation API, assign a severity (primary) and a
|
|
144
|
+
posture verdict (secondary):
|
|
145
|
+
|
|
146
|
+
- `info` / **Strong** — built-in same-origin enforcement intact (default) +
|
|
147
|
+
`SameSite=Lax`/`Strict` cookies.
|
|
148
|
+
- `info` / **Adequate** — built-in enforcement intact; cookie posture unknown
|
|
149
|
+
or `SameSite` unset (the runtime check still blocks browser CSRF).
|
|
150
|
+
- `warn` / **Compensated** — `requireSameOrigin: false` but a verified
|
|
151
|
+
origin-check middleware or token protocol covers the surface.
|
|
152
|
+
- `warn` / **Token-only** — token verified, cookie has `SameSite=None`.
|
|
153
|
+
- `error` / **Weak** — `requireSameOrigin: false` and no token / no
|
|
154
|
+
origin-check.
|
|
155
|
+
- `warn` / **Unknown** — cookie source not located — investigate.
|
|
156
|
+
|
|
157
|
+
Produce:
|
|
158
|
+
|
|
159
|
+
| Surface | File:Line | Method | requireSameOrigin | Cookie posture | Middleware/Token? | Severity | Verdict |
|
|
160
|
+
| ------- | --------- | ------ | ----------------- | -------------- | ----------------- | -------- | ------- |
|
|
161
|
+
|
|
162
|
+
## Rules
|
|
163
|
+
|
|
164
|
+
1. Point users at `examples/docs/src/routes/docs/recipes-auth.md` for cookie
|
|
165
|
+
and middleware patterns, and at `ApiConfig.requireSameOrigin` for the
|
|
166
|
+
built-in guard.
|
|
167
|
+
2. Do not flag GET-only forms or read-only API methods.
|
|
168
|
+
3. Remember: a hydrated `<Form>` intercepts unsafe-method submissions and
|
|
169
|
+
issues `fetch(actionUrl, { method, body: formData })` — not a document
|
|
170
|
+
POST. Fetch's default credentials mode is `same-origin`, so a cross-origin
|
|
171
|
+
`action` gets no cookies at all; same-origin submissions carry cookies and
|
|
172
|
+
the browser's `Sec-Fetch-Site`/`Origin` headers, which the built-in check
|
|
173
|
+
validates.
|
|
174
|
+
4. If the project sets `SameSite=None`, require either a token check or an
|
|
175
|
+
origin-check in addition to the built-in guard — explain why in the report.
|
|
176
|
+
5. Do not auto-fix. CSRF strategy is a policy decision; surface the gaps and
|
|
177
|
+
let the user choose layers.
|
|
178
|
+
|
|
179
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-deps
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: |
|
|
5
|
+
Run a dependency vulnerability audit and map each finding to the pracht
|
|
6
|
+
routes, loaders, middleware, or API handlers that import the affected
|
|
7
|
+
package — so users know which surface area they need to test after upgrading.
|
|
8
|
+
Use when asked to "audit deps", "scan for CVEs", "which routes use this
|
|
9
|
+
vulnerable package", "npm audit", or "dependency security review".
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
- Grep
|
|
14
|
+
- Glob
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Pracht Audit Deps
|
|
18
|
+
|
|
19
|
+
`npm audit` (or `pnpm audit`) gives you a list of vulnerable packages. This
|
|
20
|
+
skill goes one step further: for each advisory, it tells you **which routes
|
|
21
|
+
and APIs touch the vulnerable code path** so you can prioritize and write
|
|
22
|
+
targeted regression tests after upgrading.
|
|
23
|
+
|
|
24
|
+
Prerequisites: `pracht inspect` requires a vite config that registers the
|
|
25
|
+
pracht plugin.
|
|
26
|
+
|
|
27
|
+
## Step 1: Run the audit
|
|
28
|
+
|
|
29
|
+
Detect the package manager from the lockfile in repo root; for yarn,
|
|
30
|
+
disambiguate Classic vs Berry via the `packageManager` field in
|
|
31
|
+
`package.json` (fall back to `yarn --version`):
|
|
32
|
+
|
|
33
|
+
| Lockfile | Manager | Command |
|
|
34
|
+
| -------------------- | -------------- | -------------------------------- |
|
|
35
|
+
| `pnpm-lock.yaml` | pnpm | `pnpm audit --json` |
|
|
36
|
+
| `package-lock.json` | npm | `npm audit --json` |
|
|
37
|
+
| `yarn.lock` | yarn Classic (`yarn@1.x`) | `yarn audit --json` |
|
|
38
|
+
| `yarn.lock` | yarn Berry (`yarn@2+`) | `yarn npm audit --json --recursive` |
|
|
39
|
+
| `bun.lockb` / `bun.lock` | bun | `bun audit --json` (if available; otherwise note as gap) |
|
|
40
|
+
|
|
41
|
+
Capture the JSON. Track each advisory: package, severity, range, fixed-in.
|
|
42
|
+
|
|
43
|
+
## Step 2: Resolve "which package depends on the vulnerable one"
|
|
44
|
+
|
|
45
|
+
For transitive vulns, the direct importer matters more than the leaf. Use the
|
|
46
|
+
package manager:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm why <package>
|
|
50
|
+
# or
|
|
51
|
+
npm ls <package>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Capture the dependency chain. The first non-pracht-internal direct dependency
|
|
55
|
+
is the one the user owns.
|
|
56
|
+
|
|
57
|
+
## Step 3: Map to routes/APIs
|
|
58
|
+
|
|
59
|
+
If the pracht MCP server is registered (see `docs/MCP.md`), prefer its tools
|
|
60
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
61
|
+
shelling out.
|
|
62
|
+
|
|
63
|
+
For each direct dependency identified in step 2:
|
|
64
|
+
|
|
65
|
+
1. Grep `src/` for `import .* from "<dep>"` and `require("<dep>")`.
|
|
66
|
+
2. Classify each hit file against the resolved graph — do NOT classify by
|
|
67
|
+
directory convention (pages mode uses a configurable `pagesDir`, and app
|
|
68
|
+
layout is not fixed). From `pracht inspect routes --json`, match the hit
|
|
69
|
+
against each route's `file`, `loaderFile`, and `shellFile`; resolve
|
|
70
|
+
`middleware` names to files via the `defineApp({ middleware })` map in the
|
|
71
|
+
manifest. From `pracht inspect api --json`, match against each API route's
|
|
72
|
+
`file`.
|
|
73
|
+
3. A hit that matches none of those is a shared module — trace its importers
|
|
74
|
+
upward until you reach a route, loader, shell, middleware, or API file
|
|
75
|
+
from the graph.
|
|
76
|
+
|
|
77
|
+
This produces a "blast radius" per advisory.
|
|
78
|
+
|
|
79
|
+
## Step 4: Categorize urgency
|
|
80
|
+
|
|
81
|
+
For each advisory, score:
|
|
82
|
+
|
|
83
|
+
| Factor | Weight |
|
|
84
|
+
| --------------------------------------- | ------ |
|
|
85
|
+
| Advisory severity (`critical`/`high`/`moderate`/`low`) | base |
|
|
86
|
+
| Reachable from a request handler | +1 tier |
|
|
87
|
+
| Reachable from an unauthenticated route | +1 tier |
|
|
88
|
+
| Reachable only from build scripts / dev tools | -1 tier |
|
|
89
|
+
|
|
90
|
+
Build scripts that never ship to runtime (e.g., a Vite plugin used only at
|
|
91
|
+
build time) are lower priority than a package imported into a production
|
|
92
|
+
loader.
|
|
93
|
+
|
|
94
|
+
## Step 5: Report
|
|
95
|
+
|
|
96
|
+
Report severity is the primary scale — `error` (critical/high reachable from
|
|
97
|
+
runtime), `warn` (moderate, or high but build-time only), `info` (low, or
|
|
98
|
+
unreachable) — with the advisory's own severity as a secondary column:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
## error
|
|
102
|
+
|
|
103
|
+
- <pkg> @ <version> — advisory: <severity> — <CVE>
|
|
104
|
+
Direct importer: <dep>
|
|
105
|
+
Reachable from:
|
|
106
|
+
- GET /api/users (src/api/users.ts)
|
|
107
|
+
- SSR /dashboard (src/routes/dashboard.tsx → src/server/db.ts)
|
|
108
|
+
Fix: upgrade to <range>
|
|
109
|
+
Test after upgrade: <list of routes/APIs above>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
End with a one-line verdict: `N critical, N high, N moderate, N low — N
|
|
113
|
+
reachable from runtime`.
|
|
114
|
+
|
|
115
|
+
## Step 6: Recommend the upgrade
|
|
116
|
+
|
|
117
|
+
This skill is report-only — do not run any install or upgrade command. For
|
|
118
|
+
each fix:
|
|
119
|
+
|
|
120
|
+
- If the direct dependency has a non-breaking range covering the fix:
|
|
121
|
+
recommend running `pnpm up <dep>` (or the equivalent for the detected
|
|
122
|
+
package manager).
|
|
123
|
+
- If a major bump is required: link to the package's CHANGELOG and recommend
|
|
124
|
+
a deliberate migration.
|
|
125
|
+
- Recommend running `pnpm test` and the route-targeted tests derived from
|
|
126
|
+
step 3 after any upgrade.
|
|
127
|
+
|
|
128
|
+
## Rules
|
|
129
|
+
|
|
130
|
+
1. Always determine the direct importer; transitive-only output is unhelpful.
|
|
131
|
+
2. Distinguish runtime vs. build-time exposure — they have very different
|
|
132
|
+
urgency.
|
|
133
|
+
3. Do not run any upgrade; propose commands only. The user applies them.
|
|
134
|
+
4. If the audit tool reports zero advisories, still note the package counts
|
|
135
|
+
and lockfile age — staleness is a precursor to advisories.
|
|
136
|
+
5. Cross-reference with `pre-deploy` before shipping any post-upgrade build.
|
|
137
|
+
|
|
138
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-headers
|
|
3
|
+
version: 1.2.0
|
|
4
|
+
description: |
|
|
5
|
+
Audit security header coverage in a pracht app. The framework applies four
|
|
6
|
+
default security headers on every response path; this skill audits the
|
|
7
|
+
exceptions — static output served outside first-party adapters, `headers()`
|
|
8
|
+
exports that weaken the defaults, and the headers only the user can decide
|
|
9
|
+
(HSTS, CSP).
|
|
10
|
+
Use when asked to "audit security headers", "check CSP", "harden headers",
|
|
11
|
+
"set up HSTS", or "review header policy".
|
|
12
|
+
allowed-tools:
|
|
13
|
+
- Bash
|
|
14
|
+
- Read
|
|
15
|
+
- Grep
|
|
16
|
+
- Glob
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Pracht Audit Headers
|
|
20
|
+
|
|
21
|
+
What the framework guarantees: four default security headers are applied
|
|
22
|
+
automatically on EVERY framework response path — SSR pages, API responses,
|
|
23
|
+
404s, and static/ISG output on the first-party adapters (`adapter-node`,
|
|
24
|
+
`adapter-cloudflare`) as well as the Vercel headers config generated at build
|
|
25
|
+
time:
|
|
26
|
+
|
|
27
|
+
- `permissions-policy` (disables device sensors)
|
|
28
|
+
- `referrer-policy: strict-origin-when-cross-origin`
|
|
29
|
+
- `x-content-type-options: nosniff`
|
|
30
|
+
- `x-frame-options: SAMEORIGIN`
|
|
31
|
+
|
|
32
|
+
The helper behind this (`applyDefaultSecurityHeaders`) only sets a header
|
|
33
|
+
**when missing** — so a route or shell `headers()` export always wins,
|
|
34
|
+
including when it weakens a default. The framework does NOT set
|
|
35
|
+
`strict-transport-security`, `content-security-policy`, or `cross-origin-*`
|
|
36
|
+
headers — those need a project decision.
|
|
37
|
+
|
|
38
|
+
One deliberate exception: **protocol-switch responses** (a `101` WebSocket
|
|
39
|
+
handshake, or any response carrying a `webSocket` handle) are returned exactly
|
|
40
|
+
as the handler produced them, with no headers applied. This is not a gap —
|
|
41
|
+
copying the response would destroy the socket, and a handshake has no body for
|
|
42
|
+
a sniffing or framing policy to protect. Do not report it as a finding.
|
|
43
|
+
|
|
44
|
+
The audit surface is therefore:
|
|
45
|
+
|
|
46
|
+
- **(a)** `dist/client` served by a custom CDN/host outside the first-party
|
|
47
|
+
adapters — nothing applies the defaults there.
|
|
48
|
+
- **(b)** `headers()` exports that override a default with a weaker value.
|
|
49
|
+
- **(c)** HSTS and CSP, which genuinely need user action.
|
|
50
|
+
|
|
51
|
+
Prerequisites: `pracht inspect` requires a vite config that registers the
|
|
52
|
+
pracht plugin; `pracht inspect build` and `dist/client/_pracht/headers.json`
|
|
53
|
+
require a prior `pracht build`.
|
|
54
|
+
|
|
55
|
+
## Step 1: Inventory header sources
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pracht inspect routes --json
|
|
59
|
+
pracht inspect api --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If the pracht MCP server is registered (see `docs/MCP.md`), prefer its tools
|
|
63
|
+
(`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
|
|
64
|
+
shelling out.
|
|
65
|
+
|
|
66
|
+
Only route modules and shells have a `headers()` export — API handlers do not
|
|
67
|
+
(there is no `headers` in the API module shape; API handlers set headers
|
|
68
|
+
inline on the `Response` they build, and the runtime applies the defaults on
|
|
69
|
+
top). Inventory:
|
|
70
|
+
|
|
71
|
+
- `headers()` exports in route files, loader files (`loaderFile`), and shells
|
|
72
|
+
(`shellFile`).
|
|
73
|
+
- Hand-rolled `Response` constructions in API handlers and middleware where
|
|
74
|
+
security-relevant headers are set inline.
|
|
75
|
+
|
|
76
|
+
## Step 2: Find weakened defaults and uncovered static hosting
|
|
77
|
+
|
|
78
|
+
Defaults are framework-applied, so do not build a "which route is covered"
|
|
79
|
+
matrix — instead:
|
|
80
|
+
|
|
81
|
+
1. For each `headers()` source from Step 1, flag any of the four default
|
|
82
|
+
header names set to a weaker value (e.g. `x-frame-options: ALLOWALL`,
|
|
83
|
+
an over-permissive `permissions-policy`, `referrer-policy: unsafe-url`).
|
|
84
|
+
Because the defaults are set-when-missing, the route value wins.
|
|
85
|
+
2. Ask how `dist/client` is deployed. On `adapter-node`, `adapter-cloudflare`,
|
|
86
|
+
or the generated Vercel config, static/ISG responses get the defaults. If
|
|
87
|
+
a custom CDN or host serves `dist/client` directly, flag it (`warn`) and
|
|
88
|
+
recommend replicating the four defaults in that host's header config.
|
|
89
|
+
|
|
90
|
+
## Step 3: HSTS
|
|
91
|
+
|
|
92
|
+
Grep for `strict-transport-security`. If absent everywhere, recommend adding
|
|
93
|
+
it (e.g. via a shell `headers()` export or the host's config):
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
"strict-transport-security": "max-age=63072000; includeSubDomains; preload"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Only recommend `preload` if the user confirms they want to commit to HTTPS
|
|
100
|
+
permanently and submit to the preload list.
|
|
101
|
+
|
|
102
|
+
## Step 4: Content-Security-Policy
|
|
103
|
+
|
|
104
|
+
Start from the canonical starter policy in `docs/CSP.md` — it includes
|
|
105
|
+
`'inline-speculation-rules'` in `script-src`, required for routes that opt
|
|
106
|
+
into `speculation` (they emit an inline
|
|
107
|
+
`<script type="speculationrules">`). Then add observed origins:
|
|
108
|
+
|
|
109
|
+
1. Check which routes use `speculation`: read the `speculation` field from
|
|
110
|
+
`pracht inspect routes --json` (requires a current `@pracht/cli`; on older
|
|
111
|
+
CLIs, grep the manifest for `speculation:` instead). If none do,
|
|
112
|
+
`'inline-speculation-rules'` can be dropped.
|
|
113
|
+
2. Grep the app for fetch URLs, image URLs, CSS `@import`, `<script src>`,
|
|
114
|
+
`<link href>`, font URLs, iframe sources.
|
|
115
|
+
3. Group by directive: `default-src`, `script-src`, `style-src`, `img-src`,
|
|
116
|
+
`font-src`, `connect-src`, `frame-src`; always include `'self'` per
|
|
117
|
+
directive, per the starter policy.
|
|
118
|
+
4. Pracht injects hydration state in a non-executable
|
|
119
|
+
`<script id="pracht-state" type="application/json">` — it does not need
|
|
120
|
+
`'unsafe-inline'`. Do not add `'unsafe-inline'` unless the app truly emits
|
|
121
|
+
executable inline scripts and the tradeoff is documented.
|
|
122
|
+
|
|
123
|
+
Output a draft CSP referencing `docs/CSP.md` and explain what each origin is
|
|
124
|
+
for. Do not hand the user a CSP that breaks their site — present as draft.
|
|
125
|
+
|
|
126
|
+
## Step 5: Prerendered header manifest safety
|
|
127
|
+
|
|
128
|
+
For SSG/ISG output, the framework refuses to prerender any route whose
|
|
129
|
+
document headers include dangerous names (`set-cookie`, `authorization`,
|
|
130
|
+
`proxy-authorization`, `www-authenticate`, `proxy-authenticate`) or
|
|
131
|
+
secret-shaped custom `x-*` names (token/secret/key/credential patterns) — a
|
|
132
|
+
build containing them hard-fails. So do not hunt for those at runtime; audit
|
|
133
|
+
the `headers()` sources statically to catch them **before** a build failure,
|
|
134
|
+
and report each as `error` with the prerender failure it would cause.
|
|
135
|
+
|
|
136
|
+
The real target is the `warn` class the framework cannot catch: innocuously
|
|
137
|
+
named headers carrying user-specific values, which get copied into
|
|
138
|
+
`dist/client/_pracht/headers.json` (public client output) and replayed across
|
|
139
|
+
users on static responses.
|
|
140
|
+
|
|
141
|
+
Secret VALUES in headers are owned by `audit-secrets`; this skill owns policy
|
|
142
|
+
headers. Cross-reference `audit-secrets` for value-level findings.
|
|
143
|
+
|
|
144
|
+
## Step 6: Cross-origin isolation (optional)
|
|
145
|
+
|
|
146
|
+
If the user mentions `SharedArrayBuffer`, WASM threading, or high-precision
|
|
147
|
+
timers, recommend:
|
|
148
|
+
|
|
149
|
+
- `cross-origin-opener-policy: same-origin`
|
|
150
|
+
- `cross-origin-embedder-policy: require-corp`
|
|
151
|
+
- `cross-origin-resource-policy: same-origin` on assets
|
|
152
|
+
|
|
153
|
+
Otherwise leave these unset — they break embedded third-party content.
|
|
154
|
+
|
|
155
|
+
## Step 7: Report
|
|
156
|
+
|
|
157
|
+
Two outputs:
|
|
158
|
+
|
|
159
|
+
1. **Findings table** (source × header × severity).
|
|
160
|
+
2. **Recommendations**, in priority order:
|
|
161
|
+
- `error`: `headers()` values that would fail prerender (Step 5).
|
|
162
|
+
- `error`: `headers()` exports that weaken a default.
|
|
163
|
+
- `warn`: `dist/client` served by a host that does not apply the defaults.
|
|
164
|
+
- `warn`: Missing HSTS in production-grade apps.
|
|
165
|
+
- `warn`: User-specific headers in prerendered static output.
|
|
166
|
+
- `info`: CSP draft, COOP/COEP suggestions.
|
|
167
|
+
|
|
168
|
+
Show the exact code change required (e.g. the `headers()` export to fix, or
|
|
169
|
+
the CDN header config to add).
|
|
170
|
+
|
|
171
|
+
## Rules
|
|
172
|
+
|
|
173
|
+
1. The four defaults are framework-applied — do not recommend re-adding them
|
|
174
|
+
in app code; audit for weakening and for hosting paths outside the
|
|
175
|
+
first-party adapters.
|
|
176
|
+
2. Never recommend a CSP with `'unsafe-eval'` unless the user has documented
|
|
177
|
+
why they need it.
|
|
178
|
+
3. `applyDefaultSecurityHeaders` is set-when-missing, so per-route `headers()`
|
|
179
|
+
wins over the defaults — weakened values are the primary finding, not
|
|
180
|
+
missing ones.
|
|
181
|
+
4. Static/ISG responses already get the defaults on first-party adapters and
|
|
182
|
+
the generated Vercel config; only flag static hosting that bypasses those.
|
|
183
|
+
5. Treat prerender header manifests as public artifacts.
|
|
184
|
+
6. Do not auto-edit. Headers are policy; surface gaps, propose patches.
|
|
185
|
+
|
|
186
|
+
$ARGUMENTS
|