create-pracht 0.3.0 → 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.
- package/README.md +11 -1
- package/package.json +3 -2
- package/skills/add-auth/SKILL.md +346 -0
- package/skills/add-db/SKILL.md +316 -0
- package/skills/add-i18n/SKILL.md +239 -0
- package/skills/add-observability/SKILL.md +270 -0
- package/skills/audit-a11y/SKILL.md +170 -0
- package/skills/audit-auth/SKILL.md +144 -0
- package/skills/audit-bundles/SKILL.md +167 -0
- package/skills/audit-csrf/SKILL.md +179 -0
- package/skills/audit-deps/SKILL.md +138 -0
- package/skills/audit-headers/SKILL.md +186 -0
- package/skills/audit-islands/SKILL.md +144 -0
- package/skills/audit-loaders/SKILL.md +135 -0
- package/skills/audit-redirects/SKILL.md +146 -0
- package/skills/audit-secrets/SKILL.md +150 -0
- package/skills/audit-seo/SKILL.md +164 -0
- package/skills/audit-shells/SKILL.md +142 -0
- package/skills/configure-isg/SKILL.md +172 -0
- package/skills/migrate-nextjs/SKILL.md +519 -0
- package/skills/pracht-debug/SKILL.md +146 -0
- package/skills/pracht-deploy/SKILL.md +208 -0
- package/skills/pracht-scaffold/SKILL.md +191 -0
- package/skills/pracht-test-api/SKILL.md +165 -0
- package/skills/pre-deploy/SKILL.md +164 -0
- package/skills/scaffold-e2e/SKILL.md +189 -0
- package/skills/scaffold-tests/SKILL.md +283 -0
- package/skills/tune-render-mode/SKILL.md +168 -0
- package/skills/typed-routes/SKILL.md +203 -0
- package/skills/upgrade-pracht/SKILL.md +151 -0
- package/src/index.js +146 -8
package/README.md
CHANGED
|
@@ -18,6 +18,9 @@ npm run dev
|
|
|
18
18
|
- Lets the user choose between the Node.js, Cloudflare, and Vercel adapters.
|
|
19
19
|
- Optionally wires up Tailwind CSS (`tailwindcss` + `@tailwindcss/vite`, a global stylesheet, and the shell import).
|
|
20
20
|
- Scaffolds a minimal app with a route manifest or pages router, shell, home route, sample API route, runnable project README, and agent instructions.
|
|
21
|
+
- Manifest scaffolds include a commented-out `constraints` example in `src/routes.ts`, ready for `pracht verify`.
|
|
22
|
+
- The generated `.gitignore` keeps `.pracht/app-graph.json` committable, and the README and agent instructions cover the `pracht verify` / `pracht plan` / `pracht report` loop.
|
|
23
|
+
- Seeds the pracht Claude Code skills into `.claude/skills/` and writes a `.mcp.json` registering the `pracht mcp` server (yes-default prompt; skipped with `--no-agent-tools`).
|
|
21
24
|
- Initializes a git repository with an initial commit (skipped with `--no-git`, when git is unavailable, or when the target is already inside a repository).
|
|
22
25
|
- `--dry-run` uses pinned fallback versions and does not require npm registry access.
|
|
23
26
|
|
|
@@ -37,9 +40,10 @@ node ./packages/start/bin/create-pracht.js my-app --adapter=node --no-tailwind -
|
|
|
37
40
|
- `--router=manifest|pages` — choose the routing system (default: manifest).
|
|
38
41
|
- `--template=minimal|tailwind` — non-interactive template selection; `minimal` is the default output, `tailwind` is minimal plus Tailwind CSS wiring.
|
|
39
42
|
- `--tailwind` / `--no-tailwind` — enable or disable Tailwind CSS without going through the prompt.
|
|
43
|
+
- `--agent-tools` / `--no-agent-tools` — seed the Claude Code skills and `.mcp.json` (or skip them) without going through the prompt.
|
|
40
44
|
- `--no-git` — skip `git init` and the initial commit.
|
|
41
45
|
- `--skip-install` — skip dependency installation.
|
|
42
|
-
- `--yes`, `-y` — accept defaults (node adapter, manifest router, no Tailwind) and skip all prompts.
|
|
46
|
+
- `--yes`, `-y` — accept defaults (node adapter, manifest router, no Tailwind, agent tooling on) and skip all prompts.
|
|
43
47
|
- `--json` — output a JSON summary instead of prose.
|
|
44
48
|
- `--dry-run` — list the files that would be created without writing anything.
|
|
45
49
|
|
|
@@ -52,6 +56,12 @@ node ./packages/start/bin/create-pracht.js my-app --adapter=node --no-tailwind -
|
|
|
52
56
|
- `src/shells/public.tsx`
|
|
53
57
|
- `src/api/health.ts`
|
|
54
58
|
- `.gitignore`
|
|
59
|
+
- `.claude/skills/<name>/SKILL.md` — the pracht agent skills (unless `--no-agent-tools`)
|
|
60
|
+
- `.mcp.json` — registers the `pracht mcp` server for MCP clients (unless `--no-agent-tools`)
|
|
61
|
+
|
|
62
|
+
The skills are copied from the repo's [skills/](../../skills/README.md) directory into this
|
|
63
|
+
package at build/publish time (`scripts/sync-skills.js`), so the published npm tarball is
|
|
64
|
+
self-contained.
|
|
55
65
|
|
|
56
66
|
Node scaffolds also include:
|
|
57
67
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pracht",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Interactive and scriptable starter CLI for creating full-stack Preact apps with Pracht.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pracht",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"bin",
|
|
31
31
|
"src",
|
|
32
|
+
"skills",
|
|
32
33
|
"README.md"
|
|
33
34
|
],
|
|
34
35
|
"type": "module",
|
|
@@ -36,6 +37,6 @@
|
|
|
36
37
|
"provenance": true
|
|
37
38
|
},
|
|
38
39
|
"scripts": {
|
|
39
|
-
"build": "node -
|
|
40
|
+
"build": "node ./scripts/sync-skills.js"
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-auth
|
|
3
|
+
version: 1.1.0
|
|
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".
|
|
12
|
+
allowed-tools:
|
|
13
|
+
- Bash
|
|
14
|
+
- Read
|
|
15
|
+
- Write
|
|
16
|
+
- Edit
|
|
17
|
+
- Grep
|
|
18
|
+
- Glob
|
|
19
|
+
- AskUserQuestion
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Pracht Add Auth
|
|
23
|
+
|
|
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.
|
|
27
|
+
|
|
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.
|
|
32
|
+
|
|
33
|
+
## Step 1: Confirm the scope
|
|
34
|
+
|
|
35
|
+
Use `AskUserQuestion`:
|
|
36
|
+
|
|
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`.
|
|
45
|
+
|
|
46
|
+
## Step 2: Session utilities
|
|
47
|
+
|
|
48
|
+
`src/server/session.ts`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { serverEnv } from "@pracht/core/env/server";
|
|
52
|
+
|
|
53
|
+
export interface Session {
|
|
54
|
+
userId: string;
|
|
55
|
+
email: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getSession(request: Request): Promise<Session | null> {
|
|
59
|
+
const cookie = request.headers.get("cookie") ?? "";
|
|
60
|
+
const match = cookie.match(/session=([^;]+)/);
|
|
61
|
+
if (!match) return null;
|
|
62
|
+
try {
|
|
63
|
+
const [payload, signature] = match[1].split(".");
|
|
64
|
+
if (!payload || !signature) return null;
|
|
65
|
+
if (!(await verify(payload, signature))) return null;
|
|
66
|
+
return JSON.parse(atob(payload));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function createSessionCookie(session: Session): Promise<string> {
|
|
73
|
+
const payload = btoa(JSON.stringify(session));
|
|
74
|
+
const signature = await sign(payload);
|
|
75
|
+
return `session=${payload}.${signature}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=604800`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function clearSessionCookie(): string {
|
|
79
|
+
return "session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
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).
|
|
87
|
+
const secret = serverEnv.SESSION_SECRET;
|
|
88
|
+
if (!secret) throw new Error("SESSION_SECRET is required");
|
|
89
|
+
return crypto.subtle.importKey(
|
|
90
|
+
"raw",
|
|
91
|
+
new TextEncoder().encode(secret),
|
|
92
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
93
|
+
false,
|
|
94
|
+
[usage],
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function sign(data: string): Promise<string> {
|
|
99
|
+
const key = await getKey("sign");
|
|
100
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
|
|
101
|
+
return btoa(String.fromCharCode(...new Uint8Array(sig)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function verify(data: string, signature: string): Promise<boolean> {
|
|
105
|
+
let sig: Uint8Array;
|
|
106
|
+
try {
|
|
107
|
+
sig = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const key = await getKey("verify");
|
|
112
|
+
return crypto.subtle.verify("HMAC", key, sig, new TextEncoder().encode(data));
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Notes:
|
|
117
|
+
- `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`).
|
|
123
|
+
|
|
124
|
+
## Step 3: Auth middleware
|
|
125
|
+
|
|
126
|
+
`src/middleware/auth.ts`:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { redirect, type MiddlewareFn } from "@pracht/core";
|
|
130
|
+
import { getSession } from "../server/session";
|
|
131
|
+
|
|
132
|
+
export const middleware: MiddlewareFn = async ({ request, url }, next) => {
|
|
133
|
+
const session = await getSession(request);
|
|
134
|
+
if (!session) {
|
|
135
|
+
const target = encodeURIComponent(url.pathname + url.search);
|
|
136
|
+
return redirect(`/login?redirect=${target}`, { request });
|
|
137
|
+
}
|
|
138
|
+
request.headers.set("x-user-id", session.userId);
|
|
139
|
+
request.headers.set("x-user-email", session.email);
|
|
140
|
+
return next();
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
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
|
|
148
|
+
|
|
149
|
+
`src/api/auth/login.ts`:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import type { ApiRouteArgs } from "@pracht/core";
|
|
153
|
+
import { createSessionCookie } from "../../server/session";
|
|
154
|
+
|
|
155
|
+
export async function POST({ request }: ApiRouteArgs) {
|
|
156
|
+
const form = await request.formData();
|
|
157
|
+
const email = String(form.get("email") ?? "").trim();
|
|
158
|
+
const password = String(form.get("password") ?? "");
|
|
159
|
+
const requested = String(form.get("redirect") ?? "/dashboard");
|
|
160
|
+
|
|
161
|
+
// Enforce same-origin redirect (defense against open-redirect via form input).
|
|
162
|
+
const safeRedirect = requested.startsWith("/") && !requested.startsWith("//")
|
|
163
|
+
? requested
|
|
164
|
+
: "/dashboard";
|
|
165
|
+
|
|
166
|
+
const user = await verifyCredentials(email, password);
|
|
167
|
+
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.
|
|
173
|
+
const back = new URLSearchParams({ error: "1", redirect: safeRedirect });
|
|
174
|
+
return new Response(null, {
|
|
175
|
+
status: 302,
|
|
176
|
+
headers: { location: `/login?${back}` },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const cookie = await createSessionCookie({ userId: user.id, email: user.email });
|
|
181
|
+
return new Response(null, {
|
|
182
|
+
status: 302,
|
|
183
|
+
headers: { location: safeRedirect, "set-cookie": cookie },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function verifyCredentials(_email: string, _password: string) {
|
|
188
|
+
// TODO: replace with a real DB lookup + password hash check (argon2 / bcrypt).
|
|
189
|
+
return null as null | { id: string; email: string };
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
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):
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import type { ApiRouteArgs } from "@pracht/core";
|
|
211
|
+
import { createSessionCookie } from "../../server/session";
|
|
212
|
+
|
|
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
|
+
```
|
|
233
|
+
|
|
234
|
+
## Step 5: Login & signup pages
|
|
235
|
+
|
|
236
|
+
`src/routes/login.tsx`:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
|
|
240
|
+
import { Form } from "@pracht/core";
|
|
241
|
+
|
|
242
|
+
export async function loader({ url }: LoaderArgs) {
|
|
243
|
+
return {
|
|
244
|
+
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
|
+
error: url.searchParams.get("error") === "1",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function head() {
|
|
252
|
+
return { title: "Log in" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function Component({ data }: RouteComponentProps<typeof loader>) {
|
|
256
|
+
return (
|
|
257
|
+
<section class="login">
|
|
258
|
+
<h1>Log in</h1>
|
|
259
|
+
{data.error && <p class="error">Invalid email or password.</p>}
|
|
260
|
+
<Form method="post" action="/api/auth/login">
|
|
261
|
+
<input type="hidden" name="redirect" value={data.redirect} />
|
|
262
|
+
<label>Email <input type="email" name="email" required /></label>
|
|
263
|
+
<label>Password <input type="password" name="password" required /></label>
|
|
264
|
+
<button type="submit">Log in</button>
|
|
265
|
+
</Form>
|
|
266
|
+
</section>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Generate `signup.tsx` analogously, posting to `/api/auth/signup`.
|
|
272
|
+
|
|
273
|
+
## Step 6: Wire the manifest
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { defineApp, group, route } from "@pracht/core";
|
|
277
|
+
|
|
278
|
+
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
|
+
},
|
|
286
|
+
routes: [
|
|
287
|
+
group({ shell: "public" }, [
|
|
288
|
+
route("/", "./routes/home.tsx", { render: "ssg" }),
|
|
289
|
+
route("/login", "./routes/login.tsx", { render: "ssr" }),
|
|
290
|
+
route("/signup", "./routes/signup.tsx", { render: "ssr" }),
|
|
291
|
+
]),
|
|
292
|
+
group({ shell: "app", middleware: ["auth"] }, [
|
|
293
|
+
route("/dashboard", "./routes/dashboard.tsx", { render: "ssr" }),
|
|
294
|
+
// any other protected routes…
|
|
295
|
+
]),
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
If the project already has `defineApp({...})`, merge — preserve existing
|
|
301
|
+
shells/middleware/routes.
|
|
302
|
+
|
|
303
|
+
## Step 7: Env vars
|
|
304
|
+
|
|
305
|
+
Add to `.env.example`:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
SESSION_SECRET=<generate with: openssl rand -base64 32>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Confirm `.env*` is gitignored.
|
|
312
|
+
|
|
313
|
+
## Step 8: Verify
|
|
314
|
+
|
|
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.
|
|
345
|
+
|
|
346
|
+
$ARGUMENTS
|