sh-ui-cli 0.31.1 → 0.32.1

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.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/data/changelog/versions.json +26 -0
  3. package/package.json +1 -1
  4. package/src/create/cli-args.js +1 -1
  5. package/src/create/generator.js +55 -1
  6. package/src/create/index.mjs +2 -2
  7. package/src/create/plugins/authJwt.js +340 -0
  8. package/src/create/plugins/index.js +2 -1
  9. package/src/create/plugins/sentry.js +32 -280
  10. package/src/mcp.mjs +1 -1
  11. package/templates/nextjs-app/app/api/proxy/[...path]/route.ts +31 -4
  12. package/templates/nextjs-app/package.json +0 -1
  13. package/templates/nextjs-app/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
  14. package/templates/nextjs-app/src/shared/api/clientFetch.ts +40 -0
  15. package/templates/nextjs-app/src/shared/api/http.ts +13 -56
  16. package/templates/nextjs-app/src/shared/api/observability.ts +20 -0
  17. package/templates/nextjs-app/src/shared/api/queryClient.ts +30 -0
  18. package/templates/nextjs-app/src/shared/api/serverFetch.ts +59 -0
  19. package/templates/nextjs-app/src/shared/hooks/useAppMutation.ts +52 -0
  20. package/templates/nextjs-standalone/app/api/proxy/[...path]/route.ts +31 -4
  21. package/templates/nextjs-standalone/package.json +0 -1
  22. package/templates/nextjs-standalone/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
  23. package/templates/nextjs-standalone/src/shared/api/clientFetch.ts +40 -0
  24. package/templates/nextjs-standalone/src/shared/api/http.ts +13 -56
  25. package/templates/nextjs-standalone/src/shared/api/observability.ts +20 -0
  26. package/templates/nextjs-standalone/src/shared/api/queryClient.ts +30 -0
  27. package/templates/nextjs-standalone/src/shared/api/serverFetch.ts +59 -0
  28. package/templates/nextjs-standalone/src/shared/hooks/useAppMutation.ts +52 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ npx sh-ui-cli create
22
22
 
23
23
  # 비대화형 (에이전트 / CI)
24
24
  npx sh-ui-cli create my-app --platform next --structure standalone --yes
25
- npx sh-ui-cli create my-app --platform next --structure monorepo --plugins sentry,next-intl --yes
25
+ npx sh-ui-cli create my-app --platform next --structure monorepo --plugins sentry,next-intl,auth-jwt --yes
26
26
  npx sh-ui-cli create my-app --platform flutter --yes
