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,144 @@
1
+ ---
2
+ name: audit-islands
3
+ version: 1.0.0
4
+ description: |
5
+ Audit pracht islands usage: find over-hydrated routes that should use
6
+ `hydration: "islands"` or `"none"`, dead interactivity outside the islands
7
+ directory, non-serializable island props, mis-tuned client strategies, and
8
+ invalid render/hydration combinations.
9
+ Use when asked to "audit islands", "reduce hydration", "why is this island
10
+ not interactive", "should this page be an island", or "check partial
11
+ hydration".
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Grep
16
+ - Glob
17
+ ---
18
+
19
+ # Pracht Audit Islands
20
+
21
+ Report-only audit of hydration modes and islands usage (see `docs/ISLANDS.md`).
22
+ Pracht hydrates the whole page by default (`hydration: "full"`); routes can opt
23
+ into `"islands"` (only components from the islands directory hydrate) or
24
+ `"none"` (zero JS). This audit finds routes shipping JS they don't need and
25
+ islands wired in ways that break at render time or ship dead handlers.
26
+
27
+ ## Step 1: Enumerate routes and hydration modes
28
+
29
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
30
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
31
+ shelling out.
32
+
33
+ ```bash
34
+ pracht inspect routes --json
35
+ ```
36
+
37
+ Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
38
+ wired up.
39
+
40
+ Each route entry carries `render` and `hydration` (`null` = framework default,
41
+ i.e. full hydration). Also locate the islands directory: default `src/islands/`,
42
+ configurable via `pracht({ islandsDir })` in the vite config. In pages-router
43
+ apps (`mode: "pages"`), hydration is the per-file `export const HYDRATION =
44
+ "islands" | "none"` constant; islands still live in `src/islands/`.
45
+
46
+ ## Step 2: Measure what each route actually ships
47
+
48
+ ```bash
49
+ pracht build --json
50
+ ```
51
+
52
+ Prerequisite: this runs a full production build (`--json` implies `--analyze`).
53
+ Interpret the totals hydration-aware:
54
+
55
+ - Full-hydration routes: route chunks + shell chunks + shared entry.
56
+ - `hydration: "islands"` routes: islands bootstrap + island chunks only — no
57
+ shared client entry. The listed island chunks are an **upper bound** (every
58
+ island in the app); a page only downloads the islands it renders.
59
+ - `hydration: "none"` routes report `0b`.
60
+
61
+ ## Step 3: Run the checks
62
+
63
+ ### 3a. Over-hydration (the headline check)
64
+
65
+ For every route with `hydration` `null`/`"full"`: read the route module, its
66
+ shell, and their imported components. If the page is mostly static — no
67
+ hooks, no event handlers, or only one or two isolated widgets — flag it:
68
+
69
+ - Zero interactivity → recommend `hydration: "none"` (`warn`; `info` if the
70
+ route's total gzip JS is already tiny).
71
+ - A few isolated widgets → recommend `hydration: "islands"` with the widgets
72
+ moved into the islands directory (`warn`). Cite the Step 2 total as the
73
+ payload this change removes.
74
+
75
+ Render-mode fit is owned by `/tune-render-mode`; deep chunk analysis is owned
76
+ by `/audit-bundles`. Point there instead of duplicating their findings.
77
+
78
+ ### 3b. Dead interactivity on islands routes (`error`)
79
+
80
+ On a `hydration: "islands"` route, everything outside the islands directory
81
+ renders as inert HTML — an `onClick` in a regular component silently does
82
+ nothing. Grep the route tree of every islands route for event handlers and
83
+ hooks in components **not** under the islands directory and flag each one.
84
+ Islands are auto-discovered from that directory only (default and named
85
+ function-component exports alike; each becomes its own code-split chunk) —
86
+ there is no wrapper to mark a component elsewhere as an island.
87
+
88
+ ### 3c. Island props and children (`error`)
89
+
90
+ Island props are serialized to JSON in the HTML. At each island call site on
91
+ an islands route, flag props that are functions, symbols, bigints, class
92
+ instances (`Date`, `Map`, ...), JSX elements, or circular — rendering throws a
93
+ descriptive error naming the offending prop path. Passing children into an
94
+ island from a server component also throws (unsupported in v1): move the
95
+ content inside the island or pass a serializable prop.
96
+
97
+ ### 3d. Hydration strategy tuning (`info`)
98
+
99
+ Each island usage picks a strategy via the framework-owned `client` prop:
100
+ `"load"` (default, chunk is `<link rel="modulepreload">`-ed), `"idle"`
101
+ (requestIdleCallback), `"visible"` (IntersectionObserver; chunk fetched only
102
+ when triggered). Flag default-`load` islands that are plausibly below the fold
103
+ or not needed at first paint (comment sections, newsletter signups, footers) →
104
+ suggest `client="visible"` or `client="idle"`.
105
+
106
+ ### 3e. Invalid combinations (`error`)
107
+
108
+ `render: "spa"` always implies full hydration; combining it with
109
+ `hydration: "islands"` or `"none"` is a configuration error. Flag any such
110
+ route (manifest field or pages-router `RENDER_MODE`/`HYDRATION` pair).
111
+
112
+ ### 3f. MPA navigation assumptions (`warn`)
113
+
114
+ Routes with `hydration: "islands"` or `"none"` never load the client router:
115
+ navigation to, from, and between them is full-document (MPA-style), and
116
+ route-state prefetching is skipped for them. Flag apps that rely on
117
+ client-side state surviving navigation across such routes (in-memory stores,
118
+ module-level caches shared between pages) — every navigation is a fresh
119
+ document.
120
+
121
+ ## Step 4: Report
122
+
123
+ | Route | File | Severity | Finding | Suggested fix |
124
+ | ----- | ---- | -------- | ------- | ------------- |
125
+
126
+ Severities: `error` (breaks or throws today: dead handlers, non-serializable
127
+ props, spa+islands), `warn` (works but wasteful or fragile), `info` (tuning
128
+ opportunity). Include the measured gzip totals for every over-hydration
129
+ finding.
130
+
131
+ ## Rules
132
+
133
+ 1. Report only — never edit routes, components, or config. Hand changes to
134
+ `/tune-render-mode` (modes) or the user.
135
+ 2. Use `pracht inspect routes --json` as the source of truth for hydration
136
+ modes — groups inherit `hydration`, so reading `src/routes.ts` manually
137
+ under-counts.
138
+ 3. Islands components behave like plain components on full-hydration routes
139
+ and inside other islands — only flag 3b/3c on `hydration: "islands"`
140
+ routes.
141
+ 4. Verify hydration in a running app via `html[data-pracht-islands-hydrated="true"]`
142
+ (set after all `load` islands hydrate) and per-island `data-hydrated="true"`.
143
+
144
+ $ARGUMENTS
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: audit-loaders
3
+ version: 1.1.0
4
+ description: |
5
+ Audit pracht route loaders for serializability, leaked secrets,
6
+ unsafe loader caching, browser-only API misuse, and missing AbortSignal plumbing.
7
+ Use when asked to "audit loaders", "check loader data", "find serialization
8
+ bugs", "are my loaders safe", or "loader security review".
9
+ allowed-tools:
10
+ - Bash
11
+ - Read
12
+ - Grep
13
+ - Glob
14
+ ---
15
+
16
+ # Pracht Audit Loaders
17
+
18
+ Static analysis of every loader in the project. The framework serializes loader
19
+ return values to the client via `window.__PRACHT_STATE__`, so anything returned
20
+ ends up in the browser — including secrets you never meant to expose.
21
+
22
+ ## Step 1: Enumerate routes
23
+
24
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
25
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
26
+ shelling out.
27
+
28
+ ```bash
29
+ pracht inspect routes --json
30
+ ```
31
+
32
+ Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
33
+ wired up.
34
+
35
+ For every route entry, read `loaderFile ?? file` and inspect the `loader`
36
+ export there. Loaders may live in a separate data module wired via the
37
+ manifest (`RouteConfig.loader`); the inspect JSON surfaces that as
38
+ `loaderFile` (null when the loader lives in the route module itself). Reading
39
+ only `file` misses every externalized loader.
40
+
41
+ ## Step 2: Run the five checks
42
+
43
+ For each `loader` (and `getStaticPaths` when present):
44
+
45
+ ### 2a. Serializability
46
+
47
+ Flag returns that contain any of:
48
+
49
+ | Construct | Why it breaks |
50
+ | ---------------------------- | ----------------------------------- |
51
+ | `Date`, `Map`, `Set`, `URL` | Not preserved by `JSON.stringify` |
52
+ | Class instances | Lose prototype on the client |
53
+ | `Function` / arrow values | Stripped silently |
54
+ | `Promise` | Becomes `{}` |
55
+ | Circular refs | Throws at serialize time |
56
+ | `Buffer` / typed arrays | Becomes `{}` or numeric keys |
57
+ | `bigint` | `JSON.stringify` throws |
58
+ | `undefined` in arrays/object | Drops keys; arrays become `null` |
59
+
60
+ Recommend converting to `string` (ISO for dates), plain arrays, or plain objects
61
+ before return.
62
+
63
+ ### 2b. Secret leaks
64
+
65
+ Ownership note: deep secret scanning is owned by `/audit-secrets` and by
66
+ `pracht verify`'s env scan — keep this check brief and point the user there
67
+ rather than triple-reporting the same findings. Here, only flag what falls
68
+ out of reading the return value anyway:
69
+
70
+ Grep the loader body and anything it returns for:
71
+
72
+ - `process.env.*` references that flow into the return value.
73
+ - `context.env.*` (Cloudflare bindings) flowing into the return value.
74
+ - Variables named `*SECRET*`, `*TOKEN*`, `*KEY*`, `*PASSWORD*`, `*PRIVATE*`,
75
+ `*API_KEY*` reaching the return.
76
+ - Spreads of full DB rows containing `password_hash`, `mfa_secret`, etc.
77
+
78
+ Loaders run server-side but **the return value crosses the wire**. Always
79
+ project to a smaller shape before returning.
80
+
81
+ ### 2c. Browser-only APIs at module top level or in loader
82
+
83
+ Flag any of these accessed unconditionally inside `loader` or at the top level
84
+ of a route module that is rendered SSR/SSG/ISG:
85
+
86
+ - `window`, `document`, `navigator`, `localStorage`, `sessionStorage`,
87
+ `IntersectionObserver`, `matchMedia`, `requestAnimationFrame`.
88
+
89
+ These crash on the server. For SPA-only routes (`render: "spa"`) it's fine
90
+ inside the component — but never inside `loader`.
91
+
92
+ ### 2d. AbortSignal plumbing
93
+
94
+ For loaders that call `fetch` or any I/O:
95
+
96
+ - The framework passes `signal` in `LoaderArgs`.
97
+ - Verify it is forwarded to outbound `fetch(url, { signal })` calls and to any
98
+ database client that accepts cancellation.
99
+ - A loader that ignores `signal` keeps work running after the client navigates
100
+ away.
101
+
102
+ ### 2e. Loader cache safety
103
+
104
+ For routes with a positive `loaderCache` value, flag loader data whose freshness
105
+ or visibility depends on cookies, authorization headers, sessions, user identity,
106
+ permissions, or request-specific context. Route-state HTTP caching is `private`,
107
+ so shared proxies cannot reuse it, but a stale response can still survive logout,
108
+ account switching, or permission changes in the same browser.
109
+
110
+ Recommend `loaderCache: false`/`0` for personalized or authorization-sensitive
111
+ loaders. Use a positive duration only when every field in the returned data can be
112
+ safely reused for that long. Do not confuse `loaderCache` with ISG `revalidate` or
113
+ the short-lived in-memory prefetch cache; they are independent policies.
114
+
115
+ ## Step 3: Report
116
+
117
+ Produce a markdown table:
118
+
119
+ | Route | File | Severity | Finding | Suggested fix |
120
+ | ----- | ---- | -------- | ------- | ------------- |
121
+
122
+ Severities: `error` (secret leak, crash), `warn` (serialization risk, missing
123
+ signal), `info` (style nit).
124
+
125
+ ## Rules
126
+
127
+ 1. Use `pracht inspect routes --json` as the source of truth — do not glob
128
+ `src/routes/**` and risk missing manifest wiring or catching orphan files.
129
+ 2. Read the actual loader source — do not infer from names.
130
+ 3. For each finding, point at the file and line.
131
+ 4. Do not auto-fix. Hand the user the report; let them choose.
132
+ 5. If the loader returns a typed shape from a DB ORM, recommend an explicit
133
+ `select` or projection step rather than spreading the row.
134
+
135
+ $ARGUMENTS
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: audit-redirects
3
+ version: 1.1.0
4
+ description: |
5
+ Find open-redirect vulnerabilities in pracht loaders, middleware, and
6
+ navigation calls. The framework guards both ends — the client router drops
7
+ unsafe URL schemes, and the server `redirect()` helper rejects unsafe
8
+ schemes and CRLF injection — but a hand-rolled 3xx Response bypasses every
9
+ guard, and even a guarded redirect to an attacker-chosen origin can phish.
10
+ Use when asked to "audit redirects", "check for open redirects", "is my
11
+ ?redirect= param safe", or "review login redirect handling".
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Grep
16
+ - Glob
17
+ ---
18
+
19
+ # Pracht Audit Redirects
20
+
21
+ The classic open-redirect bug: `/login?redirect=https://evil.example` →
22
+ after login, the app redirects the user to attacker territory carrying a fresh
23
+ session.
24
+
25
+ What the framework guarantees: the client router drops non-`http(s):` schemes
26
+ at the navigation boundary, and server-side `redirect()` /
27
+ `buildRedirectResponse()` reject non-`http(s):` schemes and CR/LF injection
28
+ against the `Location` header. What it cannot decide is whether the target
29
+ *origin or path* is one you trust — and a raw
30
+ `new Response(null, { status: 302, headers: { location: userInput } })`
31
+ bypasses all of those guards entirely.
32
+
33
+ Prerequisites: `pracht inspect` requires a vite config that registers the
34
+ pracht plugin.
35
+
36
+ ## Step 1: Inventory redirect sites
37
+
38
+ Bound the search first — do not grep the whole repo blind:
39
+
40
+ ```bash
41
+ pracht inspect routes --json
42
+ ```
43
+
44
+ If the pracht MCP server is registered (see `docs/MCP.md`), prefer its tools
45
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
46
+ shelling out.
47
+
48
+ The resolved graph gives you each route's `file`, `loaderFile`, and
49
+ `middleware` names (resolve names to files via the `defineApp({ middleware })`
50
+ map in the manifest). Grep those files plus the API handler files from
51
+ `pracht inspect api --json`, and client components, for two distinct classes:
52
+
53
+ **Class A — guarded sites** (scheme/CRLF-safe, still open-redirect capable):
54
+
55
+ - Middleware/loaders/handlers returning `redirect(...)` or
56
+ `buildRedirectResponse(...)`.
57
+ - `useNavigate()(value)` and `<a href={value}>` where `value` is dynamic
58
+ (client guard applies).
59
+ - `prefetchRouteState(value)` calls with dynamic input.
60
+
61
+ **Class B — unguarded sites** (bypass every framework guard):
62
+
63
+ - Hand-rolled 3xx `Response`s: `new Response(null, { status: 302, headers:
64
+ { location: ... } })` or `headers.set("location", ...)` in loaders,
65
+ middleware, or API handlers. With user input these allow not just open
66
+ redirects but also unsafe schemes and header-injection attempts — flag at
67
+ higher severity.
68
+
69
+ ## Step 2: Trace the input
70
+
71
+ For each site, identify whether the redirect target is:
72
+
73
+ - **Static** — string literal. Safe.
74
+ - **Internal-derived** — built from `params`, `route.path`, or a closed
75
+ allowlist. Safe if the allowlist is verifiable.
76
+ - **Request-derived** — read from `url.searchParams.get(...)`,
77
+ `request.headers.get('referer')`, request body fields, cookie values, or
78
+ query parameters. **Suspect.**
79
+
80
+ Common suspect names: `redirect`, `redirectTo`, `next`, `returnTo`, `continue`,
81
+ `url`, `dest`, `goto`.
82
+
83
+ ## Step 3: Check the gate
84
+
85
+ For each request-derived target, look for one of:
86
+
87
+ | Gate | Safe? |
88
+ | -------------------------------------- | ----- |
89
+ | Hardcoded allowlist of paths/origins | Yes |
90
+ | `target.startsWith('/')` AND `!target.startsWith('//')` | Yes — same-origin path only |
91
+ | `new URL(target, base).origin === url.origin` | Yes — origin comparison |
92
+ | `new URL(target).hostname === expected` | Yes if `expected` is trusted |
93
+ | No check | **Open redirect** |
94
+ | `target.includes(domain)` (substring) | **Bypassable** (`evil.com#yourdomain.com`) |
95
+ | Regex without anchors | **Likely bypassable** |
96
+
97
+ `startsWith('/')` alone is **not** sufficient — `//evil.example/path` parses
98
+ as a protocol-relative URL and most browsers treat it as cross-origin. Require
99
+ both `startsWith('/')` AND `!startsWith('//')`, or use `URL` parsing.
100
+
101
+ Remember: `redirect()`'s built-in validation covers scheme and CRLF only — it
102
+ happily redirects to any well-formed `http(s)` origin, so Class A sites still
103
+ need an origin/path gate for request-derived targets.
104
+
105
+ ## Step 4: Report
106
+
107
+ | File:Line | Class (A/B) | Source | Target expression | Gate | Severity | Verdict |
108
+ | --------- | ----------- | ------ | ----------------- | ---- | -------- | ------- |
109
+
110
+ Severity is the primary scale; the verdict is a secondary domain label:
111
+
112
+ - `error` / `open` — request-derived target, no check. A Class B site here is
113
+ the worst case (no scheme/CRLF guard either) — say so explicitly.
114
+ - `warn` / `risky` — substring/regex check; suggest URL-parse rewrite.
115
+ - `info` / `safe` — static or properly gated. For Class B sites that are
116
+ static today, still recommend migrating to `redirect()` so the scheme/CRLF
117
+ guards apply if the target ever becomes dynamic.
118
+
119
+ For each `open`/`risky` finding, propose a fix snippet, e.g.:
120
+
121
+ ```ts
122
+ const raw = url.searchParams.get("redirect") ?? "/dashboard";
123
+ const safe = raw.startsWith("/") && !raw.startsWith("//") ? raw : "/dashboard";
124
+ return redirect(safe, { request });
125
+ ```
126
+
127
+ ## Step 5: Cross-check with the framework guards
128
+
129
+ Note in the report that pracht drops `javascript:`, `data:`, `vbscript:`,
130
+ `blob:`, and `file:` schemes both in the client router (since #122) and in
131
+ server-side `redirect()`/`buildRedirectResponse()` — so a guarded open
132
+ redirect cannot become script execution. But it can still phish (redirect to
133
+ a look-alike origin) and leak session/referrer headers, and Class B raw
134
+ Responses get none of this protection.
135
+
136
+ ## Rules
137
+
138
+ 1. Default to suspicion for any request-derived target.
139
+ 2. Recommend `URL` parsing over string prefix checks for non-trivial gates.
140
+ 3. Do not trust `referer` as a gate; it is forgeable and often stripped.
141
+ 4. After-login redirects are the most dangerous — user is authenticated.
142
+ 5. Recommend `redirect()` over hand-rolled 3xx Responses so the scheme/CRLF
143
+ guards apply.
144
+ 6. Do not auto-fix; surface the gap and propose the patch.
145
+
146
+ $ARGUMENTS
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: audit-secrets
3
+ version: 1.1.0
4
+ description: |
5
+ Detect environment variables and secrets that leak from the server into
6
+ the client bundle via loader return values, hydration state, or accidental
7
+ imports of server-only modules from client code paths.
8
+ Use when asked to "audit secrets", "find leaked env vars", "is my API key
9
+ exposed", "check client bundle for secrets", or "scan for credential leaks".
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Grep
14
+ - Glob
15
+ ---
16
+
17
+ # Pracht Audit Secrets
18
+
19
+ Pracht serializes loader return values into the `pracht-state` JSON script and
20
+ hydrates the client from them. Anything a loader returns ends up readable in the
21
+ browser. The Vite plugin also strips server-only exports from route files, but
22
+ it cannot save you from a value that flows into the return.
23
+
24
+ Prerequisites: `pracht inspect` requires a vite config that registers the
25
+ pracht plugin; the env-safety report under `dist/client/_pracht/` requires a
26
+ prior `pracht build`. If the pracht MCP server is registered (see
27
+ `docs/MCP.md`), prefer its tools (`inspect_routes`, `inspect_api`,
28
+ `inspect_build`, `doctor`, `verify`) over shelling out.
29
+
30
+ ## Step 0: Run the native env safety check
31
+
32
+ Pracht has built-in env leak detection (see `docs/ENV.md`):
33
+
34
+ - `pracht build` fails when a client chunk references a non-public env var
35
+ (anything not `PRACHT_PUBLIC_`-prefixed and not a Vite built-in),
36
+ naming the variable, chunk, and likely source module.
37
+ - `pracht verify` (and `pracht doctor`) check the build-time env-safety report
38
+ and re-run the literal chunk scan against an existing `dist/client` output.
39
+ - Client-side imports of `@pracht/core/env/server` (`serverEnv`) fail the build.
40
+
41
+ Run `pracht verify` or `pracht doctor` (or a build) first and fold the
42
+ findings into the report.
43
+ Check `vite.config.*` for `envSafety: { allow: [...] }` / `envSafety: false` —
44
+ every allowlisted name and a disabled check are findings to review, since they
45
+ bypass the native gate. The native check detects _references_, not values, so
46
+ the dataflow steps below are still required.
47
+
48
+ ## Step 1: Identify "secret-shaped" identifiers
49
+
50
+ Build a regex of variable names treated as sensitive:
51
+
52
+ ```
53
+ SECRET|TOKEN|API[_-]?KEY|PRIVATE[_-]?KEY|PASSWORD|PASSPHRASE|SESSION[_-]?SECRET|JWT[_-]?SECRET|WEBHOOK[_-]?SECRET|DATABASE[_-]?URL|DB[_-]?URL|CONNECTION[_-]?STRING|CLIENT[_-]?SECRET|REFRESH[_-]?TOKEN
54
+ ```
55
+
56
+ Also flag any direct `process.env.X` or `context.env.X` reference where `X`
57
+ matches the above.
58
+
59
+ ## Step 2: Server → client flow analysis
60
+
61
+ For every route file, trace whether a sensitive value reaches the loader's
62
+ return value. Steps:
63
+
64
+ 1. Run `pracht inspect routes --json` to enumerate routes.
65
+ 2. For each route, read the loader.
66
+ 3. Build a small dataflow trace from sensitive identifiers (and `process.env.*`
67
+ / `context.env.*` reads) to the `return` statement(s).
68
+ 4. Flag any spread (`...row`, `...user`, `...env`) that originates from a
69
+ source containing secrets.
70
+ 5. Flag direct returns of objects that name sensitive keys.
71
+
72
+ This is heuristic — favor over-flagging over silent leaks. Note false-positive
73
+ risk in the report.
74
+
75
+ ## Step 3: Module import boundaries
76
+
77
+ Grep client-rendered files (route components, shells, anything imported from
78
+ them) for imports of:
79
+
80
+ - `node:*` builtins
81
+ - `@pracht/adapter-*`
82
+ - Local `src/server/**` modules
83
+ - Modules whose top level reads `process.env.*`
84
+
85
+ The Vite plugin strips the server-only exports `loader`, `head`, `headers`,
86
+ `getStaticPaths`, and `markdown` from route files when serving the client
87
+ query (`middleware` is not a route-file export — middleware lives in the
88
+ manifest); see the client module transform notes in `docs/ARCHITECTURE.md`
89
+ and `docs/ENV.md`. But a component that imports `../server/db` will still
90
+ pull `db` into the client bundle. Flag those imports.
91
+
92
+ ## Step 4: Hidden surfaces
93
+
94
+ Check for accidental exposure outside loaders:
95
+
96
+ - `head()` returns: rare, but a `meta` value containing a token leaks into HTML.
97
+ - `headers()` returns: flag values that look like secrets. For SSG/ISG pages,
98
+ document headers can be copied into `dist/client/_pracht/headers.json`, which
99
+ is public client output and may be replayed on static responses. This skill
100
+ owns secret VALUES in headers; header policy (CSP, HSTS, weakened defaults)
101
+ is owned by `audit-headers` — cross-reference it.
102
+ - `<Form>` `action` URLs containing tokens in the query string.
103
+ - `prefetchRouteState(url)` calls with sensitive query params.
104
+ - Inline `<script>` content emitted from custom shells.
105
+
106
+ ## Step 5: `.env` discipline
107
+
108
+ - Confirm `.env*` is in `.gitignore`.
109
+ - Confirm generated starters preserve `!.env.example` if they ignore `.env*`.
110
+ - Grep tracked files for likely committed secrets (long random strings near
111
+ identifier names from step 1).
112
+ - Confirm client-side env access goes through `publicEnv` (from
113
+ `@pracht/core`) or `import.meta.env.PRACHT_PUBLIC_*`; `VITE_*` values are
114
+ still exposed by Vite compatibility wiring, but Pracht does not treat them as
115
+ intentionally public, so flag client-side `VITE_*` references unless they are
116
+ explicitly allowlisted and reviewed. Warn loudly if a public env name has a
117
+ secret-shaped name.
118
+ - Confirm server-side env access uses `serverEnv` (from
119
+ `@pracht/core/env/server`) or `context.env` rather than ad-hoc globals.
120
+
121
+ ## Step 6: Report
122
+
123
+ | File:Line | Identifier / source | Sink (loader return / client import / etc.) | Severity |
124
+ | --------- | ------------------- | ------------------------------------------- | -------- |
125
+
126
+ Severities:
127
+
128
+ - `error` — direct flow from `process.env.SECRET_*` / `serverEnv.SECRET_*` to
129
+ loader return.
130
+ - `error` — `PRACHT_PUBLIC_*_SECRET` style client-public or allowlisted `VITE_*_SECRET`
131
+ secret-shaped name.
132
+ - `error` — `envSafety: false` or an allowlisted name that looks secret-shaped.
133
+ - `warn` — spread of a row that may contain secret columns.
134
+ - `warn` — client component imports a module that reads `process.env`.
135
+ - `info` — header value that looks like a token.
136
+
137
+ ## Rules
138
+
139
+ 1. Heuristic-first; document false-positive risk per finding.
140
+ 2. Recommend an explicit allowlist projection (`{ id, name, email }`) over
141
+ blocklist filtering.
142
+ 3. For Cloudflare apps, secrets live in `context.env` — same risk profile as
143
+ `process.env`. Treat them identically.
144
+ 4. Never print suspected secret values into the report — refer by name only.
145
+ 5. If you find a likely committed secret, recommend immediate rotation in
146
+ addition to removal from history.
147
+ 6. Report only — do not auto-fix. Propose the projection/rotation/config
148
+ change; never apply it.
149
+
150
+ $ARGUMENTS