@silverassist/agents-toolkit 2.0.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 +135 -0
- package/README.md +388 -0
- package/bin/cli.js +940 -0
- package/package.json +52 -0
- package/src/index.js +58 -0
- package/templates/agents/AGENTS.codex.md +195 -0
- package/templates/agents/AGENTS.md +195 -0
- package/templates/agents/CLAUDE.md +213 -0
- package/templates/agents/copilot-instructions.md +83 -0
- package/templates/shared/instructions/css-styling.instructions.md +142 -0
- package/templates/shared/instructions/documentation-language.instructions.md +216 -0
- package/templates/shared/instructions/github-workflow.instructions.md +245 -0
- package/templates/shared/instructions/php-standards.instructions.md +293 -0
- package/templates/shared/instructions/react-components.instructions.md +196 -0
- package/templates/shared/instructions/server-actions.instructions.md +429 -0
- package/templates/shared/instructions/testing-standards.instructions.md +264 -0
- package/templates/shared/instructions/tests.instructions.md +103 -0
- package/templates/shared/instructions/typescript.instructions.md +106 -0
- package/templates/shared/instructions/wordpress-plugin-architecture.instructions.md +238 -0
- package/templates/shared/prompts/README.md +129 -0
- package/templates/shared/prompts/_partials/README.md +57 -0
- package/templates/shared/prompts/_partials/documentation.md +203 -0
- package/templates/shared/prompts/_partials/git-operations.md +171 -0
- package/templates/shared/prompts/_partials/github-integration.md +100 -0
- package/templates/shared/prompts/_partials/jira-integration.md +155 -0
- package/templates/shared/prompts/_partials/pr-template.md +216 -0
- package/templates/shared/prompts/_partials/validations.md +87 -0
- package/templates/shared/prompts/add-tests.prompt.md +151 -0
- package/templates/shared/prompts/analyze-github-issue.prompt.md +73 -0
- package/templates/shared/prompts/analyze-ticket.prompt.md +63 -0
- package/templates/shared/prompts/create-plan.prompt.md +118 -0
- package/templates/shared/prompts/create-pr.prompt.md +139 -0
- package/templates/shared/prompts/finalize-pr.prompt.md +150 -0
- package/templates/shared/prompts/fix-issues.prompt.md +92 -0
- package/templates/shared/prompts/new-wp-component.prompt.md +51 -0
- package/templates/shared/prompts/new-wp-plugin.prompt.md +46 -0
- package/templates/shared/prompts/prepare-pr.prompt.md +123 -0
- package/templates/shared/prompts/prepare-release.prompt.md +74 -0
- package/templates/shared/prompts/quality-check.prompt.md +45 -0
- package/templates/shared/prompts/review-code.prompt.md +81 -0
- package/templates/shared/prompts/work-github-issue.prompt.md +96 -0
- package/templates/shared/prompts/work-ticket.prompt.md +98 -0
- package/templates/shared/skills/README.md +53 -0
- package/templates/shared/skills/component-architecture/SKILL.md +321 -0
- package/templates/shared/skills/create-component/SKILL.md +714 -0
- package/templates/shared/skills/domain-driven-design/SKILL.md +277 -0
- package/templates/shared/skills/plugin-creation/SKILL.md +1094 -0
- package/templates/shared/skills/quality-checks/SKILL.md +493 -0
- package/templates/shared/skills/release-management/SKILL.md +649 -0
- package/templates/shared/skills/testing/SKILL.md +682 -0
- package/templates/shared/skills/testing-patterns/SKILL.md +243 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/components/**/*.tsx"
|
|
3
|
+
---
|
|
4
|
+
# React Component Standards
|
|
5
|
+
|
|
6
|
+
## Component Structure
|
|
7
|
+
|
|
8
|
+
**✅ ALWAYS use folder structure** with `index.tsx`:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
components/
|
|
12
|
+
└── domain/ # Domain folder (auth, checkout, etc.)
|
|
13
|
+
├── index.ts # Barrel export for domain
|
|
14
|
+
└── component-name/ # kebab-case folder
|
|
15
|
+
├── index.tsx # Main component (export default)
|
|
16
|
+
├── types.ts # Component-specific types (if needed)
|
|
17
|
+
└── __tests__/ # Tests folder
|
|
18
|
+
└── component-name.test.tsx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Export Pattern
|
|
22
|
+
|
|
23
|
+
### Components: Default Export
|
|
24
|
+
|
|
25
|
+
Per Next.js recommendation, components use `export default` for better tree-shaking:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// ✅ CORRECT: Default export for components
|
|
29
|
+
interface ButtonProps {
|
|
30
|
+
onClick: () => void;
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function Button({ onClick, children }: ButtonProps) {
|
|
35
|
+
return <button onClick={onClick}>{children}</button>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ❌ INCORRECT: Named export for components (worse tree-shaking)
|
|
39
|
+
export function Button({ onClick, children }: ButtonProps) {
|
|
40
|
+
return <button onClick={onClick}>{children}</button>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ❌ INCORRECT: Arrow function (loses name in stack traces)
|
|
44
|
+
export default const Button = ({ onClick, children }: ButtonProps) => {
|
|
45
|
+
return <button onClick={onClick}>{children}</button>;
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Barrel Exports for Domains
|
|
50
|
+
|
|
51
|
+
Re-export components from domain `index.ts`:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// src/components/auth/index.ts
|
|
55
|
+
export { default as LoginForm } from "./login-form";
|
|
56
|
+
export { default as RegisterForm } from "./register-form";
|
|
57
|
+
|
|
58
|
+
// Usage - clean imports
|
|
59
|
+
import { LoginForm, RegisterForm } from "@/components/auth";
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Non-Components: Named Exports
|
|
63
|
+
|
|
64
|
+
Helpers, types, hooks use named exports:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// types.ts
|
|
68
|
+
export interface User { }
|
|
69
|
+
export type Status = "active" | "inactive";
|
|
70
|
+
|
|
71
|
+
// helpers.ts
|
|
72
|
+
export function formatUserName(user: User): string { }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Server vs Client Components
|
|
76
|
+
|
|
77
|
+
### Server Component (Default)
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// src/components/dashboard/stats-card/index.tsx
|
|
81
|
+
import { getStats } from "@/data/stats";
|
|
82
|
+
|
|
83
|
+
interface StatsCardProps {
|
|
84
|
+
title: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default async function StatsCard({ title }: StatsCardProps) {
|
|
88
|
+
const stats = await getStats(); // Direct data fetching
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<h3>{title}</h3>
|
|
93
|
+
<p>{stats.value}</p>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Client Component (Interactive)
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// src/components/checkout/add-to-cart/index.tsx
|
|
103
|
+
"use client";
|
|
104
|
+
|
|
105
|
+
import { useState } from "react";
|
|
106
|
+
|
|
107
|
+
interface AddToCartProps {
|
|
108
|
+
productId: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default function AddToCart({ productId }: AddToCartProps) {
|
|
112
|
+
const [quantity, setQuantity] = useState(1);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<button onClick={() => setQuantity(q => q + 1)}>
|
|
116
|
+
Add ({quantity})
|
|
117
|
+
</button>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Props Interface
|
|
123
|
+
|
|
124
|
+
**✅ ALWAYS define props interface inside the component file**:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// ✅ CORRECT: Interface defined in component file
|
|
128
|
+
interface UserCardProps {
|
|
129
|
+
user: User;
|
|
130
|
+
onSelect?: (user: User) => void;
|
|
131
|
+
className?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default function UserCard({ user, onSelect, className }: UserCardProps) {
|
|
135
|
+
return (
|
|
136
|
+
<div className={className}>
|
|
137
|
+
<h2>{user.name}</h2>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Hook Placement (CRITICAL)
|
|
144
|
+
|
|
145
|
+
**✅ ALL hooks MUST come BEFORE any conditional returns**:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// ✅ CORRECT: Hooks first, then early returns
|
|
149
|
+
export default function UserList({ users }: UserListProps) {
|
|
150
|
+
const [filter, setFilter] = useState("");
|
|
151
|
+
const filteredUsers = useMemo(() =>
|
|
152
|
+
users.filter(u => u.name.includes(filter)),
|
|
153
|
+
[users, filter]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Early returns AFTER all hooks
|
|
157
|
+
if (!users?.length) {
|
|
158
|
+
return <p>No users found</p>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return <ul>{/* ... */}</ul>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ❌ INCORRECT: Hook after conditional
|
|
165
|
+
export default function UserList({ users }: UserListProps) {
|
|
166
|
+
if (!users?.length) return null; // ❌ Early return before hooks
|
|
167
|
+
|
|
168
|
+
const [filter, setFilter] = useState(""); // ❌ Error!
|
|
169
|
+
return <ul>{/* ... */}</ul>;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Event Handlers
|
|
174
|
+
|
|
175
|
+
**✅ Use `handle` prefix** for event handlers:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import type { ChangeEvent, SyntheticEvent } from "react";
|
|
179
|
+
|
|
180
|
+
export default function Form() {
|
|
181
|
+
const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
// ...
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
187
|
+
// ...
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<form onSubmit={handleSubmit}>
|
|
192
|
+
<input onChange={handleInputChange} />
|
|
193
|
+
</form>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/actions/**/*.ts"
|
|
3
|
+
---
|
|
4
|
+
# Server Actions Standards
|
|
5
|
+
|
|
6
|
+
> **Security First**: Server Actions create public HTTP endpoints. Treat them with the same
|
|
7
|
+
> security assumptions as any public API endpoint.
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
Organize actions by domain (consistent with DDD principles):
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
actions/
|
|
15
|
+
├── auth/
|
|
16
|
+
│ ├── login.ts
|
|
17
|
+
│ └── register.ts
|
|
18
|
+
├── user/
|
|
19
|
+
│ └── update-profile.ts
|
|
20
|
+
└── contact/
|
|
21
|
+
└── submit-form.ts
|
|
22
|
+
data/ # Data Access Layer (DAL)
|
|
23
|
+
├── user-dto.ts
|
|
24
|
+
├── auth.ts
|
|
25
|
+
└── db.ts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For smaller projects, flat structure is acceptable:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
actions/
|
|
32
|
+
├── auth-actions.ts
|
|
33
|
+
├── user-actions.ts
|
|
34
|
+
└── contact-actions.ts
|
|
35
|
+
data/
|
|
36
|
+
├── dal.ts # Centralized Data Access Layer
|
|
37
|
+
└── dto.ts # Data Transfer Objects
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Security Architecture
|
|
41
|
+
|
|
42
|
+
### Data Access Layer (DAL)
|
|
43
|
+
|
|
44
|
+
**CRITICAL**: Create a dedicated Data Access Layer for all data operations. Never access
|
|
45
|
+
databases or sensitive data directly in Server Actions.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// data/dal.ts
|
|
49
|
+
import "server-only";
|
|
50
|
+
import { cache } from "react";
|
|
51
|
+
import { cookies } from "next/headers";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Cached user retrieval - safe to call multiple times
|
|
55
|
+
* Uses React cache to deduplicate requests within a render
|
|
56
|
+
*/
|
|
57
|
+
export const getCurrentUser = cache(async () => {
|
|
58
|
+
const cookieStore = await cookies();
|
|
59
|
+
const token = cookieStore.get("AUTH_TOKEN");
|
|
60
|
+
const decodedToken = await decryptAndValidate(token);
|
|
61
|
+
|
|
62
|
+
// Return minimal, safe user object - NOT the full database record
|
|
63
|
+
return new User(decodedToken.id);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get user profile with authorization checks
|
|
68
|
+
* Returns only safe, filtered data (DTO pattern)
|
|
69
|
+
*/
|
|
70
|
+
export async function getUserProfileDTO(userId: string) {
|
|
71
|
+
const currentUser = await getCurrentUser();
|
|
72
|
+
const userData = await db.user.findUnique({ where: { id: userId } });
|
|
73
|
+
|
|
74
|
+
// Return only authorized fields
|
|
75
|
+
return {
|
|
76
|
+
username: canSeeUsername(currentUser) ? userData.username : null,
|
|
77
|
+
email: canSeeEmail(currentUser, userData) ? userData.email : null,
|
|
78
|
+
// Never expose: password, tokens, internal IDs, etc.
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Server-Only Marker
|
|
84
|
+
|
|
85
|
+
**CRITICAL**: Always mark DAL files with `server-only` to prevent client-side imports:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// data/dal.ts
|
|
89
|
+
import "server-only";
|
|
90
|
+
|
|
91
|
+
// This file will cause a build error if imported in client code
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Install the package: `npm install server-only`
|
|
95
|
+
|
|
96
|
+
## Action Pattern
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
"use server";
|
|
100
|
+
|
|
101
|
+
import { revalidatePath } from "next/cache";
|
|
102
|
+
import { getCurrentUser } from "@/data/dal";
|
|
103
|
+
import { createUserInDB } from "@/data/user-dto";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* State returned from server actions
|
|
107
|
+
*/
|
|
108
|
+
interface ActionState {
|
|
109
|
+
success: boolean;
|
|
110
|
+
message: string;
|
|
111
|
+
data?: unknown;
|
|
112
|
+
errors?: Record<string, string>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a new user
|
|
117
|
+
* @param prevState - Previous form state
|
|
118
|
+
* @param formData - Form data from submission
|
|
119
|
+
* @returns Action result state
|
|
120
|
+
*/
|
|
121
|
+
export async function createUser(
|
|
122
|
+
prevState: ActionState,
|
|
123
|
+
formData: FormData
|
|
124
|
+
): Promise<ActionState> {
|
|
125
|
+
try {
|
|
126
|
+
// 1. ALWAYS verify authentication first
|
|
127
|
+
const user = await getCurrentUser();
|
|
128
|
+
if (!user) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
message: "You must be signed in to perform this action",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 2. ALWAYS verify authorization
|
|
136
|
+
if (!user.canCreateUsers) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
message: "You do not have permission to create users",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 3. ALWAYS validate ALL inputs (never trust client data)
|
|
144
|
+
const name = formData.get("name") as string;
|
|
145
|
+
const email = formData.get("email") as string;
|
|
146
|
+
|
|
147
|
+
if (!name || !email) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
message: "Missing required fields",
|
|
151
|
+
errors: {
|
|
152
|
+
name: !name ? "Name is required" : "",
|
|
153
|
+
email: !email ? "Email is required" : "",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 4. Use DAL for data operations (never direct DB access)
|
|
159
|
+
const result = await createUserInDB({ name, email });
|
|
160
|
+
|
|
161
|
+
// 5. Revalidate cache after mutations
|
|
162
|
+
revalidatePath("/users");
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
success: true,
|
|
166
|
+
message: "User created successfully",
|
|
167
|
+
data: result, // Return only safe DTO, not raw DB result
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Security Rules (CRITICAL)
|
|
179
|
+
|
|
180
|
+
### 1. Always use "use server" directive
|
|
181
|
+
```typescript
|
|
182
|
+
"use server";
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 2. ALWAYS authenticate and authorize
|
|
186
|
+
```typescript
|
|
187
|
+
// ❌ INCORRECT: No auth check
|
|
188
|
+
export async function deleteUser(userId: string) {
|
|
189
|
+
await db.user.delete({ where: { id: userId } });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ✅ CORRECT: Auth + authz before any operation
|
|
193
|
+
export async function deleteUser(userId: string) {
|
|
194
|
+
const currentUser = await getCurrentUser();
|
|
195
|
+
if (!currentUser) {
|
|
196
|
+
throw new Error("You must be signed in");
|
|
197
|
+
}
|
|
198
|
+
if (!currentUser.isAdmin && currentUser.id !== userId) {
|
|
199
|
+
throw new Error("Not authorized");
|
|
200
|
+
}
|
|
201
|
+
await deleteUserFromDB(userId); // Use DAL
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 3. NEVER trust client input
|
|
206
|
+
```typescript
|
|
207
|
+
// ❌ INCORRECT: Trusting client data
|
|
208
|
+
export async function updateRole(formData: FormData) {
|
|
209
|
+
const isAdmin = formData.get("isAdmin") === "true";
|
|
210
|
+
await db.user.update({ data: { isAdmin } }); // Vulnerable!
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ✅ CORRECT: Re-verify permissions server-side
|
|
214
|
+
export async function updateRole(formData: FormData) {
|
|
215
|
+
const currentUser = await getCurrentUser();
|
|
216
|
+
if (!currentUser?.isSuperAdmin) {
|
|
217
|
+
return { success: false, message: "Not authorized" };
|
|
218
|
+
}
|
|
219
|
+
// Now safe to proceed...
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 4. Return state, don't redirect
|
|
224
|
+
```typescript
|
|
225
|
+
// ❌ INCORRECT: Don't call redirect in actions
|
|
226
|
+
redirect("/success");
|
|
227
|
+
|
|
228
|
+
// ✅ CORRECT: Return state and handle redirect in client
|
|
229
|
+
return { success: true, redirectTo: "/success" };
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 5. Validate all inputs with schema validation
|
|
233
|
+
```typescript
|
|
234
|
+
import { z } from "zod";
|
|
235
|
+
|
|
236
|
+
const CreateUserSchema = z.object({
|
|
237
|
+
name: z.string().min(2).max(100),
|
|
238
|
+
email: z.string().email(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
export async function createUser(prevState: ActionState, formData: FormData) {
|
|
242
|
+
const validatedFields = CreateUserSchema.safeParse({
|
|
243
|
+
name: formData.get("name"),
|
|
244
|
+
email: formData.get("email"),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!validatedFields.success) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
message: "Invalid fields",
|
|
251
|
+
errors: validatedFields.error.flatten().fieldErrors,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Use validatedFields.data (sanitized)
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 6. Handle errors gracefully (don't leak internals)
|
|
260
|
+
```typescript
|
|
261
|
+
try {
|
|
262
|
+
// action logic
|
|
263
|
+
} catch (error) {
|
|
264
|
+
// Log full error server-side
|
|
265
|
+
console.error("Action error:", error);
|
|
266
|
+
|
|
267
|
+
// Return generic message to client (don't expose internals)
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
message: "An error occurred. Please try again.",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 7. Revalidate after mutations
|
|
276
|
+
```typescript
|
|
277
|
+
revalidatePath("/affected-path");
|
|
278
|
+
// or
|
|
279
|
+
revalidateTag("users");
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 8. Avoid mutations during rendering
|
|
283
|
+
```typescript
|
|
284
|
+
// ❌ INCORRECT: Side effect during render
|
|
285
|
+
export default async function Page({ searchParams }) {
|
|
286
|
+
if (searchParams.get("logout")) {
|
|
287
|
+
cookies().delete("AUTH_TOKEN"); // Bad!
|
|
288
|
+
}
|
|
289
|
+
return <div>...</div>;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ✅ CORRECT: Use Server Action
|
|
293
|
+
export default function Page() {
|
|
294
|
+
return (
|
|
295
|
+
<form action={logout}>
|
|
296
|
+
<button type="submit">Logout</button>
|
|
297
|
+
</form>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Closures Security
|
|
303
|
+
|
|
304
|
+
When defining Server Actions inside components, be aware that closed-over variables
|
|
305
|
+
are sent to the client (encrypted). Never close over sensitive data:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// ⚠️ Sensitive data in closure (encrypted but still transmitted)
|
|
309
|
+
export default async function Page() {
|
|
310
|
+
const secretKey = process.env.SECRET_KEY; // Don't do this!
|
|
311
|
+
|
|
312
|
+
async function submit() {
|
|
313
|
+
"use server";
|
|
314
|
+
// secretKey is captured and sent to client (encrypted)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return <form action={submit}>...</form>;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ✅ CORRECT: Read secrets inside the action
|
|
321
|
+
export default function Page() {
|
|
322
|
+
async function submit() {
|
|
323
|
+
"use server";
|
|
324
|
+
const secretKey = process.env.SECRET_KEY; // Safe - read server-side
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return <form action={submit}>...</form>;
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Using with useActionState
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
"use client";
|
|
335
|
+
|
|
336
|
+
import { useActionState } from "react";
|
|
337
|
+
import { createUser } from "@/actions/user-actions";
|
|
338
|
+
|
|
339
|
+
const initialState = { success: false, message: "" };
|
|
340
|
+
|
|
341
|
+
export function UserForm() {
|
|
342
|
+
const [state, formAction, isPending] = useActionState(
|
|
343
|
+
createUser,
|
|
344
|
+
initialState
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<form action={formAction}>
|
|
349
|
+
<input name="name" required />
|
|
350
|
+
<input name="email" type="email" required />
|
|
351
|
+
<button type="submit" disabled={isPending}>
|
|
352
|
+
{isPending ? "Saving..." : "Save"}
|
|
353
|
+
</button>
|
|
354
|
+
{state.message && (
|
|
355
|
+
<p role="alert" className={state.success ? "text-green-600" : "text-red-600"}>
|
|
356
|
+
{state.message}
|
|
357
|
+
</p>
|
|
358
|
+
)}
|
|
359
|
+
</form>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Troubleshooting
|
|
365
|
+
|
|
366
|
+
### Error: "Failed to find Server Action"
|
|
367
|
+
|
|
368
|
+
**Cause**: Next.js creates encrypted, non-deterministic IDs for Server Actions. These IDs
|
|
369
|
+
are recalculated between builds. In multi-server environments (self-hosted), each instance
|
|
370
|
+
may have different encryption keys.
|
|
371
|
+
|
|
372
|
+
**Solutions**:
|
|
373
|
+
|
|
374
|
+
1. **Self-hosting with multiple servers**: Configure a consistent encryption key:
|
|
375
|
+
```bash
|
|
376
|
+
# .env
|
|
377
|
+
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="your-aes-gcm-encrypted-key"
|
|
378
|
+
```
|
|
379
|
+
The key must be AES-GCM encrypted and consistent across all servers.
|
|
380
|
+
|
|
381
|
+
2. **Vercel**: Use [Skew Protection](https://vercel.com/docs/deployments/skew-protection) to
|
|
382
|
+
keep assets and functions from previous versions available after a deploy.
|
|
383
|
+
|
|
384
|
+
3. **Local development**: If it occurs in development, restart the dev server (`npm run dev`).
|
|
385
|
+
|
|
386
|
+
4. **Corrupted cache**: Clear the Next.js cache:
|
|
387
|
+
```bash
|
|
388
|
+
rm -rf .next
|
|
389
|
+
npm run build
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Error: Action doesn't execute / Does nothing
|
|
393
|
+
|
|
394
|
+
Verify that:
|
|
395
|
+
- The function has `"use server"` at the top of the file OR inside the function
|
|
396
|
+
- The form uses `action={formAction}` (not `onSubmit`)
|
|
397
|
+
- There are no JavaScript errors in the browser console
|
|
398
|
+
|
|
399
|
+
### Error: "Functions cannot be passed directly to Client Components"
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// ❌ INCORRECT: Passing function directly
|
|
403
|
+
<ClientComponent onSubmit={serverAction} />
|
|
404
|
+
|
|
405
|
+
// ✅ CORRECT: Use in form action or useActionState
|
|
406
|
+
<form action={serverAction}>...</form>
|
|
407
|
+
|
|
408
|
+
// ✅ CORRECT: Wrap in client component
|
|
409
|
+
"use client";
|
|
410
|
+
export function FormWrapper({ action }) {
|
|
411
|
+
const [state, formAction] = useActionState(action, initialState);
|
|
412
|
+
return <form action={formAction}>...</form>;
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Security Checklist
|
|
417
|
+
|
|
418
|
+
Before deploying any Server Action, verify:
|
|
419
|
+
|
|
420
|
+
- [ ] `"use server"` directive is present
|
|
421
|
+
- [ ] User is authenticated (if required)
|
|
422
|
+
- [ ] User is authorized for the specific action
|
|
423
|
+
- [ ] All inputs are validated (use Zod or similar)
|
|
424
|
+
- [ ] Data access goes through DAL (not direct DB queries)
|
|
425
|
+
- [ ] No sensitive data in closures
|
|
426
|
+
- [ ] Error messages don't leak internal details
|
|
427
|
+
- [ ] Cache is revalidated after mutations
|
|
428
|
+
- [ ] DAL files are marked with `server-only`
|
|
429
|
+
- [ ] `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` configured (if multi-server self-hosting)
|