27
27
  ```
28
28
 
@@ -2,6 +2,32 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
4
4
  "versions": [
5
+ {
6
+ "version": "0.32.1",
7
+ "date": "2026-04-29",
8
+ "title": "auth-jwt 의 proxy.ts 경로 버그 수정 + next-intl 와 자동 합성",
9
+ "type": "patch",
10
+ "highlights": [
11
+ "auth-jwt 플러그인이 proxy.ts 를 src/proxy.ts 로 잘못 깔아 Next 16 이 인식 못 하던 문제 — 베이스 템플릿이 app/ 을 root 에 두므로 proxy.ts 도 root 로 가야 함. 결과적으로 v0.32.0 의 인증 라우트 가드가 동작하지 않았음, 이번 패치에서 해결",
12
+ "auth-jwt + next-intl 동시 활성화 시 generator 가 proxy.ts 를 자동 합성 — intl 미들웨어 + locale prefix 를 벗긴 경로 기반 인증 가드. 단독 사용 시에는 각 플러그인의 단일 proxy.ts 그대로 적용",
13
+ "/cli 페이지 플러그인 섹션 갱신 — Sentry 의 옛 axios 인터셉터/apiCore 설명 제거, observability 브릿지 정확화, auth-jwt 항목 신설. 기본 스택의 'Axios' 표기를 fetch 기반 isomorphic http() 로 정정",
14
+ "auth-jwt / next-intl 플러그인 페이지의 폴더 트리 + 파일 설명을 실제 manifest 와 동기화"
15
+ ],
16
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.32.1"
17
+ },
18
+ {
19
+ "version": "0.32.0",
20
+ "date": "2026-04-29",
21
+ "title": "auth-jwt 플러그인 신설 + isomorphic HTTP transport 재설계",
22
+ "type": "minor",
23
+ "highlights": [
24
+ "신규 --plugins auth-jwt — Next 16 proxy.ts 미들웨어, refresh placeholder, withAuthRetry 헬퍼, refresh-aware BFF 를 한 번에 추가. 백엔드 refresh API 명세 확정 후 refreshSession.ts 본문만 채우면 자동 활성화",
25
+ "베이스 HTTP 레이어 재설계 — axios 단일 인스턴스를 isomorphic http() + serverFetch / clientFetch 두 갈래 transport 로 교체. RSC 는 백엔드 직통, 브라우저는 /api/proxy 경유 → API 함수는 한 번만 작성하고 RSC prefetch + hydration 패턴과 자연스럽게 어울림",
26
+ "Sentry 플러그인 슬림화 — HTTP/proxy 인프라 파일을 베이스로 이관하고, Sentry 는 observability.ts 를 Sentry-aware 버전으로 덮어써 5xx 캡처/로깅을 활성화. 두 플러그인이 독립적으로 조합됨",
27
+ "queryClient.ts 추가 — React cache() 기반 RSC 스코프 + 브라우저 싱글톤. 요청 간 캐시 누수 방지. apps/docs 의 auth / api-layer / testing 레시피도 새 설계에 맞춰 갱신"
28
+ ],
29
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.32.0"
30
+ },
5
31
  {
6
32
  "version": "0.31.1",
7
33
  "date": "2026-04-28",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.31.1",
3
+ "version": "0.32.1",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  const VALID_PLATFORMS = ['next', 'flutter'];
2
2
  const VALID_STRUCTURES = ['standalone', 'monorepo'];
3
- const VALID_PLUGINS = ['sentry', 'next-intl'];
3
+ const VALID_PLUGINS = ['sentry', 'next-intl', 'auth-jwt'];
4
4
 
5
5
  const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app'];
6
6
  const BOOL_FLAGS = ['yes', 'help'];
@@ -90,7 +90,7 @@ export async function createProject(options = {}) {
90
90
  });
91
91
 
92
92
  // plugins 는 미지정시 기본 빈 배열 — prompt 띄우지 않는다.
93
- // (플러그인을 쓰려면 명시적으로 --plugins sentry,next-intl 사용)
93
+ // (플러그인을 쓰려면 명시적으로 --plugins sentry,next-intl,auth-jwt 사용)
94
94
  const selectedPluginNames = options.plugins ?? [];
95
95
 
96
96
  const plugins = getPluginsByNames(selectedPluginNames);
@@ -428,6 +428,60 @@ async function writePluginFiles(targetDir, plugins) {
428
428
  }
429
429
  }
430
430
  }
431
+
432
+ // auth-jwt + next-intl 동시 활성화 시 proxy.ts 병합
433
+ // (각 플러그인이 단독으로 깐 proxy.ts 를 합친 버전으로 덮어쓴다)
434
+ const names = new Set(plugins.map((p) => p.name));
435
+ if (names.has('auth-jwt') && names.has('next-intl')) {
436
+ const mergedProxy = `import createIntlMiddleware from 'next-intl/middleware';
437
+ import { NextRequest, NextResponse } from 'next/server';
438
+
439
+ import { routing } from '@/src/shared/config/i18n/routing';
440
+
441
+ const AUTH_ROUTES = ['/sign-in', '/sign-up'];
442
+
443
+ const intl = createIntlMiddleware(routing);
444
+
445
+ /**
446
+ * 로케일 prefix (/ko, /en) 를 벗겨 인증 라우트 매칭에 사용한다.
447
+ * 예: /ko/sign-in → /sign-in
448
+ */
449
+ const stripLocalePrefix = (pathname: string): string => {
450
+ const locales = routing.locales as readonly string[];
451
+ const segments = pathname.split('/').filter(Boolean);
452
+ if (segments[0] && locales.includes(segments[0])) {
453
+ const rest = segments.slice(1).join('/');
454
+ return \`/\${rest}\`.replace(/\\/$/, '') || '/';
455
+ }
456
+ return pathname;
457
+ };
458
+
459
+ /**
460
+ * Next 16+ proxy.ts (구 middleware.ts).
461
+ * next-intl 라우팅 + auth-jwt 토큰 존재 체크 합성 버전.
462
+ *
463
+ * - intl 이 먼저 로케일 prefix 처리 + NEXT_LOCALE 쿠키 set
464
+ * - 그 위에 인증 가드 — 토큰 없고 인증 라우트도 아니면 /sign-in 으로 redirect
465
+ * - AT 만료 검사나 refresh 는 하지 않는다 (BFF 가 처리)
466
+ */
467
+ export default function proxy(req: NextRequest) {
468
+ const intlRes = intl(req);
469
+ const pathname = stripLocalePrefix(req.nextUrl.pathname);
470
+ const hasToken = !!req.cookies.get('accessToken')?.value;
471
+ const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
472
+
473
+ if (isAuthRoute) return intlRes;
474
+ if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
475
+
476
+ return intlRes;
477
+ }
478
+
479
+ export const config = {
480
+ matcher: '/((?!api|_next|_vercel|monitoring|.*\\\\..*).*)',
481
+ };
482
+ `;
483
+ await fs.writeFile(path.join(targetDir, 'proxy.ts'), mergedProxy);
484
+ }
431
485
  }
