@spfn/core 0.2.0-beta.49 → 0.2.0-beta.50

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.
@@ -1,337 +0,0 @@
1
- # Middleware
2
-
3
- Named middleware system with route-level skip control.
4
-
5
- ## Define Middleware
6
-
7
- ### Regular Middleware
8
-
9
- ```typescript
10
- import { defineMiddleware } from '@spfn/core/route';
11
-
12
- export const authMiddleware = defineMiddleware('auth', async (c, next) => {
13
- const token = c.req.header('authorization');
14
-
15
- if (!token)
16
- {
17
- return c.json({ error: 'Unauthorized' }, 401);
18
- }
19
-
20
- const user = await verifyToken(token);
21
- c.set('user', user);
22
-
23
- await next();
24
- });
25
- ```
26
-
27
- ### Factory Middleware
28
-
29
- Middleware with parameters:
30
-
31
- ```typescript
32
- export const requireRole = defineMiddleware('role',
33
- (...roles: string[]) => async (c, next) => {
34
- const user = c.get('user');
35
-
36
- if (!roles.includes(user.role))
37
- {
38
- return c.json({ error: 'Forbidden' }, 403);
39
- }
40
-
41
- await next();
42
- }
43
- );
44
-
45
- // Usage
46
- route.get('/admin')
47
- .use([requireRole('admin', 'superadmin')])
48
- .handler(...)
49
- ```
50
-
51
- ### Two-Parameter Factory
52
-
53
- For factory with exactly 2 parameters, use `defineMiddlewareFactory`:
54
-
55
- ```typescript
56
- import { defineMiddlewareFactory } from '@spfn/core/route';
57
-
58
- export const rateLimiter = defineMiddlewareFactory('rateLimit',
59
- (limit: number, windowMs: number) => async (c, next) => {
60
- // Rate limit logic
61
- await next();
62
- }
63
- );
64
-
65
- // Usage
66
- route.get('/api')
67
- .use([rateLimiter(100, 60000)]) // 100 requests per minute
68
- .handler(...)
69
- ```
70
-
71
- ---
72
-
73
- ## Register Global Middleware
74
-
75
- ```typescript
76
- // src/server/server.config.ts
77
- import { defineServerConfig } from '@spfn/core/server';
78
- import { authMiddleware, loggerMiddleware, corsMiddleware } from './middlewares';
79
-
80
- export default defineServerConfig()
81
- .middlewares([
82
- loggerMiddleware,
83
- corsMiddleware,
84
- authMiddleware
85
- ])
86
- .routes(appRouter)
87
- .build();
88
- ```
89
-
90
- **Execution order:**
91
- 1. Global middlewares (in registration order)
92
- 2. Route-level middlewares (from `.use()`)
93
- 3. Validation middleware (automatic)
94
- 4. Route handler
95
-
96
- ---
97
-
98
- ## Use in Routes
99
-
100
- ### Add Middleware
101
-
102
- ```typescript
103
- import { Transactional } from '@spfn/core/db';
104
- import { authMiddleware, requireRole } from './middlewares';
105
-
106
- route.post('/admin/users')
107
- .use([authMiddleware, requireRole('admin'), Transactional()])
108
- .handler(async (c) => {
109
- const user = c.raw.get('user'); // From authMiddleware
110
- // ...
111
- });
112
- ```
113
-
114
- ### Skip Global Middleware
115
-
116
- ```typescript
117
- // Skip specific middlewares by name
118
- route.get('/public/health')
119
- .skip(['auth', 'rateLimit'])
120
- .handler(async (c) => {
121
- return { status: 'ok' };
122
- });
123
-
124
- // Skip all global middlewares
125
- route.get('/webhooks/stripe')
126
- .skip('*')
127
- .handler(async (c) => {
128
- // No global middleware applied
129
- });
130
- ```
131
-
132
- **Note:** Route-level middlewares (`.use()`) are never skipped.
133
-
134
- ---
135
-
136
- ## Common Middleware Patterns
137
-
138
- ### Authentication
139
-
140
- ```typescript
141
- export const authMiddleware = defineMiddleware('auth', async (c, next) => {
142
- const token = c.req.header('authorization')?.replace('Bearer ', '');
143
-
144
- if (!token)
145
- {
146
- return c.json({ error: 'Missing token' }, 401);
147
- }
148
-
149
- try
150
- {
151
- const payload = await verifyJWT(token);
152
- c.set('userId', payload.sub);
153
- c.set('user', await userRepo.findById(payload.sub));
154
- await next();
155
- }
156
- catch
157
- {
158
- return c.json({ error: 'Invalid token' }, 401);
159
- }
160
- });
161
- ```
162
-
163
- ### Role-based Access
164
-
165
- ```typescript
166
- export const requirePermissions = defineMiddleware('permission',
167
- (...permissions: string[]) => async (c, next) => {
168
- const user = c.get('user');
169
-
170
- const hasPermission = permissions.every(p =>
171
- user.permissions.includes(p)
172
- );
173
-
174
- if (!hasPermission)
175
- {
176
- return c.json({ error: 'Insufficient permissions' }, 403);
177
- }
178
-
179
- await next();
180
- }
181
- );
182
-
183
- // Usage
184
- route.delete('/posts/:id')
185
- .use([requirePermissions('posts:delete')])
186
- .handler(...)
187
- ```
188
-
189
- ### Rate Limiting
190
-
191
- ```typescript
192
- const requestCounts = new Map<string, { count: number; resetAt: number }>();
193
-
194
- export const rateLimiter = defineMiddlewareFactory('rateLimit',
195
- (limit: number, windowMs: number) => async (c, next) => {
196
- const key = c.req.header('x-forwarded-for') || 'unknown';
197
- const now = Date.now();
198
-
199
- const record = requestCounts.get(key);
200
-
201
- if (!record || now > record.resetAt)
202
- {
203
- requestCounts.set(key, { count: 1, resetAt: now + windowMs });
204
- }
205
- else if (record.count >= limit)
206
- {
207
- return c.json({ error: 'Too many requests' }, 429);
208
- }
209
- else
210
- {
211
- record.count++;
212
- }
213
-
214
- await next();
215
- }
216
- );
217
- ```
218
-
219
- ### Request Logging
220
-
221
- ```typescript
222
- export const requestLogger = defineMiddleware('requestLogger', async (c, next) => {
223
- const start = Date.now();
224
- const method = c.req.method;
225
- const path = c.req.path;
226
-
227
- await next();
228
-
229
- const duration = Date.now() - start;
230
- const status = c.res.status;
231
-
232
- console.log(`${method} ${path} ${status} ${duration}ms`);
233
- });
234
- ```
235
-
236
- ### CORS
237
-
238
- ```typescript
239
- export const corsMiddleware = defineMiddleware('cors', async (c, next) => {
240
- const origin = c.req.header('origin');
241
-
242
- if (origin && allowedOrigins.includes(origin))
243
- {
244
- c.header('Access-Control-Allow-Origin', origin);
245
- c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
246
- c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
247
- }
248
-
249
- if (c.req.method === 'OPTIONS')
250
- {
251
- return c.body(null, 204);
252
- }
253
-
254
- await next();
255
- });
256
- ```
257
-
258
- ---
259
-
260
- ## Middleware Deduplication
261
-
262
- When the same middleware is registered both globally and in a route, it's automatically deduplicated:
263
-
264
- ```typescript
265
- // Registered globally
266
- .middlewares([authMiddleware])
267
-
268
- // Also used in route
269
- route.get('/users')
270
- .use([authMiddleware]) // Skipped (already applied globally)
271
- .handler(...)
272
- ```
273
-
274
- ---
275
-
276
- ## Access Context Data
277
-
278
- ### Set Data
279
-
280
- ```typescript
281
- defineMiddleware('auth', async (c, next) => {
282
- c.set('user', user);
283
- c.set('sessionId', sessionId);
284
- await next();
285
- });
286
- ```
287
-
288
- ### Get Data in Handler
289
-
290
- ```typescript
291
- route.get('/profile')
292
- .handler(async (c) => {
293
- const user = c.raw.get('user');
294
- const sessionId = c.raw.get('sessionId');
295
- // ...
296
- });
297
- ```
298
-
299
- ---
300
-
301
- ## Best Practices
302
-
303
- ### Do
304
-
305
- ```typescript
306
- // 1. Use meaningful names for skip control
307
- export const authMiddleware = defineMiddleware('auth', ...);
308
- export const rateLimiter = defineMiddleware('rateLimit', ...);
309
-
310
- // 2. Set context data for downstream use
311
- c.set('user', user);
312
-
313
- // 3. Return early for unauthorized requests
314
- if (!token) return c.json({ error: 'Unauthorized' }, 401);
315
-
316
- // 4. Always call next() for successful middleware
317
- await next();
318
- ```
319
-
320
- ### Don't
321
-
322
- ```typescript
323
- // 1. Don't forget to call next()
324
- defineMiddleware('logger', async (c, next) => {
325
- console.log(c.req.path);
326
- // Missing next() - request hangs!
327
- });
328
-
329
- // 2. Don't use generic names
330
- defineMiddleware('middleware1', ...); // Bad
331
-
332
- // 3. Don't throw errors - return responses
333
- defineMiddleware('auth', async (c, next) => {
334
- if (!token) throw new Error('No token'); // Bad
335
- if (!token) return c.json({ error: 'No token' }, 401); // Good
336
- });
337
- ```
package/docs/nextjs.md DELETED
@@ -1,247 +0,0 @@
1
- # Next.js Integration
2
-
3
- RPC proxy and type-safe API client for Next.js.
4
-
5
- ## Setup
6
-
7
- ### 1. Create RPC Proxy
8
-
9
- ```typescript
10
- // app/api/rpc/[routeName]/route.ts
11
- import '@spfn/auth/nextjs/api';
12
- import { createRpcProxy } from '@spfn/core/nextjs/server';
13
- import { authRouteMap } from '@spfn/auth';
14
- import { eventRouteMap } from '@spfn/core/event';
15
- import { routeMap } from '@/generated/route-map';
16
-
17
- export const { GET, POST } = createRpcProxy({
18
- routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },
19
- });
20
- ```
21
-
22
- ### 2. Create API Client
23
-
24
- ```typescript
25
- // src/lib/api.ts
26
- import { createApi } from '@spfn/core/nextjs';
27
- import type { AppRouter } from '@/server/server.config';
28
-
29
- export const api = createApi<AppRouter>();
30
- ```
31
-
32
- ## Usage
33
-
34
- ### Server Components
35
-
36
- ```typescript
37
- // app/users/[id]/page.tsx
38
- import { api } from '@/lib/api';
39
-
40
- export default async function UserPage({ params }: { params: { id: string } })
41
- {
42
- const user = await api.getUser.call({
43
- params: { id: params.id }
44
- });
45
-
46
- return <div>{user.name}</div>;
47
- }
48
- ```
49
-
50
- ### Client Components
51
-
52
- ```typescript
53
- 'use client';
54
-
55
- import { api } from '@/lib/api';
56
- import { useState } from 'react';
57
-
58
- export function CreateUserForm()
59
- {
60
- const [loading, setLoading] = useState(false);
61
-
62
- async function handleSubmit(formData: FormData)
63
- {
64
- setLoading(true);
65
- try
66
- {
67
- await api.createUser.call({
68
- body: {
69
- email: formData.get('email') as string,
70
- name: formData.get('name') as string
71
- }
72
- });
73
- }
74
- finally
75
- {
76
- setLoading(false);
77
- }
78
- }
79
-
80
- return (
81
- <form action={handleSubmit}>
82
- {/* ... */}
83
- </form>
84
- );
85
- }
86
- ```
87
-
88
- ### Server Actions
89
-
90
- ```typescript
91
- // app/actions.ts
92
- 'use server';
93
-
94
- import { api } from '@/lib/api';
95
-
96
- export async function createUser(formData: FormData)
97
- {
98
- const user = await api.createUser.call({
99
- body: {
100
- email: formData.get('email') as string,
101
- name: formData.get('name') as string
102
- }
103
- });
104
-
105
- return user;
106
- }
107
- ```
108
-
109
- ## API Client Methods
110
-
111
- ```typescript
112
- // Call with params
113
- const user = await api.getUser.call({
114
- params: { id: '123' }
115
- });
116
-
117
- // Call with query
118
- const users = await api.getUsers.call({
119
- query: { page: 1, limit: 20, search: 'john' }
120
- });
121
-
122
- // Call with body
123
- const created = await api.createUser.call({
124
- body: { email: 'user@example.com', name: 'User' }
125
- });
126
-
127
- // Call with multiple inputs
128
- const updated = await api.updateUser.call({
129
- params: { id: '123' },
130
- body: { name: 'Updated Name' }
131
- });
132
- ```
133
-
134
- ## Interceptors
135
-
136
- ### Request Interceptor
137
-
138
- ```typescript
139
- export const { GET, POST } = createRpcProxy({
140
- routeMap: { ...routeMap, ...authRouteMap },
141
- apiUrl: process.env.SPFN_API_URL,
142
- interceptors: {
143
- request: async (request, context) => {
144
- // Add auth header
145
- const token = cookies().get('token')?.value;
146
- if (token)
147
- {
148
- request.headers.set('Authorization', `Bearer ${token}`);
149
- }
150
- return request;
151
- }
152
- }
153
- });
154
- ```
155
-
156
- ### Response Interceptor
157
-
158
- ```typescript
159
- interceptors: {
160
- response: async (response, context) => {
161
- // Handle Set-Cookie from API
162
- const setCookie = response.headers.get('set-cookie');
163
- if (setCookie)
164
- {
165
- cookies().set(parseCookie(setCookie));
166
- }
167
- return response;
168
- }
169
- }
170
- ```
171
-
172
- ## Cookie Handling
173
-
174
- The RPC proxy automatically handles HttpOnly cookies:
175
-
176
- ```typescript
177
- // Server sets cookie
178
- c.header('Set-Cookie', 'session=abc; HttpOnly; Secure');
179
-
180
- // Proxy forwards to browser
181
- // Browser stores HttpOnly cookie
182
- // Subsequent requests include cookie automatically
183
- ```
184
-
185
- ## Error Handling
186
-
187
- ```typescript
188
- try
189
- {
190
- const user = await api.getUser.call({ params: { id: '123' } });
191
- }
192
- catch (error)
193
- {
194
- if (error.status === 404)
195
- {
196
- // Not found
197
- }
198
- else if (error.status === 401)
199
- {
200
- // Unauthorized
201
- }
202
- }
203
- ```
204
-
205
- ## Environment Variables
206
-
207
- ```bash
208
- # API server URL
209
- SPFN_API_URL=http://localhost:8790
210
-
211
- # For production
212
- SPFN_API_URL=https://api.example.com
213
-
214
- # RPC proxy timeout (AbortController, default: 120s)
215
- # Should be shorter than FETCH_HEADERS_TIMEOUT for meaningful 504 responses
216
- RPC_PROXY_TIMEOUT=120000
217
- ```
218
-
219
- ## Best Practices
220
-
221
- ```typescript
222
- // 1. Create single api instance
223
- // src/lib/api.ts
224
- export const api = createApi<AppRouter>();
225
-
226
- // 2. Use in Server Components for SSR
227
- export default async function Page() {
228
- const data = await api.getData.call({}); // SSR
229
- return <div>{data}</div>;
230
- }
231
-
232
- // 3. Handle loading states in Client Components
233
- const [loading, setLoading] = useState(false);
234
-
235
- // 4. Use Server Actions for mutations
236
- 'use server';
237
- export async function createItem(formData: FormData) {
238
- return api.createItem.call({ body: { ... } });
239
- }
240
-
241
- // 5. Type-safe error handling
242
- try {
243
- await api.getUser.call({ params: { id } });
244
- } catch (e) {
245
- if (e.status === 404) redirect('/not-found');
246
- }
247
- ```