create-pracht 0.6.1 → 0.6.3

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/skills/add-auth/SKILL.md +63 -143
  3. package/skills/add-capabilities/SKILL.md +411 -0
  4. package/skills/add-content/SKILL.md +242 -0
  5. package/skills/add-db/SKILL.md +93 -202
  6. package/skills/add-i18n/SKILL.md +178 -217
  7. package/skills/add-images/SKILL.md +203 -0
  8. package/skills/add-observability/SKILL.md +118 -15
  9. package/skills/add-openapi/SKILL.md +209 -0
  10. package/skills/audit-a11y/SKILL.md +8 -9
  11. package/skills/audit-agent-surface/SKILL.md +335 -0
  12. package/skills/audit-auth/SKILL.md +16 -11
  13. package/skills/audit-bundles/SKILL.md +48 -12
  14. package/skills/audit-csrf/SKILL.md +9 -10
  15. package/skills/audit-deps/SKILL.md +8 -8
  16. package/skills/audit-headers/SKILL.md +9 -10
  17. package/skills/audit-islands/SKILL.md +9 -10
  18. package/skills/audit-loaders/SKILL.md +23 -8
  19. package/skills/audit-redirects/SKILL.md +9 -10
  20. package/skills/audit-secrets/SKILL.md +6 -6
  21. package/skills/audit-seo/SKILL.md +8 -8
  22. package/skills/audit-shells/SKILL.md +8 -9
  23. package/skills/configure-isg/SKILL.md +9 -10
  24. package/skills/migrate-nextjs/SKILL.md +200 -424
  25. package/skills/pracht-debug/SKILL.md +165 -128
  26. package/skills/pracht-deploy/SKILL.md +246 -331
  27. package/skills/pracht-scaffold/SKILL.md +123 -146
  28. package/skills/pracht-test-api/SKILL.md +10 -10
  29. package/skills/pre-deploy/SKILL.md +165 -200
  30. package/skills/scaffold-e2e/SKILL.md +11 -12
  31. package/skills/scaffold-tests/SKILL.md +10 -12
  32. package/skills/tune-render-mode/SKILL.md +7 -8
  33. package/skills/typed-routes/SKILL.md +9 -9
  34. package/skills/upgrade-pracht/SKILL.md +7 -8
  35. package/src/index.js +39 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pracht",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Interactive and scriptable starter CLI for creating full-stack Preact apps with Pracht.",
