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.
- package/package.json +1 -1
- package/skills/add-auth/SKILL.md +63 -143
- package/skills/add-capabilities/SKILL.md +411 -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 +48 -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 -424
- package/skills/pracht-debug/SKILL.md +165 -128
- package/skills/pracht-deploy/SKILL.md +246 -331
- package/skills/pracht-scaffold/SKILL.md +123 -146
- package/skills/pracht-test-api/SKILL.md +10 -10
- package/skills/pre-deploy/SKILL.md +165 -200
- 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 +9 -9
- package/skills/upgrade-pracht/SKILL.md +7 -8
- package/src/index.js +39 -1
package/package.json
CHANGED
package/skills/add-auth/SKILL.md
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: add-auth
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.2.0
|
|
4
4
|
description: |
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
25
|
-
`examples/docs/src/routes/docs/recipes-auth.md
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
config
|
|
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. **
|
|
38
|
-
|
|
39
|
-
2. **Where
|
|
40
|
-
|
|
41
|
-
3. **Cookie posture
|
|
42
|
-
`
|
|
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
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
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
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
169
|
-
//
|
|
170
|
-
//
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
191
|
+
## Step 5: Login and signup pages
|
|
235
192
|
|
|
236
193
|
`src/routes/login.tsx`:
|
|
237
194
|
|
|
238
195
|
```tsx
|
|
239
|
-
import type
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
301
|
-
shells
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
`/
|
|
320
|
-
-
|
|
321
|
-
|
|
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
|