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.
@@ -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