432
486
 
433
487
  async function composeProviders(targetDir, plugins) {
@@ -13,7 +13,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
13
13
  옵션:
14
14
  --platform <next|flutter> 타겟 플랫폼
15
15
  --structure <standalone|monorepo> Next.js 프로젝트 구조 (next 일 때)
16
- --plugins <a,b> 플러그인 (sentry, next-intl). 미지정/"" → 없음
16
+ --plugins <a,b> 플러그인 (sentry, next-intl, auth-jwt). 미지정/"" → 없음
17
17
  --theme <base64> 테마 JSON (base64). 선택
18
18
  --yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
19
19
  -h, --help 이 도움말
@@ -23,7 +23,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
23
23
 
24
24
  예 (비대화형 / 에이전트 / CI):
25
25
  sh-ui create my-app --platform next --structure standalone --yes
26
- sh-ui create my-app --platform next --structure monorepo --plugins sentry,next-intl --yes
26
+ sh-ui create my-app --platform next --structure monorepo --plugins sentry,next-intl,auth-jwt --yes
27
27
  sh-ui create my-app --platform flutter --yes
28
28
 
29
29
  비대화형 환경(TTY 없음)에서는 누락된 필수 인자가 있으면 prompt 대신 에러로 종료한다.
@@ -0,0 +1,340 @@
1
+ export const authJwtPlugin = {
2
+ name: 'auth-jwt',
3
+ label: '쿠키 기반 JWT 인증 (refresh 자리표시자 포함)',
4
+ priority: 2,
5
+
6
+ // 의존성 추가 없음 — 베이스의 fetch + cookies + react-query 만 사용
7
+
8
+ envVars: [
9
+ '# Auth (auth-jwt)',
10
+ 'COOKIE_SECURE=false',
11
+ ],
12
+
13
+ turboEnvVars: [
14
+ 'COOKIE_SECURE',
15
+ ],
16
+
17
+ providerImports: [],
18
+ providerWrappers: [],
19
+
20
+ // ─── 독립 파일 ───
21
+ //
22
+ // 베이스의 BFF (app/api/proxy/[...path]/route.ts) 를 refresh-aware 버전으로
23
+ // 덮어쓰고, 미들웨어와 인증 헬퍼를 추가한다.
24
+ // refreshSession.ts 는 v1 placeholder — 백엔드 명세 확정 후 본문만 채우면
25
+ // BFF 와 withAuthRetry 가 자동 활용한다.
26
+
27
+ files: {
28
+ 'proxy.ts': `import { NextRequest, NextResponse } from 'next/server';
29
+
30
+ const AUTH_ROUTES = ['/sign-in', '/sign-up'];
31
+
32
+ /**
33
+ * Next 16+ 의 proxy.ts (구 middleware.ts).
34
+ * 토큰 존재 여부만 검사한다 — 만료 검사나 refresh 는 하지 않는다.
35
+ *
36
+ * - AT 쿠키 없음 + 인증 라우트 아님 → /sign-in 으로 리다이렉트
37
+ * - AT 쿠키 있음 또는 인증 라우트 → 통과
38
+ *
39
+ * AT 가 만료된 채 통과한 요청은 BFF (/api/proxy) 가 401 을 받아
40
+ * refreshSession 으로 갱신을 시도한다.
41
+ */
42
+ export default function proxy(req: NextRequest) {
43
+ const { pathname } = req.nextUrl;
44
+ const hasToken = !!req.cookies.get('accessToken')?.value;
45
+ const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
46
+
47
+ if (isAuthRoute) return NextResponse.next();
48
+ if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
49
+
50
+ return NextResponse.next();
51
+ }
52
+
53
+ export const config = {
54
+ matcher: '/((?!api|_next|.*\\\\..*).*)',
55
+ };
56
+ `,
57
+
58
+ 'src/shared/api/refreshSession.ts': `type RefreshResult =
59
+ | { ok: true; accessToken: string; refreshToken: string }
60
+ | { ok: false };
61
+
62
+ let inflight: Promise<RefreshResult> | null = null;
63
+
64
+ /**
65
+ * refreshToken 으로 새 accessToken / refreshToken 을 발급받는다.
66
+ *
67
+ * v1 placeholder — 백엔드 refresh API 명세가 확정되면 아래 TODO 부분을 채운다.
68
+ * 본문이 채워지면 BFF (/api/proxy) 와 withAuthRetry 가 자동으로 활용한다.
69
+ *
70
+ * 동시에 여러 요청이 401 을 만나도 inflight 모듈 변수로 코얼레싱돼서
71
+ * refresh 는 한 번만 발사된다.
72
+ *
73
+ * 참고 구현 예시:
74
+ *
75
+ * const res = await fetch(\`\${process.env.API_URL}/v1/auth/token/refresh\`, {
76
+ * method: 'POST',
77
+ * headers: { 'Content-Type': 'application/json' },
78
+ * body: JSON.stringify({ refreshToken }),
79
+ * });
80
+ * const body = await res.json();
81
+ * if (body.result === 'SUCCESS') {
82
+ * return {
83
+ * ok: true,
84
+ * accessToken: body.data.accessToken,
85
+ * refreshToken: body.data.refreshToken,
86
+ * };
87
+ * }
88
+ * return { ok: false };
89
+ */
90
+ export async function refreshSession(
91
+ refreshToken: string,
92
+ ): Promise<RefreshResult> {
93
+ if (inflight) return inflight;
94
+
95
+ inflight = (async (): Promise<RefreshResult> => {
96
+ try {
97
+ // TODO: 백엔드 refresh API 명세 확정 후 여기에 fetch 호출을 작성.
98
+ // 지금은 placeholder 라 항상 실패 → BFF 가 쿠키 삭제 + 401 응답.
99
+ void refreshToken;
100
+ return { ok: false };
101
+ } finally {
102
+ inflight = null;
103
+ }
104
+ })();
105
+
106
+ return inflight;
107
+ }
108
+ `,
109
+
110
+ 'src/shared/api/withAuthRetry.ts': `import { cookies } from 'next/headers';
111
+
112
+ import { ApiError } from './error';
113
+ import { refreshSession } from './refreshSession';
114
+
115
+ const ACCESS_TOKEN_COOKIE = 'accessToken';
116
+ const REFRESH_TOKEN_COOKIE = 'refreshToken';
117
+
118
+ const COOKIE = {
119
+ httpOnly: true,
120
+ secure: process.env.COOKIE_SECURE === 'true',
121
+ sameSite: 'lax' as const,
122
+ path: '/',
123
+ };
124
+
125
+ /**
126
+ * Route Handler 또는 Server Action 안에서 인증된 요청을 보낼 때 사용한다.
127
+ * 401 을 만나면 refreshSession 으로 토큰을 갱신하고 fn 을 한 번 더 실행한다.
128
+ *
129
+ * RSC (Server Component) 에서는 cookies().set() 이 막혀 있으므로 여기서 호출하면 안 된다.
130
+ * RSC 는 serverFetch 를 직접 호출하고, 401 은 prefetchQuery 가 swallow 한 뒤
131
+ * 클라이언트 refetch 가 BFF 경유로 자동 복구한다.
132
+ *
133
+ * 사용 예 (Server Action):
134
+ *
135
+ * 'use server';
136
+ * import { serverFetch } from '@/src/shared/api/serverFetch';
137
+ * import { withAuthRetry } from '@/src/shared/api/withAuthRetry';
138
+ *
139
+ * export async function toggleFavoriteAction(id: number) {
140
+ * return withAuthRetry(() =>
141
+ * serverFetch(\`/v1/products/\${id}/favorite\`, { method: 'POST' }),
142
+ * );
143
+ * }
144
+ */
145
+ export async function withAuthRetry<T>(fn: () => Promise<T>): Promise<T> {
146
+ try {
147
+ return await fn();
148
+ } catch (e) {
149
+ if (!(e instanceof ApiError) || e.status !== 401) throw e;
150
+
151
+ const jar = await cookies();
152
+ const refreshToken = jar.get(REFRESH_TOKEN_COOKIE)?.value;
153
+ if (!refreshToken) throw e;
154
+
155
+ const r = await refreshSession(refreshToken);
156
+ if (!r.ok) throw new ApiError(401, 'UNAUTHORIZED', null);
157
+
158
+ jar.set(ACCESS_TOKEN_COOKIE, r.accessToken, COOKIE);
159
+ jar.set(REFRESH_TOKEN_COOKIE, r.refreshToken, COOKIE);
160
+
161
+ return await fn();
162
+ }
163
+ }
164
+ `,
165
+
166
+ 'app/api/proxy/[...path]/route.ts': `import { cookies } from 'next/headers';
167
+ import { NextResponse, type NextRequest } from 'next/server';
168
+
169
+ import {
170
+ captureApiError,
171
+ logApiError,
172
+ } from '@/src/shared/api/observability';
173
+ import { refreshSession } from '@/src/shared/api/refreshSession';
174
+
175
+ const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
176
+ const ACCESS_TOKEN_COOKIE = 'accessToken';
177
+ const REFRESH_TOKEN_COOKIE = 'refreshToken';
178
+ const LOCALE_COOKIE = 'NEXT_LOCALE';
179
+
180
+ const COOKIE = {
181
+ httpOnly: true,
182
+ secure: process.env.COOKIE_SECURE === 'true',
183
+ sameSite: 'lax' as const,
184
+ path: '/',
185
+ };
186
+
187
+ const clearAuthCookies = (res: NextResponse) => {
188
+ res.cookies.set(ACCESS_TOKEN_COOKIE, '', { ...COOKIE, maxAge: 0 });
189
+ res.cookies.set(REFRESH_TOKEN_COOKIE, '', { ...COOKIE, maxAge: 0 });
190
+ return res;
191
+ };
192
+
193
+ const proxyRequest = async (
194
+ request: NextRequest,
195
+ ctx: { params: Promise<{ path: string[] }> },
196
+ method: string,
197
+ ) => {
198
+ const { path } = await ctx.params;
199
+ const apiPath = path.join('/');
200
+ const url = new URL(\`\${API_URL}/\${apiPath}\`);
201
+
202
+ request.nextUrl.searchParams.forEach((value, key) => {
203
+ url.searchParams.set(key, value);
204
+ });
205
+
206
+ const cookieStore = await cookies();
207
+ const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
208
+ const refreshToken = cookieStore.get(REFRESH_TOKEN_COOKIE)?.value;
209
+ const locale =
210
+ cookieStore.get(LOCALE_COOKIE)?.value ??
211
+ request.headers.get('Accept-Language') ??
212
+ undefined;
213
+
214
+ const headers: Record<string, string> = {};
215
+ if (accessToken) headers.Authorization = \`Bearer \${accessToken}\`;
216
+ if (locale) headers['Accept-Language'] = locale;
217
+
218
+ let body: BodyInit | undefined;
219
+ if (method !== 'GET' && method !== 'HEAD') {
220
+ const contentType = request.headers.get('Content-Type');
221
+ if (contentType?.includes('multipart/form-data')) {
222
+ body = await request.formData();
223
+ } else {
224
+ headers['Content-Type'] = 'application/json';
225
+ body = await request.text();
226
+ }
227
+ }
228
+
229
+ let response: Response;
230
+ try {
231
+ response = await fetch(url.toString(), { method, headers, body });
232
+ } catch (error) {
233
+ console.error(\`[PROXY] \${method} \${url.toString()} —\`, error);
234
+ return NextResponse.json(
235
+ {
236
+ result: 'ERROR',
237
+ data: null,
238
+ error: {
239
+ code: 'NETWORK_ERROR',
240
+ message: '서버에 연결할 수 없습니다.',
241
+ },
242
+ },
243
+ { status: 502 },
244
+ );
245
+ }
246
+
247
+ let data = await response.json();
248
+
249
+ // 401 → refresh 시도 (RT 가 있을 때만)
250
+ if (response.status === 401) {
251
+ if (!refreshToken) {
252
+ return clearAuthCookies(NextResponse.json(data, { status: 401 }));
253
+ }
254
+
255
+ const r = await refreshSession(refreshToken);
256
+
257
+ // refresh placeholder 가 본문 미구현이면 항상 ok:false
258
+ // → 쿠키 삭제 + 401 그대로 (clientFetch 가 /sign-in 으로 이동)
259
+ if (!r.ok) {
260
+ return clearAuthCookies(NextResponse.json(data, { status: 401 }));
261
+ }
262
+
263
+ // 새 AT 로 재시도
264
+ headers.Authorization = \`Bearer \${r.accessToken}\`;
265
+ const retry = await fetch(url.toString(), { method, headers, body });
266
+ data = await retry.json();
267
+
268
+ const res = NextResponse.json(data, { status: retry.status });
269
+ res.cookies.set(ACCESS_TOKEN_COOKIE, r.accessToken, COOKIE);
270
+ res.cookies.set(REFRESH_TOKEN_COOKIE, r.refreshToken, COOKIE);
271
+
272
+ if (!retry.ok) {
273
+ logApiError('PROXY', {
274
+ url: url.toString(),
275
+ method,
276
+ status: retry.status,
277
+ requestBody: typeof body === 'string' ? body : undefined,
278
+ responseBody: data,
279
+ });
280
+
281
+ captureApiError({
282
+ url: url.toString(),
283
+ apiPath,
284
+ method,
285
+ status: retry.status,
286
+ responseBody: data,
287
+ });
288
+ }
289
+
290
+ return res;
291
+ }
292
+
293
+ if (!response.ok) {
294
+ logApiError('PROXY', {
295
+ url: url.toString(),
296
+ method,
297
+ status: response.status,
298
+ requestBody: typeof body === 'string' ? body : undefined,
299
+ responseBody: data,
300
+ });
301
+
302
+ captureApiError({
303
+ url: url.toString(),
304
+ apiPath,
305
+ method,
306
+ status: response.status,
307
+ responseBody: data,
308
+ });
309
+ }
310
+
311
+ return NextResponse.json(data, { status: response.status });
312
+ };
313
+
314
+ export const GET = (
315
+ req: NextRequest,
316
+ ctx: { params: Promise<{ path: string[] }> },
317
+ ) => proxyRequest(req, ctx, 'GET');
318
+
319
+ export const POST = (
320
+ req: NextRequest,
321
+ ctx: { params: Promise<{ path: string[] }> },
322
+ ) => proxyRequest(req, ctx, 'POST');
323
+
324
+ export const PUT = (
325
+ req: NextRequest,
326
+ ctx: { params: Promise<{ path: string[] }> },
327
+ ) => proxyRequest(req, ctx, 'PUT');
328
+
329
+ export const PATCH = (
330
+ req: NextRequest,
331
+ ctx: { params: Promise<{ path: string[] }> },
332
+ ) => proxyRequest(req, ctx, 'PATCH');
333
+
334
+ export const DELETE = (
335
+ req: NextRequest,
336
+ ctx: { params: Promise<{ path: string[] }> },
337
+ ) => proxyRequest(req, ctx, 'DELETE');
338
+ `,
339
+ },
340
+ };
@@ -1,7 +1,8 @@
1
1
  import { sentryPlugin } from './sentry.js';
2
2
  import { nextIntlPlugin } from './nextIntl.js';
3
+ import { authJwtPlugin } from './authJwt.js';
3
4
 
4
- export const allPlugins = [sentryPlugin, nextIntlPlugin];
5
+ export const allPlugins = [sentryPlugin, nextIntlPlugin, authJwtPlugin];
5
6
 
6
7
  export function getPluginChoices() {
7
8
  return allPlugins.map((p) => ({