create-brainerce-store 1.66.0 → 1.68.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.
Files changed (47) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/nextjs/base/.env.local.ejs +52 -24
  4. package/templates/nextjs/base/.eslintrc.json +51 -57
  5. package/templates/nextjs/base/AGENTS.md.ejs +114 -81
  6. package/templates/nextjs/base/CLAUDE.md.ejs +125 -92
  7. package/templates/nextjs/base/next.config.ts +103 -86
  8. package/templates/nextjs/base/scripts/fetch-store-info.mjs +104 -97
  9. package/templates/nextjs/base/src/app/agents.md/route.ts +4 -3
  10. package/templates/nextjs/base/src/app/api/auth/me/route.ts +65 -59
  11. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +255 -242
  12. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +317 -308
  13. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +277 -276
  14. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +4 -3
  15. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +22 -12
  16. package/templates/nextjs/base/src/app/error.tsx.ejs +53 -0
  17. package/templates/nextjs/base/src/app/faq/page.tsx.ejs +46 -46
  18. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +25 -26
  19. package/templates/nextjs/base/src/app/layout.tsx.ejs +273 -257
  20. package/templates/nextjs/base/src/app/llms.txt/route.ts +4 -3
  21. package/templates/nextjs/base/src/app/opengraph-image.tsx +1 -1
  22. package/templates/nextjs/base/src/app/page.tsx +65 -64
  23. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +98 -92
  24. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +22 -11
  25. package/templates/nextjs/base/src/app/robots.ts +3 -2
  26. package/templates/nextjs/base/src/app/sitemap.ts +4 -3
  27. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +294 -292
  28. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +60 -59
  29. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +60 -61
  30. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +2 -1
  31. package/templates/nextjs/base/src/core/lib/brainerce.server.ts +81 -0
  32. package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +60 -110
  33. package/templates/nextjs/base/src/core/lib/site-url.ts +230 -0
  34. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  35. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  36. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +301 -285
  37. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +97 -96
  38. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +142 -141
  39. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +139 -138
  40. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +295 -267
  41. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  42. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +72 -71
  43. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +83 -82
  44. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +120 -119
  45. package/templates/nextjs/ui-canvas/product/faq-section.tsx.ejs +54 -0
  46. package/templates/nextjs/ui-canvas/product/review-form.tsx +267 -266
  47. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