5
5
  "keywords": [
6
6
  "pracht",
@@ -1,14 +1,12 @@
1
1
  ---
2
2
  name: add-auth
3
- version: 1.1.0
3
+ version: 1.2.0
4
4
  description: |
5
- Drop session-based auth into a pracht app following the framework's
6
- recommended pattern (middleware checks the session, loaders read user info,
7
- API routes mutate it). Generates session utilities, the auth middleware,
8
- login/logout/signup API routes, and the matching `<Form>`-driven pages
9
- then wires the manifest with public vs. protected groups.
10
- Use when asked to "add auth", "set up login", "wire authentication",
11
- "add session middleware", or "I need users".
5
+ Wire session-based auth into a pracht app: session utilities, auth middleware,
6
+ login/logout/signup API routes, `<Form>` pages, and public vs. protected route
7
+ groups.
8
+ Use for "add auth", "set up login", "wire authentication", "add session
9
+ middleware", "I need users".
12
10
  allowed-tools:
13
11
  - Bash
14
12
  - Read
@@ -21,31 +19,29 @@ allowed-tools:
21
19
 
22
20
  # Pracht Add Auth
23
21
 
24
- Implements the auth pattern documented in
25
- `examples/docs/src/routes/docs/recipes-auth.md`. This skill stamps out the
26
- files; the user replaces `verifyCredentials()` with a real DB lookup.
22
+ Stamps out the auth pattern documented in
23
+ `examples/docs/src/routes/docs/recipes-auth.md`; the user replaces
24
+ `verifyCredentials()` with a real DB lookup.
27
25
 
28
- If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
29
- (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
30
- `generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
31
- config with the pracht plugin registered.
26
+ MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
27
+ `inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify`/`generate_*`
28
+ tools over shelling out. `pracht inspect` needs the pracht plugin in the vite
29
+ config.
32
30
 
33
31
  ## Step 1: Confirm the scope
34
32
 
35
- Use `AskUserQuestion`:
33
+ Use `AskUserQuestion` for:
36
34
 
37
- 1. **What flavor?** Session cookie + email/password (default) OR magic link
38
- OR OAuth (out of scope recommend a separate skill / library).
39
- 2. **Where do credentials live?** A DB the user already has, or no DB yet?
40
- If no DB, recommend running `add-db` first.
41
- 3. **Cookie posture for CSRF**: `SameSite=Lax` (default, recommended) vs.
42
- `SameSite=Strict` vs. `SameSite=None` + token. (Cross-link `audit-csrf`.)
43
-
44
- This skill defaults to: session cookie + email/password + `SameSite=Lax`.
35
+ 1. **Flavor** session cookie + email/password (default), magic link, or OAuth
36
+ (out of scope: recommend a dedicated library).
37
+ 2. **Where credentials live** an existing DB, or none yet? If none, run
38
+ `/add-db` first.
39
+ 3. **Cookie posture** `SameSite=Lax` (default, recommended), `Strict`, or
40
+ `None` + token. See `/audit-csrf`.
45
41
 
46
42
  ## Step 2: Session utilities
47
43
 
48
- `src/server/session.ts`:
44
+ `src/server/session.ts` — HMAC-signed cookie payload:
49
45
 
50
46
  ```ts
51
47
  import { serverEnv } from "@pracht/core/env/server";
@@ -80,10 +76,10 @@ export function clearSessionCookie(): string {
80
76
  }
81
77
 
82
78
  async function getKey(usage: "sign" | "verify"): Promise<CryptoKey> {
83
- // Read the secret INSIDE the function, never at module scope. On
84
- // Cloudflare Workers env bindings only exist per request — a module-level
85
- // `process.env.SESSION_SECRET` read (or throw) bricks the worker at import
86
- // time. `serverEnv` resolves correctly per adapter (see docs/ENV.md).
79
+ // Read the secret INSIDE the function, never at module scope. On Cloudflare
80
+ // Workers env bindings only exist per request — a module-level read (or
81
+ // throw) bricks the worker at import time. `serverEnv` resolves correctly
82
+ // per adapter (docs/ENV.md).
87
83
  const secret = serverEnv.SESSION_SECRET;
88
84
  if (!secret) throw new Error("SESSION_SECRET is required");
89
85
  return crypto.subtle.importKey(
@@ -113,17 +109,17 @@ async function verify(data: string, signature: string): Promise<boolean> {
113
109
  }
114
110
  ```
115
111
 
116
- Notes:
117
112
  - `crypto.subtle` works in Node 18+, Cloudflare Workers, and Vercel Edge.
118
- - Signature verification goes through `crypto.subtle.verify`, which is a
119
- constant-time comparison. Never compare signature strings with `===` — that
120
- leaks timing information an attacker can use to forge signatures.
121
- - Drop `Secure` only if the user is on plain HTTP locally (recommend
122
- conditionalizing on `NODE_ENV`).
113
+ - Verification goes through `crypto.subtle.verify`, which compares in constant
114
+ time. Never compare signature strings with `===` — that leaks timing an
115
+ attacker can use to forge signatures.
116
+ - Keep `HttpOnly`, `SameSite=Lax`, and `Secure` on the cookie. Drop `Secure`
117
+ only for plain-HTTP local dev, conditionalized on `NODE_ENV`.
123
118
 
124
119
  ## Step 3: Auth middleware
125
120
 
126
- `src/middleware/auth.ts`:
121
+ `src/middleware/auth.ts` — a **Gate**: it short-circuits with a redirect rather
122
+ than merely augmenting context (see `/audit-auth` for that distinction).
127
123
 
128
124
  ```ts
129
125
  import { redirect, type MiddlewareFn } from "@pracht/core";
@@ -141,10 +137,7 @@ export const middleware: MiddlewareFn = async ({ request, url }, next) => {
141
137
  };
142
138
  ```
143
139
 
144
- This is a **Gate** (short-circuits with a redirect on failure). Cross-reference
145
- `audit-auth` for the distinction between Gate and Augmenter.
146
-
147
- ## Step 4: Login / logout API routes
140
+ ## Step 4: Auth API routes
148
141
 
149
142
  `src/api/auth/login.ts`:
150
143
 
@@ -158,23 +151,18 @@ export async function POST({ request }: ApiRouteArgs) {
158
151
  const password = String(form.get("password") ?? "");
159
152
  const requested = String(form.get("redirect") ?? "/dashboard");
160
153
 
161
- // Enforce same-origin redirect (defense against open-redirect via form input).
154
+ // The redirect field is user input gate it, or this is an open redirect.
162
155
  const safeRedirect = requested.startsWith("/") && !requested.startsWith("//")
163
156
  ? requested
164
157
  : "/dashboard";
165
158
 
166
159
  const user = await verifyCredentials(email, password);
167
160
  if (!user) {
168
- // Redirect back to /login with an error flag — do NOT return a 401 JSON
169
- // body. Pracht's <Form> only acts on 3xx responses (it follows the
170
- // `location` header); a non-redirect response is silently ignored by the
171
- // client, so the user would see nothing happen. The login page loader
172
- // reads `?error=1` and renders the message.
161
+ // Redirect back with an error flag — do NOT return a 401 JSON body.
162
+ // Pracht's <Form> only acts on 3xx (it follows `location`); a non-redirect
163
+ // response is silently ignored and the user sees nothing happen.
173
164
  const back = new URLSearchParams({ error: "1", redirect: safeRedirect });
174
- return new Response(null, {
175
- status: 302,
176
- headers: { location: `/login?${back}` },
177
- });
165
+ return new Response(null, { status: 302, headers: { location: `/login?${back}` } });
178
166
  }
179
167
 
180
168
  const cookie = await createSessionCookie({ userId: user.id, email: user.email });
@@ -190,60 +178,26 @@ async function verifyCredentials(_email: string, _password: string) {
190
178
  }
191
179
  ```
192
180
 
193
- `src/api/auth/logout.ts`:
194
-
195
- ```ts
196
- import type { ApiRouteArgs } from "@pracht/core";
197
- import { clearSessionCookie } from "../../server/session";
198
-
199
- export async function POST(_args: ApiRouteArgs) {
200
- return new Response(null, {
201
- status: 302,
202
- headers: { location: "/", "set-cookie": clearSessionCookie() },
203
- });
204
- }
205
- ```
206
-
207
- `src/api/auth/signup.ts` (skeleton — user wires hashing + DB insert):
181
+ `src/api/auth/logout.ts` — `POST` returning a 302 to `/` with
182
+ `clearSessionCookie()` as `set-cookie`.
208
183
 
209
- ```ts
210
- import type { ApiRouteArgs } from "@pracht/core";
211
- import { createSessionCookie } from "../../server/session";
184
+ `src/api/auth/signup.ts` — same shape as login: validate (`email` present,
185
+ `password.length >= 8`), redirect to `/signup?error=1` on failure, otherwise
186
+ hash the password, insert the user, and issue `createSessionCookie()` with a
187
+ 302 to `/dashboard`. Leave the hashing and insert as TODOs for the user.
212
188
 
213
- export async function POST({ request }: ApiRouteArgs) {
214
- const form = await request.formData();
215
- const email = String(form.get("email") ?? "").trim();
216
- const password = String(form.get("password") ?? "");
217
- if (!email || password.length < 8) {
218
- // Same redirect-with-flag pattern as login — <Form> ignores non-3xx.
219
- return new Response(null, {
220
- status: 302,
221
- headers: { location: "/signup?error=1" },
222
- });
223
- }
224
- // TODO: hash password, insert user, set session.
225
- const user = { id: crypto.randomUUID(), email };
226
- const cookie = await createSessionCookie({ userId: user.id, email: user.email });
227
- return new Response(null, {
228
- status: 302,
229
- headers: { location: "/dashboard", "set-cookie": cookie },
230
- });
231
- }
232
- ```
189
+ **Never ship the skeleton without real password hashing (argon2 or bcrypt).**
233
190
 
234
- ## Step 5: Login & signup pages
191
+ ## Step 5: Login and signup pages
235
192
 
236
193
  `src/routes/login.tsx`:
237
194
 
238
195
  ```tsx
239
- import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
240
- import { Form } from "@pracht/core";
196
+ import { Form, type LoaderArgs, type RouteComponentProps } from "@pracht/core";
241
197
 
242
198
  export async function loader({ url }: LoaderArgs) {
243
199
  return {
244
200
  redirect: url.searchParams.get("redirect") ?? "/dashboard",
245
- // Set by the login API route on failed credentials (see Step 4 — the
246
- // API redirects back here because <Form> only acts on 3xx responses).
247
201
  error: url.searchParams.get("error") === "1",
248
202
  };
249
203
  }
@@ -272,17 +226,12 @@ Generate `signup.tsx` analogously, posting to `/api/auth/signup`.
272
226
 
273
227
  ## Step 6: Wire the manifest
274
228
 
275
- ```ts
276
- import { defineApp, group, route } from "@pracht/core";
229
+ Public routes in one group, protected routes in a group carrying the middleware:
277
230
 
231
+ ```ts
278
232
  export const app = defineApp({
279
- shells: {
280
- public: "./shells/public.tsx",
281
- app: "./shells/app.tsx",
282
- },
283
- middleware: {
284
- auth: "./middleware/auth.ts",
285
- },
233
+ shells: { public: "./shells/public.tsx", app: "./shells/app.tsx" },
234
+ middleware: { auth: "./middleware/auth.ts" },
286
235
  routes: [
287
236
  group({ shell: "public" }, [
288
237
  route("/", "./routes/home.tsx", { render: "ssg" }),
@@ -291,56 +240,27 @@ export const app = defineApp({
291
240
  ]),
292
241
  group({ shell: "app", middleware: ["auth"] }, [
293
242
  route("/dashboard", "./routes/dashboard.tsx", { render: "ssr" }),
294
- // any other protected routes…
295
243
  ]),
296
244
  ],
297
245
  });
298
246
  ```
299
247
 
300
- If the project already has `defineApp({...})`, merge — preserve existing
301
- shells/middleware/routes.
248
+ If the project already has a `defineApp({...})`, merge into it — preserve the
249
+ existing shells, middleware, and routes.
302
250
 
303
- ## Step 7: Env vars
304
-
305
- Add to `.env.example`:
306
-
307
- ```
308
- SESSION_SECRET=<generate with: openssl rand -base64 32>
309
- ```
251
+ ## Step 7: Env
310
252
 
311
- Confirm `.env*` is gitignored.
253
+ Add `SESSION_SECRET=<generate with: openssl rand -base64 32>` to
254
+ `.env.example`, and confirm `.env*` is gitignored.
312
255
 
313
256
  ## Step 8: Verify
314
257
 
315
- - Step 6 added routes — run `pracht typegen` to refresh
316
- `src/pracht.d.ts` / `src/pracht-routes.ts` (use
317
- `pracht typegen --check` in CI).
318
- - `pracht dev`, navigate to `/dashboard` redirects to
319
- `/login?redirect=%2Fdashboard`.
320
- - After successful login, lands on `/dashboard`. After a failed login, lands
321
- back on `/login?error=1` with the error message rendered.
322
- - Logout posts to `/api/auth/logout` and clears the cookie.
323
- - Run `pnpm test` and `pnpm e2e`.
324
- - Run `pracht verify --json` and confirm no failures.
325
- - Run `audit-csrf` and `audit-auth` after wiring to confirm posture.
326
-
327
- ## Rules
328
-
329
- 1. Always set `HttpOnly`, `SameSite=Lax`, `Secure` on the session cookie.
330
- 2. The login form's `redirect` input is user-supplied — gate it server-side
331
- (`startsWith('/')` AND `!startsWith('//')`). Otherwise this is an open
332
- redirect.
333
- 3. `verifyCredentials` is a placeholder — never ship the skeleton without
334
- real password hashing (argon2 or bcrypt).
335
- 4. Read `SESSION_SECRET` via `serverEnv` (from `@pracht/core/env/server`)
336
- inside the signing/verifying functions and fail loudly there if missing.
337
- Never read or validate it at module scope — on Cloudflare Workers env
338
- bindings only exist per request, so a module-level throw bricks the worker
339
- at import time.
340
- 5. Failed form posts must answer with a 3xx redirect carrying an error flag —
341
- `<Form>` ignores non-redirect responses, so 4xx JSON bodies are invisible
342
- to the user.
343
- 6. After wiring, recommend running `audit-auth` to confirm protected routes
344
- are gated and `audit-csrf` for CSRF posture.
258
+ - `pracht typegen` (step 6 added routes); `pracht typegen --check` in CI.
259
+ - In `pracht dev`: `/dashboard` redirects to `/login?redirect=%2Fdashboard`; a
260
+ successful login lands on `/dashboard`; a failed one lands back on
261
+ `/login?error=1` with the message rendered; logout posts to
262
+ `/api/auth/logout` and clears the cookie.
263
+ - `pnpm test`, `pnpm e2e`, and `pracht verify --json` all pass.
264
+ - Run `/audit-auth` and `/audit-csrf` to confirm the resulting posture.
345
265
 
346
266
  $ARGUMENTS