create-pracht 0.6.0 → 0.6.2
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/package.json +1 -1
- package/skills/add-auth/SKILL.md +63 -143
- package/skills/add-capabilities/SKILL.md +409 -0
- package/skills/add-content/SKILL.md +242 -0
- package/skills/add-db/SKILL.md +93 -202
- package/skills/add-i18n/SKILL.md +178 -217
- package/skills/add-images/SKILL.md +203 -0
- package/skills/add-observability/SKILL.md +118 -15
- package/skills/add-openapi/SKILL.md +209 -0
- package/skills/audit-a11y/SKILL.md +8 -9
- package/skills/audit-agent-surface/SKILL.md +335 -0
- package/skills/audit-auth/SKILL.md +16 -11
- package/skills/audit-bundles/SKILL.md +56 -12
- package/skills/audit-csrf/SKILL.md +9 -10
- package/skills/audit-deps/SKILL.md +8 -8
- package/skills/audit-headers/SKILL.md +9 -10
- package/skills/audit-islands/SKILL.md +9 -10
- package/skills/audit-loaders/SKILL.md +23 -8
- package/skills/audit-redirects/SKILL.md +9 -10
- package/skills/audit-secrets/SKILL.md +6 -6
- package/skills/audit-seo/SKILL.md +8 -8
- package/skills/audit-shells/SKILL.md +8 -9
- package/skills/configure-isg/SKILL.md +9 -10
- package/skills/migrate-nextjs/SKILL.md +200 -415
- package/skills/pracht-debug/SKILL.md +165 -120
- package/skills/pracht-deploy/SKILL.md +248 -329
- package/skills/pracht-scaffold/SKILL.md +123 -146
- package/skills/pracht-test-api/SKILL.md +10 -10
- package/skills/pre-deploy/SKILL.md +166 -195
- package/skills/scaffold-e2e/SKILL.md +11 -12
- package/skills/scaffold-tests/SKILL.md +10 -12
- package/skills/tune-render-mode/SKILL.md +7 -8
- package/skills/typed-routes/SKILL.md +15 -11
- package/skills/upgrade-pracht/SKILL.md +12 -10
- package/src/index.js +43 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-images
|
|
3
|
+
version: 1.0.1
|
|
4
|
+
description: |
|
|
5
|
+
Wire `@pracht/image`: the CLS-safe zero-runtime `<Image>`, the loader for your
|
|
6
|
+
target (sharp endpoint, Cloudflare, Vercel, passthrough), `?pracht` build-time
|
|
7
|
+
imports with blur placeholders, prebuilt WebP variants, and the optimization
|
|
8
|
+
endpoint's security settings.
|
|
9
|
+
Use for "add images", "optimize images", "responsive images", "next/image
|
|
10
|
+
equivalent", "blur placeholders", "my images cause layout shift".
|
|
11
|
+
allowed-tools:
|
|
12
|
+
- Bash
|
|
13
|
+
- Read
|
|
14
|
+
- Write
|
|
15
|
+
- Edit
|
|
16
|
+
- Grep
|
|
17
|
+
- Glob
|
|
18
|
+
- AskUserQuestion
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Pracht Add Images
|
|
22
|
+
|
|
23
|
+
`@pracht/image` splits the problem the way next/image does: the `<Image>`
|
|
24
|
+
component decides *which widths* to render, a loader decides *what URL* serves
|
|
25
|
+
each width (`docs/IMAGES.md`). The component renders a plain `<img>` — SSR-safe,
|
|
26
|
+
zero hydration, works with `hydration: "none"`.
|
|
27
|
+
|
|
28
|
+
## Step 1: Choose the backend for the deployment target
|
|
29
|
+
|
|
30
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
31
|
+
`inspect_routes`/`inspect_build`/`doctor`/`verify` tools over shelling out.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pracht inspect build --json # adapterTarget (requires a prior `pracht build`)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Target | Loader | Notes |
|
|
38
|
+
| ------ | ------ | ----- |
|
|
39
|
+
| Node | `defaultLoader` + the built-in endpoint | needs `sharp`; put a CDN in front |
|
|
40
|
+
| Cloudflare | `cloudflareLoader` | Image Resizing must be enabled on the zone; sharp does not run on Workers |
|
|
41
|
+
| Vercel | `vercelLoader` | needs an `images` section in the project config; Vercel only serves widths listed in `images.sizes` |
|
|
42
|
+
| Static host | `passthroughLoader` | no image service — srcset is omitted |
|
|
43
|
+
| Any target, no runtime service | `?pracht&pracht-static` imports | prebuilt WebP variants, no loader involved |
|
|
44
|
+
|
|
45
|
+
Confirm the choice with `AskUserQuestion` when the app deploys to more than one
|
|
46
|
+
target — a per-target `configureImage()` behind an env flag is the usual answer
|
|
47
|
+
(`examples/basic` does exactly this).
|
|
48
|
+
|
|
49
|
+
## Step 2: Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm add @pracht/image
|
|
53
|
+
pnpm add sharp # only for the built-in Node endpoint
|
|
54
|
+
pnpm add -D sharp # only for `?pracht` imports / static variants
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
sharp stays an optional peer dependency; the vite plugin fails with an install
|
|
58
|
+
hint when a `?pracht` import needs it, and the endpoint answers 500 with the
|
|
59
|
+
same hint at runtime.
|
|
60
|
+
|
|
61
|
+
## Step 3: Configure the loader once
|
|
62
|
+
|
|
63
|
+
Put `configureImage()` where both the server and the browser evaluate it — the
|
|
64
|
+
top of `src/routes.ts` is the canonical spot:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { cloudflareLoader, configureImage } from "@pracht/image";
|
|
68
|
+
|
|
69
|
+
configureImage({
|
|
70
|
+
loader: cloudflareLoader,
|
|
71
|
+
// deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
72
|
+
// imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
|
73
|
+
// quality: 75,
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`createDefaultLoader("/my/endpoint")` builds a default-style loader for a custom
|
|
78
|
+
endpoint path. Root-absolute endpoints pick up Vite's deploy `base`
|
|
79
|
+
automatically; provider loaders (`cloudflareLoader`, `vercelLoader`)
|
|
80
|
+
deliberately stay at the origin root.
|
|
81
|
+
|
|
82
|
+
## Step 4: Render images
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Image } from "@pracht/image";
|
|
86
|
+
|
|
87
|
+
<Image
|
|
88
|
+
src="/banner.jpg"
|
|
89
|
+
alt="Product banner"
|
|
90
|
+
width={1200}
|
|
91
|
+
height={280}
|
|
92
|
+
sizes="(max-width: 1200px) 100vw, 1200px"
|
|
93
|
+
priority
|
|
94
|
+
/>;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- `alt` is required — use `alt=""` for decorative images.
|
|
98
|
+
- `width`/`height` are required unless `fill`; they reserve space and prevent
|
|
99
|
+
layout shift. Missing dimensions log a `console.error` in dev.
|
|
100
|
+
- `sizes` (or `fill`) switches the srcset to `w` descriptors across
|
|
101
|
+
`deviceSizes`; a fixed image gets `1x`/`2x` candidates snapped to the
|
|
102
|
+
breakpoint list so caches stay small.
|
|
103
|
+
- `priority` on above-the-fold images swaps `loading="lazy"` +
|
|
104
|
+
`decoding="async"` for `loading="eager"` + `fetchpriority="high"`. Use it for
|
|
105
|
+
the LCP image only.
|
|
106
|
+
- `fill` positions the image absolutely inside the nearest positioned ancestor
|
|
107
|
+
and defaults `sizes` to `100vw`; pair with `style={{ objectFit: "cover" }}`.
|
|
108
|
+
|
|
109
|
+
## Step 5: Build-time imports and static variants
|
|
110
|
+
|
|
111
|
+
Register the plugin — the main `pracht()` plugin does **not** include it:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// vite.config.ts
|
|
115
|
+
import { prachtImage } from "@pracht/image/vite";
|
|
116
|
+
|
|
117
|
+
export default { plugins: [prachtImage(), pracht({ /* … */ })] };
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import hero from "./hero.jpg?pracht"; // { src, width, height, blurDataURL }
|
|
122
|
+
import banner from "./banner.jpg?pracht&pracht-static"; // + prebuilt WebP variants
|
|
123
|
+
|
|
124
|
+
<Image src={hero} alt="Hero" placeholder="blur" />;
|
|
125
|
+
<Image src={banner} alt="Banner" sizes="(max-width: 960px) 100vw, 960px" />;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Dimensions come from sharp metadata with EXIF orientation applied, so a
|
|
129
|
+
rotated portrait reports its *display* size.
|
|
130
|
+
- `blurDataURL` is an 8px WebP data URI generated at build time. SVGs skip it,
|
|
131
|
+
animated GIFs blur their first frame.
|
|
132
|
+
- `pracht-static` emits content-hashed WebP files at `staticWidths`, takes
|
|
133
|
+
precedence over the global loader, and is cached in Vite's `cacheDir` keyed by
|
|
134
|
+
source bytes. It requires an **absolute** Vite `base`; SVG and animated
|
|
135
|
+
sources pass through unchanged.
|
|
136
|
+
- Root-relative `publicDir` imports keep stable public URLs and bypass the
|
|
137
|
+
runtime loader.
|
|
138
|
+
- Types: `"types": ["@pracht/image/client"]` in tsconfig, or a triple-slash
|
|
139
|
+
reference in any `.d.ts`.
|
|
140
|
+
|
|
141
|
+
`@pracht/markdown` uses `?pracht&pracht-static` automatically for relative
|
|
142
|
+
Markdown images and renders them with `getImageProps()` — see `/add-content`.
|
|
143
|
+
|
|
144
|
+
## Step 6: Mount the optimization endpoint (Node-compatible runtimes only)
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// src/api/_pracht/image.ts
|
|
148
|
+
import { createImageHandler } from "@pracht/image/node";
|
|
149
|
+
|
|
150
|
+
const imageHandler = createImageHandler({
|
|
151
|
+
// Required in every environment, including dev. Your own env var, not a
|
|
152
|
+
// framework one — set it to the app's public origin and use the same value
|
|
153
|
+
// as nodeAdapter({ canonicalOrigin }).
|
|
154
|
+
localOrigin: process.env.PRACHT_ORIGIN,
|
|
155
|
+
// remotePatterns: [{ protocol: "https", hostname: "*.example.com", pathname: "/uploads" }],
|
|
156
|
+
// allowedWidths: [640, 1280], // required when configureImage() customizes the breakpoints
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export const GET = imageHandler;
|
|
160
|
+
export const HEAD = imageHandler;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
That file maps to `/api/_pracht/image`, exactly what `defaultLoader` targets —
|
|
164
|
+
no further configuration. The endpoint negotiates WebP via `Accept` (pass
|
|
165
|
+
`formats: ["image/avif", "image/webp"]` for AVIF), never enlarges beyond the
|
|
166
|
+
source, passes SVG/GIF through, and answers
|
|
167
|
+
`Cache-Control: public, max-age=14400, must-revalidate` with `Vary: Accept`.
|
|
168
|
+
|
|
169
|
+
## Step 7: Verify
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
pracht dev # set PRACHT_ORIGIN to the exact origin it prints
|
|
173
|
+
pracht build
|
|
174
|
+
pracht verify --json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Check in the browser: the `<img>` carries `srcset`/`sizes`, the LCP image is
|
|
178
|
+
`fetchpriority="high"`, and no layout shift occurs on reload. Then run
|
|
179
|
+
`/audit-bundles` and `/audit-seo` if image work was part of a performance pass.
|
|
180
|
+
|
|
181
|
+
## Rules
|
|
182
|
+
|
|
183
|
+
1. Never leave `localOrigin` unset — relative sources are resolved only against
|
|
184
|
+
it, so a forged `Host` cannot turn the endpoint into an open proxy. The
|
|
185
|
+
handler answers 500 until it is configured, including in dev.
|
|
186
|
+
2. Always allowlist remote hosts with `remotePatterns`; never proxy arbitrary
|
|
187
|
+
URLs.
|
|
188
|
+
3. When `configureImage()` customizes `deviceSizes`/`imageSizes`, pass the
|
|
189
|
+
matching `allowedWidths` — the endpoint rejects widths outside its allowlist
|
|
190
|
+
by design, and skipping this silently breaks the srcset.
|
|
191
|
+
4. Do not use `priority` on more than the one above-the-fold image per route.
|
|
192
|
+
5. `placeholder="blur"` writes an inline `style` attribute: a CSP without
|
|
193
|
+
`style-src-attr 'unsafe-inline'` silently drops it (and `fill` positioning),
|
|
194
|
+
and `img-src` must include `data:`. See `docs/CSP.md`.
|
|
195
|
+
6. Platform loaders generate URLs that only resolve on the deployed platform —
|
|
196
|
+
fall back to `passthroughLoader` behind `import.meta.env.DEV` for local
|
|
197
|
+
previews.
|
|
198
|
+
7. Use an immutable one-year `cacheControl` only when every source URL is
|
|
199
|
+
content-addressed.
|
|
200
|
+
8. Never overwrite an existing `vite.config.ts`, image API route, or
|
|
201
|
+
`configureImage()` call — diff first and confirm with `AskUserQuestion`.
|
|
202
|
+
|
|
203
|
+
$ARGUMENTS
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: add-observability
|
|
3
|
-
version: 1.1.
|
|
3
|
+
version: 1.1.3
|
|
4
4
|
description: |
|
|
5
|
-
Wire
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Use
|
|
10
|
-
|
|
5
|
+
Wire Sentry or OpenTelemetry into pracht server boundaries (loaders, middleware,
|
|
6
|
+
API routes), client-side Web Vitals reporting, and a capability audit sink that
|
|
7
|
+
records every capability dispatch with transport, outcome, latency, and verified
|
|
8
|
+
identity.
|
|
9
|
+
Use for "add observability", "wire Sentry", "set up tracing", "add
|
|
10
|
+
OpenTelemetry", "monitor Web Vitals", "track errors", "log capability calls",
|
|
11
|
+
or "are agents calling my app".
|
|
11
12
|
allowed-tools:
|
|
12
13
|
- Bash
|
|
13
14
|
- Read
|
|
@@ -20,17 +21,19 @@ allowed-tools:
|
|
|
20
21
|
|
|
21
22
|
# Pracht Add Observability
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Four layers, each opt-in:
|
|
24
25
|
|
|
25
26
|
1. **Server error tracking** — capture loader/middleware/API exceptions.
|
|
26
27
|
2. **Request tracing** — span per request with child spans per loader/db call.
|
|
27
28
|
3. **Web Vitals (LCP/CLS/INP/FCP/TTFB)** — client-side, posted to a beacon
|
|
28
29
|
endpoint.
|
|
30
|
+
4. **Agent traffic** — one structured audit event per capability dispatch.
|
|
31
|
+
Only relevant when the app registers capabilities; skip it otherwise.
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`generate_*`
|
|
33
|
-
|
|
33
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
34
|
+
`inspect_agents`/`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/
|
|
35
|
+
`verify`/`generate_*` tools over shelling out. `pracht inspect` needs the pracht
|
|
36
|
+
plugin in the vite config.
|
|
34
37
|
|
|
35
38
|
## Step 1: Pick the stack
|
|
36
39
|
|
|
@@ -232,7 +235,98 @@ export async function POST({ request }: ApiRouteArgs) {
|
|
|
232
235
|
For Sentry users, Sentry's browser SDK can capture Web Vitals natively —
|
|
233
236
|
prefer that over a custom beacon if you've gone the Sentry route.
|
|
234
237
|
|
|
235
|
-
## Step 6:
|
|
238
|
+
## Step 6: Agent traffic (capability apps only)
|
|
239
|
+
|
|
240
|
+
Skip when `pracht inspect agents --json` reports an empty `capabilities` list.
|
|
241
|
+
That only means there are no capability operations to audit; `llms.txt`, MCP,
|
|
242
|
+
or Web Bot Auth may still expose other agent-facing surfaces. When capabilities
|
|
243
|
+
do exist, every dispatch already emits a structured `CapabilityAuditEvent` —
|
|
244
|
+
nothing is instrumented per capability, a sink just has to be registered.
|
|
245
|
+
|
|
246
|
+
```ts [src/server/audit.ts]
|
|
247
|
+
import { addCapabilityAuditListener } from "@pracht/core/server";
|
|
248
|
+
|
|
249
|
+
const stopAuditLog = addCapabilityAuditListener("audit-log", (event) => {
|
|
250
|
+
console.log(
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
msg: "capability",
|
|
253
|
+
at: new Date().toISOString(),
|
|
254
|
+
capability: event.capability,
|
|
255
|
+
effect: event.effect,
|
|
256
|
+
transport: event.transport, // "http" | "webmcp" | "mcp" | "server"
|
|
257
|
+
via: event.via, // causal transport for nested invokeCapability()
|
|
258
|
+
outcome: event.outcome, // "ok" or the envelope error code
|
|
259
|
+
status: event.status,
|
|
260
|
+
durationMs: Math.round(event.durationMs),
|
|
261
|
+
agent: event.agent?.agentDomain ?? event.agent?.keyId ?? null,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (import.meta.hot) {
|
|
267
|
+
import.meta.hot.dispose(stopAuditLog);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The OTel version records a counter and a histogram keyed on
|
|
272
|
+
capability/transport/outcome, and backdates a span with
|
|
273
|
+
`startTime: Date.now() - event.durationMs` (the dispatch has already
|
|
274
|
+
finished when the sink runs). The full snippet is on the agent-trust docs page.
|
|
275
|
+
|
|
276
|
+
Import the module from an eagerly loaded server module. The adapter's configured
|
|
277
|
+
`createContextFrom` module is one portable option: add `import "./audit.ts"`
|
|
278
|
+
there so the generated entry registers the sink before request handling. A
|
|
279
|
+
custom server entry can import it directly. Do not rely on an unrelated route,
|
|
280
|
+
API route, middleware, or `src/server/` registry module; those modules are lazy
|
|
281
|
+
and can miss earlier capability calls.
|
|
282
|
+
|
|
283
|
+
Key properties to state when scaffolding this:
|
|
284
|
+
|
|
285
|
+
- Sinks are invoked synchronously, so keep work before the callback returns or
|
|
286
|
+
reaches its first `await` cheap. A returned promise is never awaited, and a
|
|
287
|
+
synchronous throw is swallowed (first failure per sink reported via
|
|
288
|
+
`console.warn`, naming it), so a broken exporter cannot fail the call.
|
|
289
|
+
- **Always pass a stable name** as the first argument. Registering the same
|
|
290
|
+
name again replaces that sink, which is what keeps a module-scope call safe
|
|
291
|
+
under dev HMR: Vite re-executes importers on every save, so an unkeyed
|
|
292
|
+
registration would add a fresh closure per keystroke and deliver every event
|
|
293
|
+
N times. Never compute the name. Register the returned unsubscribe with
|
|
294
|
+
`import.meta.hot.dispose()` too, so removing the module or renaming the sink
|
|
295
|
+
cannot leave the old name active until the dev server restarts.
|
|
296
|
+
- `setCapabilityAuditHook()` is a **single slot** — a second call replaces the
|
|
297
|
+
first. Use `addCapabilityAuditListener()` whenever more than one sink exists;
|
|
298
|
+
it returns an unsubscribe handle that removes only its own registration.
|
|
299
|
+
- Warning suppression is per named registration, so differently named sinks
|
|
300
|
+
still report independently when they reuse one callback.
|
|
301
|
+
- Delivery snapshots the registered sinks before callbacks run. Adding or
|
|
302
|
+
replacing a sink from inside a callback takes effect on the next dispatch,
|
|
303
|
+
so the current event is never delivered twice to one name.
|
|
304
|
+
- On Cloudflare Workers, a batching exporter must flush within the request or
|
|
305
|
+
be handed the execution context by app code
|
|
306
|
+
(`context.executionContext.waitUntil(exporter.flush())`). Pracht does not
|
|
307
|
+
call `ctx.waitUntil()` for a sink.
|
|
308
|
+
- Audit events cover *dispatch* only. A cross-origin 403, an unknown-capability
|
|
309
|
+
404, and an unknown MCP tool name all return before dispatch and emit
|
|
310
|
+
nothing, so do not build a reconnaissance alert on these events — use the
|
|
311
|
+
HTTP access log for that.
|
|
312
|
+
|
|
313
|
+
In dev the same events are already collected: the **Agents** section of
|
|
314
|
+
`/_pracht` shows recent dispatches with transport, `via`, verified identity,
|
|
315
|
+
outcome, and duration, and `/_pracht.json` exposes all of them under
|
|
316
|
+
`agentTraffic`. The page counts verified identities, MCP, and MCP-caused
|
|
317
|
+
composition as agent-attributed; shows top-level unsigned HTTP, HTTP-caused
|
|
318
|
+
composition, and client-declared WebMCP markers separately as unverified client
|
|
319
|
+
dispatches; and hides only `invokeCapability()` work with no served-request
|
|
320
|
+
provenance behind a first-party toggle, so the panel's visible count can be
|
|
321
|
+
lower than the sink's.
|
|
322
|
+
Counts and empty-state conclusions only cover the retained window when older
|
|
323
|
+
events have been dropped. Use it to confirm the sink sees what the panel sees
|
|
324
|
+
before wiring a paid backend. Adapter-owned dev servers do not register this
|
|
325
|
+
middleware: on Cloudflare `workerd`, `/_pracht` and `/_pracht.json` return 404.
|
|
326
|
+
Validate the sink from its own output there; a missing panel is not a failed
|
|
327
|
+
audit hook.
|
|
328
|
+
|
|
329
|
+
## Step 7: Sampling and PII
|
|
236
330
|
|
|
237
331
|
- Set `SENTRY_TRACES_SAMPLE_RATE` to a small number (0.05–0.10) in
|
|
238
332
|
production.
|
|
@@ -242,10 +336,14 @@ prefer that over a custom beacon if you've gone the Sentry route.
|
|
|
242
336
|
```
|
|
243
337
|
- Never send loader return values verbatim — they often contain user data.
|
|
244
338
|
|
|
245
|
-
## Step
|
|
339
|
+
## Step 8: Verify
|
|
246
340
|
|
|
247
341
|
- Trigger a deliberate error in dev and confirm it lands in Sentry/OTel.
|
|
248
342
|
- Open a route, check the Web Vitals beacon fires (Network tab).
|
|
343
|
+
- If an audit sink was added: call a capability in dev and confirm the event
|
|
344
|
+
reaches both the sink and, when the adapter exposes it, the Agents panel at
|
|
345
|
+
`/_pracht`. On Cloudflare's adapter-owned dev server, validate the sink output
|
|
346
|
+
directly because the panel does not exist.
|
|
249
347
|
- Confirm `pnpm test` and `pnpm e2e` still pass.
|
|
250
348
|
- Run `pracht typegen` if any routes were added (the beacon API route does
|
|
251
349
|
not affect page-route types, but re-run when in doubt).
|
|
@@ -265,6 +363,11 @@ prefer that over a custom beacon if you've gone the Sentry route.
|
|
|
265
363
|
routes still benefit but the values reflect the post-bootstrap state.
|
|
266
364
|
4. Sample traces (≤ 10%) in production; full sampling in dev.
|
|
267
365
|
5. Never send raw cookies, auth headers, or full loader payloads to a
|
|
268
|
-
third-party SaaS.
|
|
366
|
+
third-party SaaS. The same applies to audit events: log the capability
|
|
367
|
+
name, effect, transport, outcome, and agent domain — not the capability's
|
|
368
|
+
input, which is application data.
|
|
369
|
+
6. Keep the synchronous part of an audit sink cheap: no CPU-heavy work or
|
|
370
|
+
synchronous network call. Returned promises are fire-and-forget; the runtime
|
|
371
|
+
does not await them or catch their rejections.
|
|
269
372
|
|
|
270
373
|
$ARGUMENTS
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-openapi
|
|
3
|
+
version: 1.0.1
|
|
4
|
+
description: |
|
|
5
|
+
Wire `@pracht/openapi`: generate an OpenAPI 3.1 document from `defineApi()`
|
|
6
|
+
routes, attach response contracts with `defineOpenApi()`, serve an optional
|
|
7
|
+
Scalar or Swagger UI, handle deploy-base and CSP, and gate completeness in CI.
|
|
8
|
+
Use for "add OpenAPI", "generate an API spec", "add Swagger", "publish API
|
|
9
|
+
docs", "document my API routes".
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
- Write
|
|
14
|
+
- Edit
|
|
15
|
+
- Grep
|
|
16
|
+
- Glob
|
|
17
|
+
- AskUserQuestion
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Pracht Add OpenAPI
|
|
21
|
+
|
|
22
|
+
`@pracht/openapi` is an opt-in companion package (`docs/OPENAPI.md`). Ordinary
|
|
23
|
+
`defineApi()` routes keep their authoring, runtime behavior, and `apiFetch()`
|
|
24
|
+
type inference — the package only reads the resolved server graph and owns its
|
|
25
|
+
own descriptor.
|
|
26
|
+
|
|
27
|
+
## Step 1: Inventory the API first
|
|
28
|
+
|
|
29
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
30
|
+
`inspect_api`/`inspect_routes`/`doctor`/`verify` tools over shelling out.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pracht inspect api --json # endpoint paths, methods, hasDefaultHandler
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Note two things before generating anything:
|
|
37
|
+
|
|
38
|
+
- **Default-export handlers** are not expanded — their supported methods cannot
|
|
39
|
+
be inferred honestly. Split them into named method exports if they belong in
|
|
40
|
+
the document.
|
|
41
|
+
- **Catch-all paths** become a single `{path}` parameter and emit a warning
|
|
42
|
+
about slash encoding.
|
|
43
|
+
|
|
44
|
+
Ask the user whether the document should be public, and whether they want a
|
|
45
|
+
reference UI at all (`ui: false` is the default).
|
|
46
|
+
|
|
47
|
+
## Step 2: Install and register the plugin
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add @pracht/openapi
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// vite.config.ts — the companion plugin goes after pracht()
|
|
55
|
+
import { prachtOpenApi } from "@pracht/openapi/vite";
|
|
56
|
+
import { pracht } from "@pracht/vite-plugin";
|
|
57
|
+
import { defineConfig } from "vite";
|
|
58
|
+
|
|
59
|
+
export default defineConfig({
|
|
60
|
+
plugins: [
|
|
61
|
+
pracht(),
|
|
62
|
+
prachtOpenApi({
|
|
63
|
+
info: {
|
|
64
|
+
title: "Acme API",
|
|
65
|
+
version: "1.0.0",
|
|
66
|
+
description: "Public HTTP API for Acme.",
|
|
67
|
+
},
|
|
68
|
+
ui: "scalar", // or "swagger", an object, or omit for no UI
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This reserves `/openapi.json` and (with a UI) `/docs`. Both are live in the
|
|
75
|
+
Vite dev server; `pracht build` writes `dist/client/openapi.json` and
|
|
76
|
+
`dist/client/docs/index.html`, which Node, Cloudflare, Netlify, and Vercel serve
|
|
77
|
+
through their existing static-asset paths. Paths are configurable via
|
|
78
|
+
`documentPath` and `ui.path`; the document path **must** end in `.json` so
|
|
79
|
+
static hosts assign the right media type.
|
|
80
|
+
|
|
81
|
+
Reserved paths shadow app routes: development logs a warning on a collision and
|
|
82
|
+
production generation replaces the colliding public/build file and reports the
|
|
83
|
+
replacement. Grep the manifest and `public/` for `/docs` before enabling the UI
|
|
84
|
+
on an app that already documents itself there.
|
|
85
|
+
|
|
86
|
+
## Step 3: Attach the metadata that cannot be inferred
|
|
87
|
+
|
|
88
|
+
Paths, methods, path params, and convertible request validators come from the
|
|
89
|
+
routes. Status codes and payload shapes cannot be recovered from an arbitrary
|
|
90
|
+
`Response` or an erased TypeScript type — declare them:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { defineApi, json } from "@pracht/core";
|
|
94
|
+
import { defineOpenApi } from "@pracht/openapi";
|
|
95
|
+
|
|
96
|
+
export const POST = defineOpenApi(
|
|
97
|
+
defineApi({
|
|
98
|
+
body: createItemSchema,
|
|
99
|
+
handler: ({ body }) => json({ id: createItem(body) }, { status: 201 }),
|
|
100
|
+
}),
|
|
101
|
+
{
|
|
102
|
+
operationId: "createItem",
|
|
103
|
+
summary: "Create an item",
|
|
104
|
+
tags: ["items"],
|
|
105
|
+
responses: {
|
|
106
|
+
201: { description: "Item created", body: itemSchema },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`defineOpenApi()` mutates and returns the same handler — validation, dispatch,
|
|
113
|
+
and client inference stay with pracht. Generation adds the framework-known 400
|
|
114
|
+
and 422 validation responses itself, marks a request body optional when the
|
|
115
|
+
validator accepts the empty-body `undefined`, and emits a valid undocumented
|
|
116
|
+
`default` response plus a scoped warning when the contract is unknown.
|
|
117
|
+
|
|
118
|
+
## Step 4: Shared document metadata
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
prachtOpenApi({
|
|
122
|
+
info: { title: "Acme API", version: "1.0.0" },
|
|
123
|
+
document: {
|
|
124
|
+
servers: [
|
|
125
|
+
{ url: "https://api.example.com", description: "Production" },
|
|
126
|
+
{ url: "http://localhost:3000", description: "Local development" },
|
|
127
|
+
],
|
|
128
|
+
tags: [{ name: "items", description: "Item lifecycle" }],
|
|
129
|
+
components: {
|
|
130
|
+
securitySchemes: {
|
|
131
|
+
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
security: [{ bearerAuth: [] }],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A security scheme referenced by an operation's `security` must exist in
|
|
140
|
+
`document.components.securitySchemes`. `document.security` applies globally; set
|
|
141
|
+
`security: []` on an operation to mark it public.
|
|
142
|
+
|
|
143
|
+
**Deploy base:** with Vite `base: "/app/"`, the UI loads its document from
|
|
144
|
+
`/app/openapi.json`, and generation adds `servers: [{ url: "/app" }]` when
|
|
145
|
+
`document.servers` is omitted so "Try it out" reaches base-prefixed routes. An
|
|
146
|
+
explicit `document.servers` always wins, including an empty array.
|
|
147
|
+
|
|
148
|
+
## Step 5: Make it a CI gate
|
|
149
|
+
|
|
150
|
+
Conversion failures are warnings by default, so one unsupported handler does not
|
|
151
|
+
erase the document. Once every public operation has an explicit response
|
|
152
|
+
contract, tighten it:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
prachtOpenApi({ info: { title: "Acme API", version: "1.0.0" }, failOnWarnings: true });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Any warning then fails the dev request and `pracht build`. There is no dedicated
|
|
159
|
+
diff/check command yet — deterministic build output plus `failOnWarnings` is how
|
|
160
|
+
drift is caught.
|
|
161
|
+
|
|
162
|
+
## Step 6: UI provider, CDN, and CSP
|
|
163
|
+
|
|
164
|
+
- `ui: "scalar"` — modern reference with request examples and an API client.
|
|
165
|
+
- `ui: "swagger"` — Swagger UI, with deep links on and the remote validator
|
|
166
|
+
disabled so internal documents are never sent to `validator.swagger.io`.
|
|
167
|
+
- Both shells load pinned browser assets from jsDelivr. Apps with offline
|
|
168
|
+
requirements or a no-third-party-CDN policy should self-host and override
|
|
169
|
+
(`scriptUrl` for both providers, `styleUrl` for Swagger), copying the pinned
|
|
170
|
+
distribution into `public/vendor/`.
|
|
171
|
+
- The shell also contains a small inline bootstrap script. A strict CSP must
|
|
172
|
+
allow that exact script by hash (and the asset origin), or the page must be
|
|
173
|
+
replaced with an app-owned route following the app's nonce policy —
|
|
174
|
+
self-hosting the bundle alone does not fix an inline-script ban. See
|
|
175
|
+
`docs/CSP.md` and `/audit-headers`.
|
|
176
|
+
|
|
177
|
+
## Step 7: Verify
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pracht dev # GET /openapi.json and the UI path
|
|
181
|
+
pracht build
|
|
182
|
+
pracht verify --json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Confirm the document parses, every intended operation is present with a real
|
|
186
|
+
response contract, and no `500`-only `default` responses remain for public
|
|
187
|
+
endpoints. Then run `/audit-headers` if a CSP is in place and
|
|
188
|
+
`/audit-agent-surface` if the document is part of a deliberate agent-facing
|
|
189
|
+
surface.
|
|
190
|
+
|
|
191
|
+
## Rules
|
|
192
|
+
|
|
193
|
+
1. Treat the document and UI as public unless the hosting layer protects the
|
|
194
|
+
emitted static files. No secrets, internal hostnames, or credentials in
|
|
195
|
+
descriptions or examples.
|
|
196
|
+
2. Never embed an OAuth client secret in UI configuration — browser-delivered
|
|
197
|
+
configuration is observable by every visitor.
|
|
198
|
+
3. Keep the UI and JSON on the same origin.
|
|
199
|
+
4. "Try it out" sends real requests: mutation endpoints must carry the same
|
|
200
|
+
authentication, authorization, CSRF, rate-limit, and confirmation policies as
|
|
201
|
+
any other client.
|
|
202
|
+
5. Set a sane static cache policy for the document; immutable caching is wrong
|
|
203
|
+
unless the URL is versioned or deployments purge it.
|
|
204
|
+
6. Do not document a capability HTTP projection here — capability endpoints are
|
|
205
|
+
not included in generation yet (`/add-capabilities` owns that contract).
|
|
206
|
+
7. Never overwrite an existing `vite.config.ts` or a hand-written
|
|
207
|
+
`public/openapi.json` — diff first and confirm with `AskUserQuestion`.
|
|
208
|
+
|
|
209
|
+
$ARGUMENTS
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-a11y
|
|
3
|
-
version: 1.1.
|
|
3
|
+
version: 1.1.1
|
|
4
4
|
description: |
|
|
5
|
-
Per-route
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"WCAG compliance", or "screen reader test".
|
|
5
|
+
Per-route axe-core audit of a pracht app in a headless browser: alt text,
|
|
6
|
+
contrast, landmarks, focus order, and form labels, grouped by severity and
|
|
7
|
+
route.
|
|
8
|
+
Use for "audit a11y", "check accessibility", "axe my app", "WCAG compliance",
|
|
9
|
+
"screen reader test".
|
|
11
10
|
allowed-tools:
|
|
12
11
|
- Bash
|
|
13
12
|
- Read
|
|
@@ -61,8 +60,8 @@ project tooling.
|
|
|
61
60
|
|
|
62
61
|
## Step 3: Enumerate routes
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
64
|
+
`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
|
|
66
65
|
shelling out.
|
|
67
66
|
|
|
68
67
|
```bash
|