@folpe/loom 0.1.0 → 0.3.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 +82 -16
- package/data/agents/backend/AGENT.md +12 -0
- package/data/agents/database/AGENT.md +3 -0
- package/data/agents/frontend/AGENT.md +10 -0
- package/data/agents/marketing/AGENT.md +3 -0
- package/data/agents/orchestrator/AGENT.md +1 -15
- package/data/agents/security/AGENT.md +3 -0
- package/data/agents/tests/AGENT.md +2 -0
- package/data/agents/ux-ui/AGENT.md +5 -0
- package/data/presets/api-backend.yaml +37 -0
- package/data/presets/chrome-extension.yaml +36 -0
- package/data/presets/cli-tool.yaml +31 -0
- package/data/presets/e-commerce.yaml +49 -0
- package/data/presets/expo-mobile.yaml +41 -0
- package/data/presets/fullstack-auth.yaml +45 -0
- package/data/presets/landing-page.yaml +38 -0
- package/data/presets/mvp-lean.yaml +35 -0
- package/data/presets/saas-default.yaml +7 -11
- package/data/presets/saas-full.yaml +71 -0
- package/data/skills/api-design/SKILL.md +149 -0
- package/data/skills/auth-rbac/SKILL.md +179 -0
- package/data/skills/better-auth-patterns/SKILL.md +212 -0
- package/data/skills/chrome-extension-patterns/SKILL.md +105 -0
- package/data/skills/cli-development/SKILL.md +147 -0
- package/data/skills/drizzle-patterns/SKILL.md +166 -0
- package/data/skills/env-validation/SKILL.md +142 -0
- package/data/skills/form-validation/SKILL.md +169 -0
- package/data/skills/hero-copywriting/SKILL.md +12 -4
- package/data/skills/i18n-patterns/SKILL.md +176 -0
- package/data/skills/layered-architecture/SKILL.md +131 -0
- package/data/skills/nextjs-conventions/SKILL.md +46 -7
- package/data/skills/react-native-patterns/SKILL.md +87 -0
- package/data/skills/react-query-patterns/SKILL.md +193 -0
- package/data/skills/resend-email/SKILL.md +181 -0
- package/data/skills/seo-optimization/SKILL.md +106 -0
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +126 -0
- package/data/skills/stripe-integration/SKILL.md +96 -0
- package/data/skills/supabase-patterns/SKILL.md +110 -0
- package/data/skills/table-pagination/SKILL.md +224 -0
- package/data/skills/tailwind-patterns/SKILL.md +12 -2
- package/data/skills/testing-patterns/SKILL.md +203 -0
- package/data/skills/ui-ux-guidelines/SKILL.md +179 -0
- package/dist/index.js +254 -100
- package/package.json +2 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-design
|
|
3
|
+
description: "REST API design with validation, error handling, auth wrappers, and facades. Use when building API routes, server actions, implementing auth middleware, or designing backend services."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Design Principles
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Validate all incoming data** with Zod schemas at the API boundary.
|
|
11
|
+
- **Never expose stack traces** or internal details in production error responses.
|
|
12
|
+
- **Always use facades** between presentation and services.
|
|
13
|
+
- **Auth wrappers on every protected endpoint** — `withAuth()`, `withAdmin()`.
|
|
14
|
+
- **Consistent response shapes** — `{ data }` for success, `{ error, code }` for errors.
|
|
15
|
+
- **Parameterized queries only** — never string concatenation for SQL.
|
|
16
|
+
|
|
17
|
+
## Route Structure
|
|
18
|
+
|
|
19
|
+
- Use resource-based URLs: `/api/users`, `/api/posts/:id/comments`.
|
|
20
|
+
- Use HTTP methods semantically:
|
|
21
|
+
- `GET` — read (no side effects)
|
|
22
|
+
- `POST` — create
|
|
23
|
+
- `PUT/PATCH` — update (PUT replaces, PATCH partial update)
|
|
24
|
+
- `DELETE` — remove
|
|
25
|
+
- Nest sub-resources only one level deep: `/api/posts/:id/comments` — not deeper.
|
|
26
|
+
- Use plural nouns for collections: `/api/users` (not `/api/user`).
|
|
27
|
+
|
|
28
|
+
## Request Validation
|
|
29
|
+
|
|
30
|
+
- Validate **all** incoming data with Zod schemas at the API boundary:
|
|
31
|
+
```ts
|
|
32
|
+
const CreatePostSchema = z.object({
|
|
33
|
+
title: z.string().min(1).max(200),
|
|
34
|
+
content: z.string().min(1),
|
|
35
|
+
published: z.boolean().default(false),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const body = CreatePostSchema.parse(await request.json())
|
|
39
|
+
```
|
|
40
|
+
- Return 400 with structured validation errors:
|
|
41
|
+
```json
|
|
42
|
+
{ "error": "Validation failed", "details": [{ "field": "title", "message": "Required" }] }
|
|
43
|
+
```
|
|
44
|
+
- Validate path params, query params, and headers — not just body.
|
|
45
|
+
|
|
46
|
+
## Response Format
|
|
47
|
+
|
|
48
|
+
- Use consistent response shapes across all endpoints:
|
|
49
|
+
```ts
|
|
50
|
+
// Success
|
|
51
|
+
{ "data": { ... } }
|
|
52
|
+
{ "data": [...], "pagination": { "total": 100, "page": 1, "pageSize": 20 } }
|
|
53
|
+
|
|
54
|
+
// Error
|
|
55
|
+
{ "error": "Not found", "code": "RESOURCE_NOT_FOUND" }
|
|
56
|
+
```
|
|
57
|
+
- Always return appropriate HTTP status codes:
|
|
58
|
+
- `200` — success
|
|
59
|
+
- `201` — created
|
|
60
|
+
- `204` — no content (successful delete)
|
|
61
|
+
- `400` — bad request (validation error)
|
|
62
|
+
- `401` — unauthorized (no/invalid auth)
|
|
63
|
+
- `403` — forbidden (insufficient permissions)
|
|
64
|
+
- `404` — not found
|
|
65
|
+
- `409` — conflict (duplicate resource)
|
|
66
|
+
- `429` — too many requests (rate limit)
|
|
67
|
+
- `500` — internal server error
|
|
68
|
+
|
|
69
|
+
## Error Handling
|
|
70
|
+
|
|
71
|
+
- Catch errors at the route handler level with a consistent pattern:
|
|
72
|
+
```ts
|
|
73
|
+
try {
|
|
74
|
+
// ... handler logic
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof z.ZodError) {
|
|
77
|
+
return NextResponse.json({ error: 'Validation failed', details: error.errors }, { status: 400 })
|
|
78
|
+
}
|
|
79
|
+
console.error('Unhandled error:', error)
|
|
80
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
- Never expose stack traces or internal details in production error responses.
|
|
84
|
+
- Log errors server-side with request context (method, path, user ID).
|
|
85
|
+
|
|
86
|
+
## Authentication & Authorization
|
|
87
|
+
|
|
88
|
+
- Validate auth on every protected endpoint — middleware + route-level checks.
|
|
89
|
+
- Extract user from session/token, never from request body.
|
|
90
|
+
- Check permissions at the resource level: "Can this user access THIS specific post?"
|
|
91
|
+
- Return `401` for missing/invalid auth, `403` for insufficient permissions.
|
|
92
|
+
|
|
93
|
+
### Auth Wrappers
|
|
94
|
+
|
|
95
|
+
Use auth wrapper functions for consistent protection:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Require any authenticated user
|
|
99
|
+
export async function withAuth() {
|
|
100
|
+
const user = await getCurrentUser();
|
|
101
|
+
if (!user) throw new ApiError(401, "Unauthorized");
|
|
102
|
+
return user;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Require authenticated user with valid token
|
|
106
|
+
export async function withAuthToken(request: Request) {
|
|
107
|
+
const token = request.headers.get("authorization")?.replace("Bearer ", "");
|
|
108
|
+
if (!token) throw new ApiError(401, "Missing token");
|
|
109
|
+
return verifyToken(token);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Dynamic auth — optional session, different behavior for authed/unauthed
|
|
113
|
+
export async function withDynamicAuth() {
|
|
114
|
+
const user = await getCurrentUser();
|
|
115
|
+
return { user, isAuthenticated: !!user };
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Facades
|
|
120
|
+
|
|
121
|
+
- **Always use facades** between presentation and services.
|
|
122
|
+
- One facade per domain entity in `src/facades/`.
|
|
123
|
+
- Facades handle auth context extraction and coordinate service calls.
|
|
124
|
+
- Routes/pages call facades — never services directly.
|
|
125
|
+
|
|
126
|
+
## Rate Limiting
|
|
127
|
+
|
|
128
|
+
- Implement rate limiting on public endpoints and auth endpoints.
|
|
129
|
+
- Use token bucket or sliding window algorithm.
|
|
130
|
+
- Return `429` with `Retry-After` header when rate limited.
|
|
131
|
+
- Rate limit by IP for public endpoints, by user ID for authenticated endpoints.
|
|
132
|
+
|
|
133
|
+
## Pagination
|
|
134
|
+
|
|
135
|
+
- Use cursor-based pagination for large datasets:
|
|
136
|
+
```
|
|
137
|
+
GET /api/posts?cursor=abc123&limit=20
|
|
138
|
+
```
|
|
139
|
+
- Return pagination metadata: `{ "data": [...], "nextCursor": "def456", "hasMore": true }`
|
|
140
|
+
- Default limit to 20, max to 100.
|
|
141
|
+
- For simple cases, offset-based is acceptable: `?page=1&pageSize=20`.
|
|
142
|
+
|
|
143
|
+
## Security
|
|
144
|
+
|
|
145
|
+
- Validate Content-Type header on POST/PUT/PATCH requests.
|
|
146
|
+
- Set CORS headers explicitly — never use `*` in production.
|
|
147
|
+
- Sanitize user input before database queries.
|
|
148
|
+
- Use parameterized queries — never string concatenation for SQL.
|
|
149
|
+
- Set security headers: `X-Content-Type-Options`, `X-Frame-Options`, `Strict-Transport-Security`.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-rbac
|
|
3
|
+
description: "CASL-based authorization with role hierarchies, organization roles, and safe route protection. Use when implementing access control, role-based permissions, or protecting routes and API endpoints."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auth & RBAC Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Authorization in services, not routes** — use CASL in the service layer.
|
|
11
|
+
- **Route groups for access control** — `(public)`, `(auth)`, `(app)`, `admin`.
|
|
12
|
+
- **Middleware for redirects** — protect routes at the edge.
|
|
13
|
+
- **Auth wrappers for pages** — `withAuth()`, `withAdmin()` in server components.
|
|
14
|
+
- **Never trust client-side role checks** — always verify server-side.
|
|
15
|
+
- **Principle of least privilege** — grant minimum required permissions.
|
|
16
|
+
|
|
17
|
+
## Role System
|
|
18
|
+
|
|
19
|
+
### Application Roles
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/lib/roles.ts
|
|
23
|
+
export const APP_ROLES = ["USER", "ADMIN", "SUPER_ADMIN"] as const;
|
|
24
|
+
export type AppRole = (typeof APP_ROLES)[number];
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Organization Roles
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
export const ORG_ROLES = ["MEMBER", "MANAGER", "OWNER"] as const;
|
|
31
|
+
export type OrgRole = (typeof ORG_ROLES)[number];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Every user has an **app role** (global) and an **org role** (per organization).
|
|
35
|
+
- Role checks always consider both: app role for platform features, org role for org resources.
|
|
36
|
+
|
|
37
|
+
## CASL Authorization
|
|
38
|
+
|
|
39
|
+
### Ability Definition
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// src/lib/casl.ts
|
|
43
|
+
import { AbilityBuilder, createMongoAbility, type MongoAbility } from "@casl/ability";
|
|
44
|
+
|
|
45
|
+
type Actions = "create" | "read" | "update" | "delete" | "manage";
|
|
46
|
+
type Subjects = "User" | "Post" | "Organization" | "all";
|
|
47
|
+
export type AppAbility = MongoAbility<[Actions, Subjects]>;
|
|
48
|
+
|
|
49
|
+
export function defineAbilityFor(user: AuthUser): AppAbility {
|
|
50
|
+
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
|
51
|
+
|
|
52
|
+
// Base permissions for all authenticated users
|
|
53
|
+
can("read", "Post", { published: true });
|
|
54
|
+
can("update", "User", { id: user.id }); // own profile
|
|
55
|
+
|
|
56
|
+
if (user.role === "ADMIN") {
|
|
57
|
+
can("manage", "User");
|
|
58
|
+
can("manage", "Post");
|
|
59
|
+
can("read", "Organization");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (user.role === "SUPER_ADMIN") {
|
|
63
|
+
can("manage", "all");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return build();
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Organization Ability
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
export function defineOrgAbilityFor(user: AuthUser, orgRole: OrgRole): AppAbility {
|
|
74
|
+
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
|
75
|
+
|
|
76
|
+
can("read", "Organization");
|
|
77
|
+
|
|
78
|
+
if (orgRole === "MANAGER" || orgRole === "OWNER") {
|
|
79
|
+
can("update", "Organization");
|
|
80
|
+
can("manage", "User"); // manage org members
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (orgRole === "OWNER") {
|
|
84
|
+
can("delete", "Organization");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return build();
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Service Layer Authorization
|
|
92
|
+
|
|
93
|
+
Check permissions in the **service layer**, never in presentation or DAL:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/services/post.service.ts
|
|
97
|
+
import { ForbiddenError } from "@casl/ability";
|
|
98
|
+
import { defineAbilityFor } from "@/lib/casl";
|
|
99
|
+
|
|
100
|
+
export async function deletePost(user: AuthUser, postId: string) {
|
|
101
|
+
const ability = defineAbilityFor(user);
|
|
102
|
+
const post = await findPostById(postId);
|
|
103
|
+
|
|
104
|
+
ForbiddenError.from(ability).throwUnlessCan("delete", {
|
|
105
|
+
...post,
|
|
106
|
+
__typename: "Post",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return removePost(postId);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Safe Route Protection
|
|
114
|
+
|
|
115
|
+
### Middleware-Level
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// middleware.ts
|
|
119
|
+
const publicRoutes = ["/", "/login", "/register", "/api/webhook"];
|
|
120
|
+
const adminRoutes = ["/admin"];
|
|
121
|
+
|
|
122
|
+
export async function middleware(request: NextRequest) {
|
|
123
|
+
const session = await getSession();
|
|
124
|
+
|
|
125
|
+
if (!session && !publicRoutes.some(r => request.nextUrl.pathname.startsWith(r))) {
|
|
126
|
+
return NextResponse.redirect(new URL("/login", request.url));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (adminRoutes.some(r => request.nextUrl.pathname.startsWith(r))) {
|
|
130
|
+
if (session?.user.role !== "ADMIN" && session?.user.role !== "SUPER_ADMIN") {
|
|
131
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Route Group Structure
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
src/app/
|
|
141
|
+
(public)/ # No auth required — landing, login, register
|
|
142
|
+
login/
|
|
143
|
+
register/
|
|
144
|
+
(auth)/ # Auth required, any role — onboarding
|
|
145
|
+
onboarding/
|
|
146
|
+
(app)/ # Auth required, active user — main app
|
|
147
|
+
dashboard/
|
|
148
|
+
settings/
|
|
149
|
+
admin/ # Admin only — user management, app settings
|
|
150
|
+
users/
|
|
151
|
+
settings/
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Auth Wrappers
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// src/lib/auth-wrappers.ts
|
|
158
|
+
import { getCurrentUser } from "@/lib/auth";
|
|
159
|
+
import { redirect } from "next/navigation";
|
|
160
|
+
|
|
161
|
+
export async function withAuth() {
|
|
162
|
+
const user = await getCurrentUser();
|
|
163
|
+
if (!user) redirect("/login");
|
|
164
|
+
return user;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function withAdmin() {
|
|
168
|
+
const user = await withAuth();
|
|
169
|
+
if (user.role !== "ADMIN" && user.role !== "SUPER_ADMIN") redirect("/");
|
|
170
|
+
return user;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function withOrgRole(orgId: string, requiredRole: OrgRole) {
|
|
174
|
+
const user = await withAuth();
|
|
175
|
+
const membership = await getOrgMembership(user.id, orgId);
|
|
176
|
+
if (!membership || !hasOrgRole(membership.role, requiredRole)) redirect("/");
|
|
177
|
+
return { user, membership };
|
|
178
|
+
}
|
|
179
|
+
```
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: better-auth-patterns
|
|
3
|
+
description: "Better Auth setup with session management, social login, organization plugin, and middleware. Use when implementing authentication, adding social login providers, or managing user sessions with Better Auth."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Better Auth Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Server-side session check** — use `getCurrentUser()` in RSC and Server Actions.
|
|
11
|
+
- **Client-side `useSession()`** — only in Client Components for UI state.
|
|
12
|
+
- **Middleware for redirects** — protect routes at the edge, not in pages.
|
|
13
|
+
- **Never expose auth secrets** — keep `BETTER_AUTH_SECRET` server-only.
|
|
14
|
+
- **Use plugins** — `organization()`, `admin()` for built-in features.
|
|
15
|
+
- **Social login always available** — Google + GitHub as defaults.
|
|
16
|
+
|
|
17
|
+
## Server Setup
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// src/lib/auth.ts
|
|
21
|
+
import { betterAuth } from "better-auth";
|
|
22
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
23
|
+
import { organization, admin } from "better-auth/plugins";
|
|
24
|
+
import { db } from "@/lib/db";
|
|
25
|
+
|
|
26
|
+
export const auth = betterAuth({
|
|
27
|
+
database: drizzleAdapter(db, { provider: "pg" }),
|
|
28
|
+
emailAndPassword: { enabled: true },
|
|
29
|
+
socialProviders: {
|
|
30
|
+
google: {
|
|
31
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
32
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
33
|
+
},
|
|
34
|
+
github: {
|
|
35
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
36
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
plugins: [
|
|
40
|
+
organization(),
|
|
41
|
+
admin(),
|
|
42
|
+
],
|
|
43
|
+
session: {
|
|
44
|
+
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
45
|
+
updateAge: 60 * 60 * 24, // refresh daily
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Client Setup
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// src/lib/auth-client.ts
|
|
54
|
+
import { createAuthClient } from "better-auth/react";
|
|
55
|
+
import { organizationClient, adminClient } from "better-auth/client/plugins";
|
|
56
|
+
|
|
57
|
+
export const authClient = createAuthClient({
|
|
58
|
+
plugins: [organizationClient(), adminClient()],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const { useSession, signIn, signUp, signOut } = authClient;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API Route Handler
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/app/api/auth/[...all]/route.ts
|
|
68
|
+
import { auth } from "@/lib/auth";
|
|
69
|
+
import { toNextJsHandler } from "better-auth/next-js";
|
|
70
|
+
|
|
71
|
+
export const { GET, POST } = toNextJsHandler(auth);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Session Management
|
|
75
|
+
|
|
76
|
+
### Server-Side Session
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/lib/auth-server.ts
|
|
80
|
+
import { auth } from "@/lib/auth";
|
|
81
|
+
import { headers } from "next/headers";
|
|
82
|
+
|
|
83
|
+
export async function getCurrentUser() {
|
|
84
|
+
const session = await auth.api.getSession({
|
|
85
|
+
headers: await headers(),
|
|
86
|
+
});
|
|
87
|
+
return session?.user ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function requireAuth() {
|
|
91
|
+
const user = await getCurrentUser();
|
|
92
|
+
if (!user) redirect("/login");
|
|
93
|
+
return user;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Client-Side Session
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
"use client";
|
|
101
|
+
import { useSession } from "@/lib/auth-client";
|
|
102
|
+
|
|
103
|
+
export function UserMenu() {
|
|
104
|
+
const { data: session, isPending } = useSession();
|
|
105
|
+
|
|
106
|
+
if (isPending) return <Skeleton />;
|
|
107
|
+
if (!session) return <LoginButton />;
|
|
108
|
+
|
|
109
|
+
return <span>{session.user.name}</span>;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Authentication Flows
|
|
114
|
+
|
|
115
|
+
### Sign Up
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
"use client";
|
|
119
|
+
import { authClient } from "@/lib/auth-client";
|
|
120
|
+
|
|
121
|
+
async function handleSignUp(data: SignUpInput) {
|
|
122
|
+
const { error } = await authClient.signUp.email({
|
|
123
|
+
email: data.email,
|
|
124
|
+
password: data.password,
|
|
125
|
+
name: data.name,
|
|
126
|
+
});
|
|
127
|
+
if (error) toast.error(error.message);
|
|
128
|
+
else router.push("/dashboard");
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Social Login
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
async function handleGoogleLogin() {
|
|
136
|
+
await authClient.signIn.social({ provider: "google" });
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Sign Out
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
async function handleSignOut() {
|
|
144
|
+
await authClient.signOut();
|
|
145
|
+
router.push("/");
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Middleware
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// middleware.ts
|
|
153
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
154
|
+
import { NextResponse } from "next/server";
|
|
155
|
+
import type { NextRequest } from "next/server";
|
|
156
|
+
import type { Session } from "better-auth/types";
|
|
157
|
+
|
|
158
|
+
const publicRoutes = ["/", "/login", "/register", "/api/auth"];
|
|
159
|
+
|
|
160
|
+
export async function middleware(request: NextRequest) {
|
|
161
|
+
const { pathname } = request.nextUrl;
|
|
162
|
+
|
|
163
|
+
// Skip public routes
|
|
164
|
+
if (publicRoutes.some((r) => pathname.startsWith(r))) {
|
|
165
|
+
return NextResponse.next();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check session
|
|
169
|
+
const { data: session } = await betterFetch<Session>(
|
|
170
|
+
"/api/auth/get-session",
|
|
171
|
+
{
|
|
172
|
+
baseURL: request.nextUrl.origin,
|
|
173
|
+
headers: { cookie: request.headers.get("cookie") ?? "" },
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!session) {
|
|
178
|
+
return NextResponse.redirect(new URL("/login", request.url));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Admin route protection
|
|
182
|
+
if (pathname.startsWith("/admin") && session.user.role !== "admin") {
|
|
183
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return NextResponse.next();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const config = {
|
|
190
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Organization Plugin
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// Create organization
|
|
198
|
+
const { data: org } = await authClient.organization.create({
|
|
199
|
+
name: "Acme Inc",
|
|
200
|
+
slug: "acme",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Invite member
|
|
204
|
+
await authClient.organization.inviteMember({
|
|
205
|
+
email: "user@example.com",
|
|
206
|
+
role: "member",
|
|
207
|
+
organizationId: org.id,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Switch active organization
|
|
211
|
+
await authClient.organization.setActive({ organizationId: org.id });
|
|
212
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chrome-extension-patterns
|
|
3
|
+
description: "Chrome extension development patterns for Manifest V3, content scripts, popup UI, service workers, messaging, and storage. Use when building browser extensions, creating content scripts, setting up background service workers, or implementing extension popup interfaces."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Chrome Extension Patterns (Manifest V3)
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Always use Manifest V3** — Manifest V2 is deprecated and no longer accepted.
|
|
11
|
+
- **Minimal permissions** — request only what the extension needs; use `activeTab` over `<all_urls>`.
|
|
12
|
+
- **Never use `setInterval` in service workers** — use `chrome.alarms` for periodic tasks.
|
|
13
|
+
- **Never use `innerHTML` with user input** — use `textContent` to prevent XSS.
|
|
14
|
+
- **Never include API keys in content scripts** — they are visible to the host page.
|
|
15
|
+
- **Never use `localStorage` in content scripts** — it belongs to the host page; use `chrome.storage`.
|
|
16
|
+
|
|
17
|
+
## Manifest
|
|
18
|
+
|
|
19
|
+
- Define minimal permissions — request only what the extension needs:
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"manifest_version": 3,
|
|
23
|
+
"permissions": ["storage", "activeTab"],
|
|
24
|
+
"host_permissions": ["https://specific-site.com/*"]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
- Use `activeTab` instead of broad `<all_urls>` when possible.
|
|
28
|
+
- Declare content scripts and their match patterns explicitly.
|
|
29
|
+
- Use `"action"` (not `"browser_action"`) for the extension button.
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
- **Popup** (`popup.html/tsx`): lightweight UI shown on extension icon click. Keep it fast — no heavy initialization.
|
|
34
|
+
- **Options page** (`options.html/tsx`): settings and configuration UI.
|
|
35
|
+
- **Background service worker** (`background.ts`): event-driven logic, API calls, alarms, message routing.
|
|
36
|
+
- **Content scripts** (`content.ts`): code injected into web pages to read/modify DOM.
|
|
37
|
+
- Keep these four concerns strictly separated — never mix responsibilities.
|
|
38
|
+
|
|
39
|
+
## Service Workers (Background)
|
|
40
|
+
|
|
41
|
+
- Service workers are **event-driven and ephemeral** — they start on events and stop when idle.
|
|
42
|
+
- Never rely on global state persisting between activations. Use `chrome.storage` instead.
|
|
43
|
+
- Register event listeners at the top level (not inside async callbacks):
|
|
44
|
+
```ts
|
|
45
|
+
chrome.runtime.onInstalled.addListener(() => { /* ... */ })
|
|
46
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { /* ... */ })
|
|
47
|
+
```
|
|
48
|
+
- Use `chrome.alarms` for periodic tasks — never `setInterval`.
|
|
49
|
+
- Return `true` from `onMessage` listener to send async responses.
|
|
50
|
+
|
|
51
|
+
## Content Scripts
|
|
52
|
+
|
|
53
|
+
- Content scripts run in an **isolated world** — they can access the page DOM but not the page's JS variables.
|
|
54
|
+
- Sanitize all DOM manipulation to prevent XSS:
|
|
55
|
+
```ts
|
|
56
|
+
element.textContent = userInput // Safe
|
|
57
|
+
element.innerHTML = userInput // DANGEROUS — never do this
|
|
58
|
+
```
|
|
59
|
+
- Minimize content script footprint — inject only what's necessary.
|
|
60
|
+
- Use `MutationObserver` for dynamic pages instead of polling.
|
|
61
|
+
- Communicate with background via `chrome.runtime.sendMessage()`.
|
|
62
|
+
|
|
63
|
+
## Storage
|
|
64
|
+
|
|
65
|
+
- Use `chrome.storage.local` for large data (up to 10MB).
|
|
66
|
+
- Use `chrome.storage.sync` for user settings that should sync across devices (100KB limit).
|
|
67
|
+
- Never use `localStorage` in content scripts — it belongs to the host page.
|
|
68
|
+
- Listen for storage changes:
|
|
69
|
+
```ts
|
|
70
|
+
chrome.storage.onChanged.addListener((changes, area) => { /* ... */ })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Messaging
|
|
74
|
+
|
|
75
|
+
- Use `chrome.runtime.sendMessage` for popup/options <-> background communication.
|
|
76
|
+
- Use `chrome.tabs.sendMessage` for background -> content script communication.
|
|
77
|
+
- Define a message type system for type-safe messaging:
|
|
78
|
+
```ts
|
|
79
|
+
type Message =
|
|
80
|
+
| { type: 'GET_DATA'; payload: { key: string } }
|
|
81
|
+
| { type: 'SAVE_DATA'; payload: { key: string; value: unknown } }
|
|
82
|
+
```
|
|
83
|
+
- Validate all incoming messages — never trust message content blindly.
|
|
84
|
+
|
|
85
|
+
## Popup UI (React)
|
|
86
|
+
|
|
87
|
+
- Keep popup bundle small — lazy load heavy features.
|
|
88
|
+
- Use React for popup and options page, bundled with tsup or Vite.
|
|
89
|
+
- Popup window is destroyed on close — persist state to `chrome.storage`.
|
|
90
|
+
- Set a fixed width/height for popup: `min-width: 320px; min-height: 400px`.
|
|
91
|
+
|
|
92
|
+
## Security
|
|
93
|
+
|
|
94
|
+
- Never inject user-generated content into web pages without sanitization.
|
|
95
|
+
- Use Content Security Policy (CSP) in manifest.
|
|
96
|
+
- Validate all data from external sources (APIs, messages, storage).
|
|
97
|
+
- Never include API keys or secrets in content scripts — they're visible to the page.
|
|
98
|
+
- Audit `host_permissions` — overly broad permissions trigger Chrome Web Store review delays.
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
- Use `chrome://extensions` with Developer Mode for loading unpacked extensions.
|
|
103
|
+
- Enable "Errors" view for debugging service worker issues.
|
|
104
|
+
- Use Chrome DevTools to inspect popup (right-click -> Inspect) and background (service worker link).
|
|
105
|
+
- Hot-reload: rebuild with tsup watch, then click "Update" in `chrome://extensions`.
|