@@ -1,242 +1,255 @@
1
- // SECURITY: This BFF proxy intentionally has no application-level rate limiting.
2
- // Rate limiting is the deployer's responsibility — configure it at the platform
3
- // edge (Vercel Firewall, Cloudflare, nginx) or add a Redis-backed limiter
4
- // (e.g. @upstash/ratelimit) here before going to production. Auth endpoints
5
- // like customers/login and customers/register are the most important to cover.
6
- import { NextRequest, NextResponse } from 'next/server';
7
- import { cookies } from 'next/headers';
8
- import { checkCsrf } from '@/core/lib/csrf';
9
-
10
- const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
11
- /\/$/,
12
- ''
13
- );
14
-
15
- const TOKEN_COOKIE = 'brainerce_customer_token';
16
- const LOGGED_IN_COOKIE = 'brainerce_logged_in';
17
-
18
- const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
19
- const BACKEND_TIMEOUT_MS = 15_000;
20
-
21
- /** Auth endpoints whose responses contain tokens to intercept */
22
- const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
23
-
24
- function isAuthEndpoint(path: string): boolean {
25
- return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
26
- }
27
-
28
- function isSafePathSegment(segment: string): boolean {
29
- if (!segment) return false;
30
- if (segment === '.' || segment === '..') return false;
31
- if (segment.includes('/') || segment.includes('\\')) return false;
32
- if (segment.includes('\0')) return false;
33
- return true;
34
- }
35
-
36
- function isSecure(): boolean {
37
- return process.env.NODE_ENV === 'production';
38
- }
39
-
40
- function setAuthCookies(response: NextResponse, token: string): void {
41
- response.cookies.set(TOKEN_COOKIE, token, {
42
- httpOnly: true,
43
- secure: isSecure(),
44
- sameSite: 'lax',
45
- path: '/',
46
- maxAge: COOKIE_MAX_AGE,
47
- });
48
- response.cookies.set(LOGGED_IN_COOKIE, '1', {
49
- httpOnly: false,
50
- secure: isSecure(),
51
- sameSite: 'lax',
52
- path: '/',
53
- maxAge: COOKIE_MAX_AGE,
54
- });
55
- }
56
-
57
- function clearAuthCookies(response: NextResponse): void {
58
- response.cookies.delete(TOKEN_COOKIE);
59
- response.cookies.delete(LOGGED_IN_COOKIE);
60
- }
61
-
62
- async function proxyRequest(
63
- request: NextRequest,
64
- params: { path: string[] }
65
- ): Promise<NextResponse> {
66
- const method = request.method;
67
-
68
- // Reject path-traversal attempts before constructing the backend URL
69
- if (!params.path.every(isSafePathSegment)) {
70
- return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
71
- }
72
-
73
- // CSRF protection for mutating requests
74
- if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
75
- const csrfError = checkCsrf(request);
76
- if (csrfError) return csrfError;
77
- }
78
-
79
- // Build backend URL from path segments
80
- const pathSegments = params.path.join('/');
81
- const backendUrl = new URL(`${BACKEND_URL}/${pathSegments}`);
82
-
83
- // Forward query parameters
84
- request.nextUrl.searchParams.forEach((value, key) => {
85
- backendUrl.searchParams.set(key, value);
86
- });
87
-
88
- // Build headers for backend request. Preserve the incoming Content-Type
89
- // (including the multipart boundary for file uploads); default to JSON
90
- // when absent. Overriding the Content-Type would strip the boundary and
91
- // corrupt multipart uploads on the backend.
92
- const incomingContentType = request.headers.get('content-type');
93
- const headers: Record<string, string> = {
94
- 'Content-Type': incomingContentType || 'application/json',
95
- };
96
-
97
- // Send the proxy's own origin (not the client-supplied Origin header).
98
- // The backend's BrowserOriginGuard only checks for presence of Origin/Referer,
99
- // so forwarding a client-controlled value adds spoofing surface for nothing.
100
- headers['Origin'] = request.nextUrl.origin;
101
-
102
- // Forward SDK version header if present
103
- const sdkVersion = request.headers.get('x-sdk-version');
104
- if (sdkVersion) {
105
- headers['X-SDK-Version'] = sdkVersion;
106
- }
107
-
108
- // Forward Accept-Language so the backend locale middleware can resolve translations
109
- const acceptLanguage = request.headers.get('accept-language');
110
- if (acceptLanguage) {
111
- headers['Accept-Language'] = acceptLanguage;
112
- }
113
-
114
- // Add auth token from httpOnly cookie
115
- const cookieStore = await cookies();
116
- const tokenCookie = cookieStore.get(TOKEN_COOKIE);
117
- if (tokenCookie?.value) {
118
- headers['Authorization'] = `Bearer ${tokenCookie.value}`;
119
- }
120
-
121
- // Forward request body for non-GET requests. Use ArrayBuffer so multipart
122
- // bodies (file uploads) pass through unchanged — `request.text()` works for
123
- // JSON but would break binary content.
124
- const isMultipart = incomingContentType?.includes('multipart/form-data') ?? false;
125
- let body: ArrayBuffer | string | undefined;
126
- if (method !== 'GET' && method !== 'HEAD') {
127
- try {
128
- body = isMultipart ? await request.arrayBuffer() : await request.text();
129
- } catch {
130
- // No body
131
- }
132
- }
133
-
134
- // Proxy the request to backend
135
- let backendResponse: Response;
136
- const abortController = new AbortController();
137
- const timeoutId = setTimeout(() => abortController.abort(), BACKEND_TIMEOUT_MS);
138
- try {
139
- backendResponse = await fetch(backendUrl.toString(), {
140
- method,
141
- headers,
142
- body,
143
- signal: abortController.signal,
144
- });
145
- } catch (error) {
146
- const isTimeout = (error as Error)?.name === 'AbortError';
147
- return NextResponse.json(
148
- { error: isTimeout ? 'Backend request timed out' : 'Backend service unavailable' },
149
- { status: isTimeout ? 504 : 502 }
150
- );
151
- } finally {
152
- clearTimeout(timeoutId);
153
- }
154
-
155
- // Read response body
156
- const responseText = await backendResponse.text();
157
-
158
- // For auth endpoints: intercept token, set cookie, strip token from response
159
- if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
160
- try {
161
- const data = JSON.parse(responseText);
162
- if (data.token) {
163
- const token = data.token;
164
-
165
- // Strip token from client response
166
- const { token: _stripped, ...safeData } = data;
167
-
168
- const response = NextResponse.json(safeData, {
169
- status: backendResponse.status,
170
- });
171
- setAuthCookies(response, token);
172
- return response;
173
- }
174
- } catch {
175
- // Not JSON or no token field — pass through
176
- }
177
- }
178
-
179
- // Handle 401 responses: clear auth cookies
180
- if (backendResponse.status === 401 && tokenCookie?.value) {
181
- const response = new NextResponse(responseText, {
182
- status: backendResponse.status,
183
- headers: {
184
- 'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
185
- },
186
- });
187
- clearAuthCookies(response);
188
- return response;
189
- }
190
-
191
- // Sanitize 5xx responses so backend internals don't leak to the client
192
- if (backendResponse.status >= 500) {
193
- console.error(`[proxy] backend ${backendResponse.status} on ${pathSegments}:`, responseText);
194
- return NextResponse.json(
195
- { error: 'Backend service error' },
196
- { status: backendResponse.status }
197
- );
198
- }
199
-
200
- // Pass through response as-is
201
- return new NextResponse(responseText, {
202
- status: backendResponse.status,
203
- headers: {
204
- 'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
205
- },
206
- });
207
- }
208
-
209
- export async function GET(
210
- request: NextRequest,
211
- { params }: { params: Promise<{ path: string[] }> }
212
- ) {
213
- return proxyRequest(request, await params);
214
- }
215
-
216
- export async function POST(
217
- request: NextRequest,
218
- { params }: { params: Promise<{ path: string[] }> }
219
- ) {
220
- return proxyRequest(request, await params);
221
- }
222
-
223
- export async function PUT(
224
- request: NextRequest,
225
- { params }: { params: Promise<{ path: string[] }> }
226
- ) {
227
- return proxyRequest(request, await params);
228
- }
229
-
230
- export async function PATCH(
231
- request: NextRequest,
232
- { params }: { params: Promise<{ path: string[] }> }
233
- ) {
234
- return proxyRequest(request, await params);
235
- }
236
-
237
- export async function DELETE(
238
- request: NextRequest,
239
- { params }: { params: Promise<{ path: string[] }> }
240
- ) {
241
- return proxyRequest(request, await params);
242
- }
1
+ // SECURITY: This BFF proxy intentionally has no application-level rate limiting.
2
+ // Rate limiting is the deployer's responsibility — configure it at the platform
3
+ // edge (Vercel Firewall, Cloudflare, nginx) or add a Redis-backed limiter
4
+ // (e.g. @upstash/ratelimit) here before going to production. Auth endpoints
5
+ // like customers/login and customers/register are the most important to cover.
6
+ import { NextRequest, NextResponse } from 'next/server';
7
+ import { cookies } from 'next/headers';
8
+ import { checkCsrf } from '@/core/lib/csrf';
9
+ import { getRequestOrigin } from '@/core/lib/site-url';
10
+
11
+ const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
12
+ /\/$/,
13
+ ''
14
+ );
15
+
16
+ const TOKEN_COOKIE = 'brainerce_customer_token';
17
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
18
+
19
+ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
20
+ const BACKEND_TIMEOUT_MS = 15_000;
21
+
22
+ /** Auth endpoints whose responses contain tokens to intercept */
23
+ const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
24
+
25
+ function isAuthEndpoint(path: string): boolean {
26
+ return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
27
+ }
28
+
29
+ function isSafePathSegment(segment: string): boolean {
30
+ if (!segment) return false;
31
+ if (segment === '.' || segment === '..') return false;
32
+ if (segment.includes('/') || segment.includes('\\')) return false;
33
+ if (segment.includes('\0')) return false;
34
+ return true;
35
+ }
36
+
37
+ function isSecure(): boolean {
38
+ return process.env.NODE_ENV === 'production';
39
+ }
40
+
41
+ function setAuthCookies(response: NextResponse, token: string): void {
42
+ response.cookies.set(TOKEN_COOKIE, token, {
43
+ httpOnly: true,
44
+ secure: isSecure(),
45
+ sameSite: 'lax',
46
+ path: '/',
47
+ maxAge: COOKIE_MAX_AGE,
48
+ });
49
+ response.cookies.set(LOGGED_IN_COOKIE, '1', {
50
+ httpOnly: false,
51
+ secure: isSecure(),
52
+ sameSite: 'lax',
53
+ path: '/',
54
+ maxAge: COOKIE_MAX_AGE,
55
+ });
56
+ }
57
+
58
+ function clearAuthCookies(response: NextResponse): void {
59
+ response.cookies.delete(TOKEN_COOKIE);
60
+ response.cookies.delete(LOGGED_IN_COOKIE);
61
+ }
62
+
63
+ async function proxyRequest(
64
+ request: NextRequest,
65
+ params: { path: string[] }
66
+ ): Promise<NextResponse> {
67
+ const method = request.method;
68
+
69
+ // Reject path-traversal attempts before constructing the backend URL
70
+ if (!params.path.every(isSafePathSegment)) {
71
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
72
+ }
73
+
74
+ // CSRF protection for mutating requests
75
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
76
+ const csrfError = checkCsrf(request);
77
+ if (csrfError) return csrfError;
78
+ }
79
+
80
+ // Build backend URL from path segments
81
+ const pathSegments = params.path.join('/');
82
+ const backendUrl = new URL(`${BACKEND_URL}/${pathSegments}`);
83
+
84
+ // Forward query parameters
85
+ request.nextUrl.searchParams.forEach((value, key) => {
86
+ backendUrl.searchParams.set(key, value);
87
+ });
88
+
89
+ // Build headers for backend request. Preserve the incoming Content-Type
90
+ // (including the multipart boundary for file uploads); default to JSON
91
+ // when absent. Overriding the Content-Type would strip the boundary and
92
+ // corrupt multipart uploads on the backend.
93
+ const incomingContentType = request.headers.get('content-type');
94
+ const headers: Record<string, string> = {
95
+ 'Content-Type': incomingContentType || 'application/json',
96
+ };
97
+
98
+ // Send the proxy's own origin, never the client-supplied Origin header —
99
+ // forwarding a client-controlled value would just hand a caller the ability
100
+ // to pick which storefront the backend thinks it is talking to.
101
+ //
102
+ // The backend READS this value: a LIVE sales channel accepts only the
103
+ // domain configured on the connection (exact host or a subdomain of it) plus
104
+ // any additional allowed origins. It is not a presence check. So this header
105
+ // has to carry the storefront's PUBLIC origin.
106
+ //
107
+ // `request.nextUrl.origin` alone is not that origin: behind a reverse proxy
108
+ // it can be an internal address (`http://localhost:3000`, a container host),
109
+ // and every storefront call then fails on a Live connection. A TEST
110
+ // connection with no domain configured accepts anything, which is why this
111
+ // used to surface only at go-live. `getRequestOrigin` prefers the forwarded
112
+ // host the browser actually used and falls back to configuration.
113
+ headers['Origin'] = await getRequestOrigin(request.headers);
114
+
115
+ // Forward SDK version header if present
116
+ const sdkVersion = request.headers.get('x-sdk-version');
117
+ if (sdkVersion) {
118
+ headers['X-SDK-Version'] = sdkVersion;
119
+ }
120
+
121
+ // Forward Accept-Language so the backend locale middleware can resolve translations
122
+ const acceptLanguage = request.headers.get('accept-language');
123
+ if (acceptLanguage) {
124
+ headers['Accept-Language'] = acceptLanguage;
125
+ }
126
+
127
+ // Add auth token from httpOnly cookie
128
+ const cookieStore = await cookies();
129
+ const tokenCookie = cookieStore.get(TOKEN_COOKIE);
130
+ if (tokenCookie?.value) {
131
+ headers['Authorization'] = `Bearer ${tokenCookie.value}`;
132
+ }
133
+
134
+ // Forward request body for non-GET requests. Use ArrayBuffer so multipart
135
+ // bodies (file uploads) pass through unchanged — `request.text()` works for
136
+ // JSON but would break binary content.
137
+ const isMultipart = incomingContentType?.includes('multipart/form-data') ?? false;
138
+ let body: ArrayBuffer | string | undefined;
139
+ if (method !== 'GET' && method !== 'HEAD') {
140
+ try {
141
+ body = isMultipart ? await request.arrayBuffer() : await request.text();
142
+ } catch {
143
+ // No body
144
+ }
145
+ }
146
+
147
+ // Proxy the request to backend
148
+ let backendResponse: Response;
149
+ const abortController = new AbortController();
150
+ const timeoutId = setTimeout(() => abortController.abort(), BACKEND_TIMEOUT_MS);
151
+ try {
152
+ backendResponse = await fetch(backendUrl.toString(), {
153
+ method,
154
+ headers,
155
+ body,
156
+ signal: abortController.signal,
157
+ });
158
+ } catch (error) {
159
+ const isTimeout = (error as Error)?.name === 'AbortError';
160
+ return NextResponse.json(
161
+ { error: isTimeout ? 'Backend request timed out' : 'Backend service unavailable' },
162
+ { status: isTimeout ? 504 : 502 }
163
+ );
164
+ } finally {
165
+ clearTimeout(timeoutId);
166
+ }
167
+
168
+ // Read response body
169
+ const responseText = await backendResponse.text();
170
+
171
+ // For auth endpoints: intercept token, set cookie, strip token from response
172
+ if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
173
+ try {
174
+ const data = JSON.parse(responseText);
175
+ if (data.token) {
176
+ const token = data.token;
177
+
178
+ // Strip token from client response
179
+ const { token: _stripped, ...safeData } = data;
180
+
181
+ const response = NextResponse.json(safeData, {
182
+ status: backendResponse.status,
183
+ });
184
+ setAuthCookies(response, token);
185
+ return response;
186
+ }
187
+ } catch {
188
+ // Not JSON or no token field — pass through
189
+ }
190
+ }
191
+
192
+ // Handle 401 responses: clear auth cookies
193
+ if (backendResponse.status === 401 && tokenCookie?.value) {
194
+ const response = new NextResponse(responseText, {
195
+ status: backendResponse.status,
196
+ headers: {
197
+ 'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
198
+ },
199
+ });
200
+ clearAuthCookies(response);
201
+ return response;
202
+ }
203
+
204
+ // Sanitize 5xx responses so backend internals don't leak to the client
205
+ if (backendResponse.status >= 500) {
206
+ console.error(`[proxy] backend ${backendResponse.status} on ${pathSegments}:`, responseText);
207
+ return NextResponse.json(
208
+ { error: 'Backend service error' },
209
+ { status: backendResponse.status }
210
+ );
211
+ }
212
+
213
+ // Pass through response as-is
214
+ return new NextResponse(responseText, {
215
+ status: backendResponse.status,
216
+ headers: {
217
+ 'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
218
+ },
219
+ });
220
+ }
221
+
222
+ export async function GET(
223
+ request: NextRequest,
224
+ { params }: { params: Promise<{ path: string[] }> }
225
+ ) {
226
+ return proxyRequest(request, await params);
227
+ }
228
+
229
+ export async function POST(
230
+ request: NextRequest,
231
+ { params }: { params: Promise<{ path: string[] }> }
232
+ ) {
233
+ return proxyRequest(request, await params);
234
+ }
235
+
236
+ export async function PUT(
237
+ request: NextRequest,
238
+ { params }: { params: Promise<{ path: string[] }> }
239
+ ) {
240
+ return proxyRequest(request, await params);
241
+ }
242
+
243
+ export async function PATCH(
244
+ request: NextRequest,
245
+ { params }: { params: Promise<{ path: string[] }> }
246
+ ) {
247
+ return proxyRequest(request, await params);
248
+ }
249
+
250
+ export async function DELETE(
251
+ request: NextRequest,
252
+ { params }: { params: Promise<{ path: string[] }> }
253
+ ) {
254
+ return proxyRequest(request, await params);
255
+ }