@baldurpan/create-ai-workflow 0.2.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/agents-block.js +70 -0
- package/dist/bin.js +14 -0
- package/dist/check/markdown.js +47 -0
- package/dist/check/parse.js +113 -0
- package/dist/check/rules.js +245 -0
- package/dist/cli.js +109 -0
- package/dist/commands/check.js +41 -0
- package/dist/commands/install.js +76 -0
- package/dist/commands/standards-add.js +145 -0
- package/dist/commands/update.js +214 -0
- package/dist/layout.js +115 -0
- package/dist/log.js +20 -0
- package/dist/manifest.js +53 -0
- package/dist/paths.js +41 -0
- package/package.json +37 -0
- package/templates/blocks/agents-block.md +30 -0
- package/templates/claude/agents/planner.agent.md +60 -0
- package/templates/claude/agents/reviewer.agent.md +77 -0
- package/templates/context/README.md +81 -0
- package/templates/context/plan-template.md +69 -0
- package/templates/context/plan-template.notes.md +55 -0
- package/templates/context/roles/coder.md +76 -0
- package/templates/context/workflow.md +143 -0
- package/templates/skills/feature-close/SKILL.md +102 -0
- package/templates/skills/feature-implement/SKILL.md +142 -0
- package/templates/skills/feature-plan/SKILL.md +138 -0
- package/templates/skills/feature-status/SKILL.md +86 -0
- package/templates/skills/onboard/SKILL.md +122 -0
- package/templates/skills/orchestrate/SKILL.md +92 -0
- package/templates/skills/roadmap/SKILL.md +97 -0
- package/templates/standards/.source +8 -0
- package/templates/standards/README.md +104 -0
- package/templates/standards/architecture/api-design.md +212 -0
- package/templates/standards/architecture/dependency-boundaries.md +104 -0
- package/templates/standards/architecture/feature-driven.md +102 -0
- package/templates/standards/architecture/folder-structure.md +108 -0
- package/templates/standards/architecture/monorepos.md +107 -0
- package/templates/standards/architecture/refactoring.md +97 -0
- package/templates/standards/architecture/shared-code.md +81 -0
- package/templates/standards/docs/PHP-SPEC.md +1195 -0
- package/templates/standards/docs/SPEC.md +540 -0
- package/templates/standards/examples/bad/data-fetching.tsx +52 -0
- package/templates/standards/examples/bad/derived-state.tsx +60 -0
- package/templates/standards/examples/bad/feature-component.tsx +80 -0
- package/templates/standards/examples/bad/prop-drilling.tsx +70 -0
- package/templates/standards/examples/good/data-fetching.tsx +53 -0
- package/templates/standards/examples/good/derived-state.tsx +52 -0
- package/templates/standards/examples/good/feature-component.tsx +141 -0
- package/templates/standards/examples/good/invoice.schema.ts +20 -0
- package/templates/standards/examples/good/prop-drilling.tsx +83 -0
- package/templates/standards/philosophy/ai-agent-behavior.md +93 -0
- package/templates/standards/philosophy/core-principles.md +49 -0
- package/templates/standards/philosophy/incremental-abstraction.md +56 -0
- package/templates/standards/philosophy/maintainability.md +66 -0
- package/templates/standards/philosophy/readability.md +87 -0
- package/templates/standards/php/README.md +62 -0
- package/templates/standards/php/anti-patterns.md +259 -0
- package/templates/standards/php/architecture.md +241 -0
- package/templates/standards/php/frameworks.md +197 -0
- package/templates/standards/php/rules.md +205 -0
- package/templates/standards/php/security.md +190 -0
- package/templates/standards/php/testing.md +139 -0
- package/templates/standards/php/tooling.md +245 -0
- package/templates/standards/react/accessibility.md +130 -0
- package/templates/standards/react/anti-patterns.md +187 -0
- package/templates/standards/react/component-design.md +139 -0
- package/templates/standards/react/error-boundaries.md +195 -0
- package/templates/standards/react/forms.md +147 -0
- package/templates/standards/react/hooks.md +88 -0
- package/templates/standards/react/memoization.md +87 -0
- package/templates/standards/react/state-management.md +123 -0
- package/templates/standards/react/testing.md +118 -0
- package/templates/standards/react/use-effect.md +110 -0
- package/templates/standards/security/api-security.md +136 -0
- package/templates/standards/security/auth.md +112 -0
- package/templates/standards/security/secrets.md +105 -0
- package/templates/standards/security/validation.md +89 -0
- package/templates/standards/templates/.editorconfig +16 -0
- package/templates/standards/templates/.nvmrc +1 -0
- package/templates/standards/templates/AGENTS.md +46 -0
- package/templates/standards/templates/CLAUDE.md +3 -0
- package/templates/standards/templates/_dot_gitignore +50 -0
- package/templates/standards/templates/biome.json +53 -0
- package/templates/standards/templates/eslint.config.js +61 -0
- package/templates/standards/templates/project-readme.md +76 -0
- package/templates/standards/templates/pull-request-template.md +46 -0
- package/templates/standards/templates/tsconfig.json +35 -0
- package/templates/standards/tooling/biome.md +92 -0
- package/templates/standards/tooling/ci.md +128 -0
- package/templates/standards/tooling/dates.md +194 -0
- package/templates/standards/tooling/dependencies.md +90 -0
- package/templates/standards/tooling/nx.md +113 -0
- package/templates/standards/tooling/observability.md +183 -0
- package/templates/standards/tooling/prisma.md +152 -0
- package/templates/standards/tooling/shadcn.md +115 -0
- package/templates/standards/tooling/tailwind.md +135 -0
- package/templates/standards/tooling/tanstack-query.md +214 -0
- package/templates/standards/tooling/tanstack-router.md +365 -0
- package/templates/standards/tooling/tanstack.md +31 -0
- package/templates/standards/tooling/vite.md +135 -0
- package/templates/standards/typescript/anti-patterns.md +107 -0
- package/templates/standards/typescript/error-handling.md +216 -0
- package/templates/standards/typescript/naming.md +78 -0
- package/templates/standards/typescript/rules.md +159 -0
- package/templates/standards/typescript/tsconfig/base.json +32 -0
- package/templates/standards/typescript/tsconfig/nextjs.json +12 -0
- package/templates/standards/typescript/tsconfig/react.json +9 -0
- package/templates/standards/typescript/validation.md +104 -0
- package/templates/stubs/executors.md +49 -0
- package/templates/stubs/findings.md +40 -0
- package/templates/stubs/history.md +13 -0
- package/templates/stubs/roadmap.md +29 -0
- package/templates/stubs/stack.md +35 -0
- package/templates/stubs/verify.md +48 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# State Management
|
|
2
|
+
|
|
3
|
+
## Hierarchy
|
|
4
|
+
|
|
5
|
+
Choose the simplest state mechanism that works. Escalate only when necessary.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
1. URL state — shareable, bookmarkable, free
|
|
9
|
+
2. Server state — TanStack Query (cached, synchronized, background-refreshed)
|
|
10
|
+
3. Local state — useState / useReducer
|
|
11
|
+
4. Context — for shared UI state (theme, current user, modal stack)
|
|
12
|
+
5. Zustand — only when Context is insufficient
|
|
13
|
+
6. Redux — never
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## URL State
|
|
17
|
+
|
|
18
|
+
Prefer URL state for anything that should be shareable, bookmarkable, or survive a refresh:
|
|
19
|
+
|
|
20
|
+
- Filters, search queries, pagination, selected tab
|
|
21
|
+
- Use TanStack Router's typed search params (validated with Zod)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// TanStack Router — typed search params
|
|
25
|
+
const search = invoicesRoute.useSearch();
|
|
26
|
+
const navigate = invoicesRoute.useNavigate();
|
|
27
|
+
|
|
28
|
+
function setStatus(status: InvoiceStatus) {
|
|
29
|
+
navigate({ search: (prev) => ({ ...prev, status, page: 1 }) });
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See [`../tooling/tanstack-router.md`](../tooling/tanstack-router.md) for the full pattern (route definition, validation, loaders).
|
|
34
|
+
|
|
35
|
+
## Server State — TanStack Query
|
|
36
|
+
|
|
37
|
+
For data that lives on the server, TanStack Query is the default:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const { data: invoices, isLoading } = useQuery({
|
|
41
|
+
queryKey: ["invoices", filters],
|
|
42
|
+
queryFn: () => fetchInvoices(filters),
|
|
43
|
+
staleTime: 60_000,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const { mutate: createInvoice } = useMutation({
|
|
47
|
+
mutationFn: createInvoiceFn,
|
|
48
|
+
onSuccess: () => {
|
|
49
|
+
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Do not duplicate server state into local state. Do not use `useEffect` + `useState` for data fetching.
|
|
55
|
+
|
|
56
|
+
## Local State
|
|
57
|
+
|
|
58
|
+
For UI-only state that does not need to be shared:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
62
|
+
const [activeTab, setActiveTab] = useState<"overview" | "details">("overview");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use `useReducer` when state transitions are complex or interrelated:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
type Action =
|
|
69
|
+
| { type: "SET_STEP"; step: number }
|
|
70
|
+
| { type: "SUBMIT" }
|
|
71
|
+
| { type: "RESET" };
|
|
72
|
+
|
|
73
|
+
function reducer(state: WizardState, action: Action): WizardState { ... }
|
|
74
|
+
|
|
75
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Context
|
|
79
|
+
|
|
80
|
+
Use Context for state that many components need but that does not belong in the server:
|
|
81
|
+
|
|
82
|
+
- Current user / session
|
|
83
|
+
- Theme / color scheme
|
|
84
|
+
- Open modals / toasts
|
|
85
|
+
- Feature flags
|
|
86
|
+
|
|
87
|
+
Keep Context providers small. Do not put everything in a single `AppContext`.
|
|
88
|
+
|
|
89
|
+
## Zustand
|
|
90
|
+
|
|
91
|
+
Use Zustand when:
|
|
92
|
+
- State is complex and cross-cutting enough that Context causes re-render issues
|
|
93
|
+
- Multiple independent slices of UI state need to be coordinated
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const useInvoiceStore = create<InvoiceStore>((set) => ({
|
|
97
|
+
selectedIds: [],
|
|
98
|
+
toggleSelected: (id) =>
|
|
99
|
+
set((state) => ({
|
|
100
|
+
selectedIds: state.selectedIds.includes(id)
|
|
101
|
+
? state.selectedIds.filter((x) => x !== id)
|
|
102
|
+
: [...state.selectedIds, id],
|
|
103
|
+
})),
|
|
104
|
+
}));
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Never Redux
|
|
108
|
+
|
|
109
|
+
Redux adds ceremony, boilerplate, and indirection that is not justified for modern React applications. TanStack Query + Zustand + URL state covers all real use cases with far less overhead.
|
|
110
|
+
|
|
111
|
+
## PRIORITY
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
URL state > Server state > Local state > Context > Zustand
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## See Also
|
|
118
|
+
|
|
119
|
+
- [`../tooling/tanstack-router.md`](../tooling/tanstack-router.md) — typed URL state, loaders
|
|
120
|
+
- [`../tooling/tanstack-query.md`](../tooling/tanstack-query.md) — server state cache
|
|
121
|
+
- [`use-effect.md`](use-effect.md) — what useEffect is and isn't for
|
|
122
|
+
- [`forms.md`](forms.md) — form state management
|
|
123
|
+
- [`hooks.md`](hooks.md) — composing hooks for stateful logic
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## Philosophy
|
|
4
|
+
|
|
5
|
+
Test behavior, not implementation. A test that breaks when you rename a variable is a bad test. A test that breaks when user-visible behavior changes is a good test.
|
|
6
|
+
|
|
7
|
+
Reference: [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
|
|
8
|
+
|
|
9
|
+
## Tooling
|
|
10
|
+
|
|
11
|
+
- **Vitest** — test runner (fast, native ESM, compatible with Jest API)
|
|
12
|
+
- **Testing Library** — component testing (`@testing-library/react`, `@testing-library/user-event`)
|
|
13
|
+
- **Playwright** — E2E tests for critical user flows only
|
|
14
|
+
|
|
15
|
+
## DO
|
|
16
|
+
|
|
17
|
+
- Test from the user's perspective — what does the user see and do?
|
|
18
|
+
- Query by accessible role, label, or text — not by `data-testid` or CSS class
|
|
19
|
+
- Use `userEvent` over `fireEvent` for realistic interaction simulation
|
|
20
|
+
- Test the happy path and the critical error paths
|
|
21
|
+
- Write unit tests for pure utility functions and complex business logic
|
|
22
|
+
- Write integration tests for feature flows (form submission, data fetching, state transitions)
|
|
23
|
+
|
|
24
|
+
## DO NOT
|
|
25
|
+
|
|
26
|
+
- Test implementation details (internal state, private methods, component structure)
|
|
27
|
+
- Use snapshot tests — they break constantly and encode nothing meaningful
|
|
28
|
+
- Mock things you do not own (browser APIs, global state) without good reason
|
|
29
|
+
- Assert on CSS class names or DOM structure
|
|
30
|
+
- Write tests that are tightly coupled to component internals
|
|
31
|
+
|
|
32
|
+
## Query Priority
|
|
33
|
+
|
|
34
|
+
Follow Testing Library's recommended query priority:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
1. getByRole — best: matches what a screen reader sees
|
|
38
|
+
2. getByLabelText — for form inputs
|
|
39
|
+
3. getByPlaceholderText — last resort for inputs
|
|
40
|
+
4. getByText — for non-interactive elements
|
|
41
|
+
5. getByDisplayValue — for form elements with a value
|
|
42
|
+
6. getByAltText — for images
|
|
43
|
+
7. getByTitle — for title attributes
|
|
44
|
+
8. getByTestId — last resort only (requires data-testid attribute)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Example — Integration Test
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { render, screen } from "@testing-library/react";
|
|
51
|
+
import userEvent from "@testing-library/user-event";
|
|
52
|
+
import { CreateInvoiceForm } from "./CreateInvoiceForm";
|
|
53
|
+
|
|
54
|
+
test("submits the form with valid data", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
const onSubmit = vi.fn();
|
|
57
|
+
|
|
58
|
+
render(<CreateInvoiceForm onSubmit={onSubmit} />);
|
|
59
|
+
|
|
60
|
+
await user.type(screen.getByLabelText("Title"), "Q4 Report");
|
|
61
|
+
await user.type(screen.getByLabelText("Amount"), "500");
|
|
62
|
+
await user.click(screen.getByRole("button", { name: "Create" }));
|
|
63
|
+
|
|
64
|
+
expect(onSubmit).toHaveBeenCalledWith({ title: "Q4 Report", amount: 500 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("shows validation errors for empty fields", async () => {
|
|
68
|
+
const user = userEvent.setup();
|
|
69
|
+
|
|
70
|
+
render(<CreateInvoiceForm onSubmit={vi.fn()} />);
|
|
71
|
+
|
|
72
|
+
await user.click(screen.getByRole("button", { name: "Create" }));
|
|
73
|
+
|
|
74
|
+
expect(screen.getByRole("alert")).toHaveTextContent("Title is required");
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Playwright — E2E (Critical Flows Only)
|
|
79
|
+
|
|
80
|
+
Reserve Playwright for flows that are:
|
|
81
|
+
- Business-critical (checkout, authentication, data export)
|
|
82
|
+
- Too complex to integration-test meaningfully
|
|
83
|
+
- Dependent on real browser behavior (file upload, clipboard, print)
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
test("user can log in and view their dashboard", async ({ page }) => {
|
|
87
|
+
await page.goto("/login");
|
|
88
|
+
await page.getByLabel("Email").fill("user@example.com");
|
|
89
|
+
await page.getByLabel("Password").fill("password");
|
|
90
|
+
await page.getByRole("button", { name: "Sign in" }).click();
|
|
91
|
+
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## File Colocation
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
features/
|
|
99
|
+
invoices/
|
|
100
|
+
InvoiceTable.tsx
|
|
101
|
+
InvoiceTable.test.tsx ← colocated with the component
|
|
102
|
+
useInvoiceSort.ts
|
|
103
|
+
useInvoiceSort.test.ts ← colocated with the hook
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## PRIORITY
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Integration tests > Unit tests > E2E tests
|
|
110
|
+
Behavior > Implementation
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## See Also
|
|
114
|
+
|
|
115
|
+
- [`component-design.md`](component-design.md) — testable component structure
|
|
116
|
+
- [`hooks.md`](hooks.md) — testing custom hooks via the components that use them
|
|
117
|
+
- [`accessibility.md`](accessibility.md) — query by role, label, text
|
|
118
|
+
- [`../architecture/refactoring.md`](../architecture/refactoring.md) — write tests before refactoring untested code
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# useEffect
|
|
2
|
+
|
|
3
|
+
## Core Rule
|
|
4
|
+
|
|
5
|
+
`useEffect` is an escape hatch for synchronizing with external systems. It is not a general-purpose lifecycle hook.
|
|
6
|
+
|
|
7
|
+
## VALID uses
|
|
8
|
+
|
|
9
|
+
- Synchronizing with external systems (WebSocket, EventEmitter, third-party widget)
|
|
10
|
+
- DOM APIs that require imperative access (focus management, scroll position, canvas)
|
|
11
|
+
- Subscriptions (event listeners, pub/sub, real-time)
|
|
12
|
+
- Timers and intervals
|
|
13
|
+
- Imperative bridges to non-React code
|
|
14
|
+
|
|
15
|
+
## INVALID uses
|
|
16
|
+
|
|
17
|
+
- Deriving state from props or other state
|
|
18
|
+
- Syncing two pieces of React state together
|
|
19
|
+
- Fetching data (use TanStack Query instead)
|
|
20
|
+
- Responding to user events (handle directly in event handlers)
|
|
21
|
+
- Updating state on prop change (compute during render instead)
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Correct — external system sync
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const socket = new WebSocket(url);
|
|
30
|
+
socket.addEventListener("message", handleMessage);
|
|
31
|
+
return () => {
|
|
32
|
+
socket.removeEventListener("message", handleMessage);
|
|
33
|
+
socket.close();
|
|
34
|
+
};
|
|
35
|
+
}, [url, handleMessage]);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Correct — DOM API
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (isOpen) {
|
|
43
|
+
inputRef.current?.focus();
|
|
44
|
+
}
|
|
45
|
+
}, [isOpen]);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Incorrect — derived state
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// BAD — deriving state with useEffect
|
|
52
|
+
const [fullName, setFullName] = useState("");
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setFullName(`${firstName} ${lastName}`);
|
|
55
|
+
}, [firstName, lastName]);
|
|
56
|
+
|
|
57
|
+
// GOOD — compute during render
|
|
58
|
+
const fullName = `${firstName} ${lastName}`;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Incorrect — state synchronization
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// BAD — syncing two pieces of state
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
setDraftValue(externalValue);
|
|
67
|
+
}, [externalValue]);
|
|
68
|
+
|
|
69
|
+
// GOOD — use a key to reset controlled state
|
|
70
|
+
<Input key={externalValue} defaultValue={externalValue} />
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Incorrect — data fetching
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// BAD — manual fetch in useEffect
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
let cancelled = false;
|
|
79
|
+
fetchUser(id).then((data) => {
|
|
80
|
+
if (!cancelled) setUser(data);
|
|
81
|
+
});
|
|
82
|
+
return () => { cancelled = true; };
|
|
83
|
+
}, [id]);
|
|
84
|
+
|
|
85
|
+
// GOOD — use TanStack Query
|
|
86
|
+
const { data: user } = useQuery({
|
|
87
|
+
queryKey: ["user", id],
|
|
88
|
+
queryFn: () => fetchUser(id),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Dependency Array Rules
|
|
93
|
+
|
|
94
|
+
- Always specify the dependency array — never omit it
|
|
95
|
+
- Include all reactive values used inside the effect
|
|
96
|
+
- If a dependency changes too often, consider `useRef` or restructuring
|
|
97
|
+
- Do not use `// eslint-disable-line react-hooks/exhaustive-deps` to suppress warnings — fix the underlying issue
|
|
98
|
+
|
|
99
|
+
## PRIORITY
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
No effect > Computed value > Event handler > useEffect
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## See Also
|
|
106
|
+
|
|
107
|
+
- [`state-management.md`](state-management.md) — where state should actually live
|
|
108
|
+
- [`hooks.md`](hooks.md) — composing hooks without useEffect abuse
|
|
109
|
+
- [`../tooling/tanstack-query.md`](../tooling/tanstack-query.md) — data fetching without useEffect
|
|
110
|
+
- [`anti-patterns.md`](anti-patterns.md) — common useEffect mistakes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# API Security
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Validate all input (see [validation.md](validation.md))
|
|
6
|
+
- Authenticate all protected routes
|
|
7
|
+
- Authorize every resource access
|
|
8
|
+
- Return minimal, appropriate HTTP status codes
|
|
9
|
+
- Rate limit public and sensitive endpoints
|
|
10
|
+
- Never expose internal error details to clients
|
|
11
|
+
|
|
12
|
+
## HTTP Status Codes
|
|
13
|
+
|
|
14
|
+
Use precise status codes:
|
|
15
|
+
|
|
16
|
+
| Code | When |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `200` | Successful GET, PATCH, PUT |
|
|
19
|
+
| `201` | Successful POST (resource created) |
|
|
20
|
+
| `204` | Successful DELETE (no content) |
|
|
21
|
+
| `400` | Malformed request (bad JSON, missing required field) |
|
|
22
|
+
| `401` | Not authenticated |
|
|
23
|
+
| `403` | Authenticated but not authorized |
|
|
24
|
+
| `404` | Resource not found |
|
|
25
|
+
| `409` | Conflict (duplicate, optimistic lock failure) |
|
|
26
|
+
| `422` | Validation error (well-formed but invalid data) |
|
|
27
|
+
| `429` | Rate limit exceeded |
|
|
28
|
+
| `500` | Internal server error |
|
|
29
|
+
|
|
30
|
+
## Error Responses
|
|
31
|
+
|
|
32
|
+
Return structured errors. Never expose stack traces or internal details:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// BAD — exposes internals
|
|
36
|
+
res.status(500).json({ error: error.stack });
|
|
37
|
+
|
|
38
|
+
// GOOD — structured, safe
|
|
39
|
+
res.status(422).json({
|
|
40
|
+
error: "Validation failed",
|
|
41
|
+
fields: result.error.flatten().fieldErrors,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Rate Limiting
|
|
46
|
+
|
|
47
|
+
Apply rate limiting to all public APIs:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const ratelimit = new Ratelimit({
|
|
51
|
+
redis: Redis.fromEnv(),
|
|
52
|
+
limiter: Ratelimit.slidingWindow(100, "1 m"), // 100 req/min
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export async function middleware(req: NextRequest) {
|
|
56
|
+
const ip = req.ip ?? "unknown";
|
|
57
|
+
const { success, limit, remaining } = await ratelimit.limit(ip);
|
|
58
|
+
|
|
59
|
+
if (!success) {
|
|
60
|
+
return NextResponse.json({ error: "Too many requests" }, {
|
|
61
|
+
status: 429,
|
|
62
|
+
headers: {
|
|
63
|
+
"X-RateLimit-Limit": limit.toString(),
|
|
64
|
+
"X-RateLimit-Remaining": remaining.toString(),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## CORS
|
|
72
|
+
|
|
73
|
+
Configure CORS explicitly. Do not allow all origins in production:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const corsOptions = {
|
|
77
|
+
origin: process.env.NODE_ENV === "production"
|
|
78
|
+
? ["https://app.example.com"]
|
|
79
|
+
: ["http://localhost:3000"],
|
|
80
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
81
|
+
credentials: true,
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## SQL Injection Prevention
|
|
86
|
+
|
|
87
|
+
Use parameterized queries. Prisma handles this automatically:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// SAFE — Prisma parameterizes automatically
|
|
91
|
+
const user = await db.user.findUnique({ where: { email } });
|
|
92
|
+
|
|
93
|
+
// DANGEROUS — never concatenate user input into raw SQL
|
|
94
|
+
const user = await db.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
|
|
95
|
+
|
|
96
|
+
// SAFE — use $queryRaw with tagged template (automatically parameterized)
|
|
97
|
+
const user = await db.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Content Security Policy
|
|
101
|
+
|
|
102
|
+
Set a Content Security Policy header to mitigate XSS:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// Next.js — next.config.ts
|
|
106
|
+
const securityHeaders = [
|
|
107
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
108
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
109
|
+
{ key: "X-XSS-Protection", value: "1; mode=block" },
|
|
110
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
111
|
+
{
|
|
112
|
+
key: "Content-Security-Policy",
|
|
113
|
+
value: [
|
|
114
|
+
"default-src 'self'",
|
|
115
|
+
"script-src 'self' 'unsafe-inline'", // tighten after audit
|
|
116
|
+
"style-src 'self' 'unsafe-inline'",
|
|
117
|
+
"img-src 'self' data: https:",
|
|
118
|
+
].join("; "),
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## DO NOT
|
|
124
|
+
|
|
125
|
+
- Expose database errors or stack traces to the client
|
|
126
|
+
- Trust user-supplied IDs for authorization
|
|
127
|
+
- Use `*` for CORS `Access-Control-Allow-Origin` in production
|
|
128
|
+
- Accept file uploads without size and MIME type validation
|
|
129
|
+
- Log request bodies that may contain passwords or payment data
|
|
130
|
+
|
|
131
|
+
## See Also
|
|
132
|
+
|
|
133
|
+
- [`validation.md`](validation.md) — input validation patterns
|
|
134
|
+
- [`auth.md`](auth.md) — authentication and authorization
|
|
135
|
+
- [`../typescript/error-handling.md`](../typescript/error-handling.md) — structured error responses
|
|
136
|
+
- [`../architecture/api-design.md`](../architecture/api-design.md) — error shape conventions
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Authentication and Authorization
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Authentication confirms identity. Authorization confirms permission. Both are required.
|
|
6
|
+
- Never trust client-supplied user IDs or roles without server-side verification
|
|
7
|
+
- Always verify authorization on the server — never rely solely on client-side checks
|
|
8
|
+
- Use an established auth library — do not build custom auth from scratch
|
|
9
|
+
|
|
10
|
+
## Preferred Libraries
|
|
11
|
+
|
|
12
|
+
- **Auth.js** (Next.js) — sessions, OAuth, credentials, magic links
|
|
13
|
+
- **Clerk** — managed auth with pre-built UI components
|
|
14
|
+
- **Lucia** — lightweight, flexible, self-hosted
|
|
15
|
+
|
|
16
|
+
## Session Handling
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// Server-side — always verify the session server-side
|
|
20
|
+
import { auth } from "~/lib/auth";
|
|
21
|
+
|
|
22
|
+
export async function GET(req: Request) {
|
|
23
|
+
const session = await auth();
|
|
24
|
+
if (!session?.user) {
|
|
25
|
+
return new Response("Unauthorized", { status: 401 });
|
|
26
|
+
}
|
|
27
|
+
// use session.user.id — never use a userId from the request body
|
|
28
|
+
const invoices = await getInvoicesForUser(session.user.id);
|
|
29
|
+
return Response.json(invoices);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Authorization Checks
|
|
34
|
+
|
|
35
|
+
Always check permissions on the server, for every request:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
async function getInvoice(invoiceId: string, requestingUserId: string) {
|
|
39
|
+
const invoice = await db.invoice.findUnique({ where: { id: invoiceId } });
|
|
40
|
+
if (!invoice) throw new NotFoundError("Invoice", invoiceId);
|
|
41
|
+
|
|
42
|
+
// Authorization — verify the user owns this resource
|
|
43
|
+
if (invoice.userId !== requestingUserId) {
|
|
44
|
+
throw new ForbiddenError("You do not have access to this invoice");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return invoice;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Token Storage
|
|
52
|
+
|
|
53
|
+
| Token Type | Recommended Storage | Avoid |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Session cookies | `HttpOnly`, `Secure`, `SameSite=Lax` cookies | `localStorage` |
|
|
56
|
+
| JWT (short-lived) | `HttpOnly` cookie | `localStorage` |
|
|
57
|
+
| Refresh tokens | `HttpOnly` cookie | `localStorage` or `sessionStorage` |
|
|
58
|
+
|
|
59
|
+
`localStorage` is accessible to JavaScript and vulnerable to XSS. Always use `HttpOnly` cookies for auth tokens.
|
|
60
|
+
|
|
61
|
+
## Password Handling
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import bcrypt from "bcryptjs";
|
|
65
|
+
|
|
66
|
+
// Hashing
|
|
67
|
+
const hash = await bcrypt.hash(plaintext, 12); // min 12 rounds
|
|
68
|
+
|
|
69
|
+
// Verification — use timing-safe comparison
|
|
70
|
+
const isValid = await bcrypt.compare(plaintext, hash);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Never store plaintext passwords
|
|
74
|
+
- Use bcrypt, argon2, or scrypt — not MD5 or SHA-1
|
|
75
|
+
- Minimum 12 rounds for bcrypt
|
|
76
|
+
|
|
77
|
+
## CSRF Protection
|
|
78
|
+
|
|
79
|
+
- Use `SameSite=Lax` or `SameSite=Strict` on session cookies
|
|
80
|
+
- Use CSRF tokens for state-changing requests if cookies are `SameSite=None`
|
|
81
|
+
- Auth.js and Clerk handle CSRF automatically
|
|
82
|
+
|
|
83
|
+
## Rate Limiting Auth Endpoints
|
|
84
|
+
|
|
85
|
+
Apply rate limiting to:
|
|
86
|
+
- Login endpoints
|
|
87
|
+
- Password reset endpoints
|
|
88
|
+
- OTP/magic link endpoints
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { Ratelimit } from "@upstash/ratelimit";
|
|
92
|
+
|
|
93
|
+
const ratelimit = new Ratelimit({
|
|
94
|
+
limiter: Ratelimit.slidingWindow(5, "15 m"), // 5 attempts per 15 minutes
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const { success } = await ratelimit.limit(ip);
|
|
98
|
+
if (!success) return new Response("Too many attempts", { status: 429 });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## DO NOT
|
|
102
|
+
|
|
103
|
+
- Use the user's ID from request body for authorization decisions — always use the server session
|
|
104
|
+
- Return different HTTP status codes for "user not found" vs. "wrong password" (user enumeration)
|
|
105
|
+
- Log passwords or tokens
|
|
106
|
+
- Implement custom crypto or JWT libraries
|
|
107
|
+
|
|
108
|
+
## See Also
|
|
109
|
+
|
|
110
|
+
- [`secrets.md`](secrets.md) — secret storage and rotation
|
|
111
|
+
- [`api-security.md`](api-security.md) — rate limiting and CORS
|
|
112
|
+
- [`validation.md`](validation.md) — validating auth requests
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Secrets Management
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Never commit secrets to version control
|
|
6
|
+
- Never log secrets
|
|
7
|
+
- Never expose backend secrets to the client
|
|
8
|
+
- Rotate secrets that have been compromised or accidentally exposed
|
|
9
|
+
|
|
10
|
+
## Environment Variables
|
|
11
|
+
|
|
12
|
+
Store secrets in environment variables. Never hardcode them.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
# .env.local (gitignored — for local development only)
|
|
16
|
+
DATABASE_URL=postgres://user:password@localhost:5432/mydb
|
|
17
|
+
JWT_SECRET=your-secret-key
|
|
18
|
+
STRIPE_SECRET_KEY=sk_test_...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
# .env.example (committed — shows required vars without values)
|
|
23
|
+
DATABASE_URL=
|
|
24
|
+
JWT_SECRET=
|
|
25
|
+
STRIPE_SECRET_KEY=
|
|
26
|
+
SENDGRID_API_KEY=
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## `.gitignore`
|
|
30
|
+
|
|
31
|
+
Always include:
|
|
32
|
+
|
|
33
|
+
```gitignore
|
|
34
|
+
.env
|
|
35
|
+
.env.local
|
|
36
|
+
.env.*.local
|
|
37
|
+
*.pem
|
|
38
|
+
*.key
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Validate at Startup
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// lib/env.ts (server-only)
|
|
45
|
+
import { z } from "zod";
|
|
46
|
+
|
|
47
|
+
const envSchema = z.object({
|
|
48
|
+
DATABASE_URL: z.string().url(),
|
|
49
|
+
JWT_SECRET: z.string().min(32),
|
|
50
|
+
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
|
|
51
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const env = envSchema.parse(process.env);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Fail fast on startup if required secrets are missing or malformed.
|
|
58
|
+
|
|
59
|
+
## Client vs. Server Secrets
|
|
60
|
+
|
|
61
|
+
| Type | Where | Prefix (Vite/Next.js) | Example |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| Server-only | Server environment | No prefix | `DATABASE_URL`, `JWT_SECRET` |
|
|
64
|
+
| Client-safe | Client environment | `VITE_` / `NEXT_PUBLIC_` | `VITE_API_BASE_URL` |
|
|
65
|
+
|
|
66
|
+
Never put a secret key in a `VITE_` or `NEXT_PUBLIC_` variable — it will be embedded in the client bundle.
|
|
67
|
+
|
|
68
|
+
## CI/CD Secrets
|
|
69
|
+
|
|
70
|
+
- Use the CI provider's secret store (GitHub Actions secrets, Vercel env vars, etc.)
|
|
71
|
+
- Use separate secrets for each environment (dev, staging, production)
|
|
72
|
+
- Limit secret access to only the jobs that need them
|
|
73
|
+
|
|
74
|
+
## Secret Scanning
|
|
75
|
+
|
|
76
|
+
- Enable GitHub's secret scanning on all repositories
|
|
77
|
+
- Add `gitleaks` or `truffleHog` to pre-commit hooks for local detection
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Install gitleaks pre-commit hook
|
|
81
|
+
brew install gitleaks
|
|
82
|
+
gitleaks protect --staged
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## If a Secret Is Exposed
|
|
86
|
+
|
|
87
|
+
1. Rotate it immediately — assume it has been compromised
|
|
88
|
+
2. Revoke the old secret in the provider's dashboard
|
|
89
|
+
3. Audit access logs for unauthorized use
|
|
90
|
+
4. Update all environments with the new secret
|
|
91
|
+
5. Review git history and remove the commit from public branches if possible
|
|
92
|
+
|
|
93
|
+
## DO NOT
|
|
94
|
+
|
|
95
|
+
- Hardcode API keys, passwords, or tokens in source code
|
|
96
|
+
- Log secrets (even in debug mode)
|
|
97
|
+
- Pass secrets as command-line arguments (visible in process list)
|
|
98
|
+
- Store secrets in `localStorage` or cookies without encryption
|
|
99
|
+
- Use the same secret across environments
|
|
100
|
+
|
|
101
|
+
## See Also
|
|
102
|
+
|
|
103
|
+
- [`auth.md`](auth.md) — token storage and rotation
|
|
104
|
+
- [`api-security.md`](api-security.md) — error responses that don't leak details
|
|
105
|
+
- [`../tooling/observability.md`](../tooling/observability.md) — redacting before logging
|