create-brainerce-store 1.27.6 → 1.28.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 (26) hide show
  1. package/dist/index.js +120 -22
  2. package/messages/en.json +389 -382
  3. package/messages/he.json +389 -382
  4. package/package.json +46 -46
  5. package/templates/nextjs/base/.env.local.ejs +3 -3
  6. package/templates/nextjs/base/next.config.ts +32 -31
  7. package/templates/nextjs/base/package.json.ejs +2 -1
  8. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -14
  9. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -59
  10. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -77
  11. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +229 -198
  12. package/templates/nextjs/base/src/app/checkout/page.tsx +3 -1
  13. package/templates/nextjs/base/src/app/layout.tsx.ejs +31 -13
  14. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +501 -501
  15. package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -131
  16. package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -232
  17. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -415
  18. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +656 -592
  19. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +88 -72
  20. package/templates/nextjs/base/src/lib/csrf.ts +11 -0
  21. package/templates/nextjs/base/src/lib/nonce.ts +10 -0
  22. package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -0
  23. package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -0
  24. package/templates/nextjs/base/src/lib/validation.ts +37 -0
  25. package/templates/nextjs/base/src/middleware.ts.ejs +103 -8
  26. package/templates/nextjs/base/tsconfig.tsbuildinfo +1 -0
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "create-brainerce-store",
3
- "version": "1.27.6",
4
- "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
- "bin": {
6
- "create-brainerce-store": "dist/index.js"
7
- },
8
- "files": [
9
- "dist",
10
- "templates",
11
- "messages"
12
- ],
13
- "scripts": {
14
- "build": "tsup src/index.ts --format cjs --dts --clean",
15
- "dev": "tsup src/index.ts --format cjs --dts --watch",
16
- "clean": "rimraf dist",
17
- "prepublishOnly": "npm run build"
18
- },
19
- "dependencies": {
20
- "chalk": "^4.1.2",
21
- "commander": "^12.1.0",
22
- "ejs": "^3.1.10",
23
- "fs-extra": "^11.2.0",
24
- "ora": "^5.4.1",
25
- "prompts": "^2.4.2"
26
- },
27
- "devDependencies": {
28
- "@types/ejs": "^3.1.5",
29
- "@types/fs-extra": "^11.0.4",
30
- "@types/prompts": "^2.4.9",
31
- "tsup": "^8.0.0",
32
- "typescript": "^5.4.0"
33
- },
34
- "engines": {
35
- "node": ">=18"
36
- },
37
- "keywords": [
38
- "brainerce",
39
- "ecommerce",
40
- "storefront",
41
- "scaffold",
42
- "create",
43
- "nextjs"
44
- ],
45
- "license": "MIT"
46
- }
1
+ {
2
+ "name": "create-brainerce-store",
3
+ "version": "1.28.1",
4
+ "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
+ "bin": {
6
+ "create-brainerce-store": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "templates",
11
+ "messages"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format cjs --dts --clean",
15
+ "dev": "tsup src/index.ts --format cjs --dts --watch",
16
+ "clean": "rimraf dist",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "chalk": "^4.1.2",
21
+ "commander": "^12.1.0",
22
+ "ejs": "^3.1.10",
23
+ "fs-extra": "^11.2.0",
24
+ "ora": "^5.4.1",
25
+ "prompts": "^2.4.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/ejs": "^3.1.5",
29
+ "@types/fs-extra": "^11.0.4",
30
+ "@types/prompts": "^2.4.9",
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.4.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "keywords": [
38
+ "brainerce",
39
+ "ecommerce",
40
+ "storefront",
41
+ "scaffold",
42
+ "create",
43
+ "nextjs"
44
+ ],
45
+ "license": "MIT"
46
+ }
@@ -2,11 +2,11 @@
2
2
  NEXT_PUBLIC_BRAINERCE_CONNECTION_ID=<%= connectionId %>
3
3
 
4
4
  # Store info (pre-fetched during setup to avoid flash on first load)
5
- NEXT_PUBLIC_STORE_NAME=<%= storeName %>
6
- NEXT_PUBLIC_STORE_CURRENCY=<%= currency %>
5
+ NEXT_PUBLIC_STORE_NAME=<%- storeNameEnv %>
6
+ NEXT_PUBLIC_STORE_CURRENCY=<%- currencyEnv %>
7
7
 
8
8
  # Backend API URL (server-side only — used by BFF proxy and SSR, never exposed to browser)
9
- BRAINERCE_API_URL=<%= apiBaseUrl %>
9
+ BRAINERCE_API_URL=<%- apiBaseUrlEnv %>
10
10
 
11
11
  # Public site URL — used for sitemap, robots.txt, and SEO metadata
12
12
  NEXT_PUBLIC_SITE_URL=http://localhost:3000
@@ -1,31 +1,32 @@
1
- import type { NextConfig } from 'next';
2
-
3
- const nextConfig: NextConfig = {
4
- images: {
5
- remotePatterns: [{ protocol: 'https', hostname: '**' }],
6
- },
7
- async headers() {
8
- return [
9
- {
10
- source: '/(.*)',
11
- headers: [
12
- {
13
- key: 'Content-Security-Policy',
14
- value: [
15
- "default-src 'self'",
16
- "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.meshulam.co.il https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://js.stripe.com https://pay.google.com",
17
- "style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
18
- "img-src 'self' data: blob: https:",
19
- "font-src 'self' data:",
20
- "frame-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions",
21
- "connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://google.com https://pay.google.com https://*.stripe.com https://*.creditguard.co.il",
22
- "worker-src 'self' blob:",
23
- ].join('; '),
24
- },
25
- ],
26
- },
27
- ];
28
- },
29
- };
30
-
31
- export default nextConfig;
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {
4
+ images: {
5
+ remotePatterns: [
6
+ { protocol: 'https', hostname: 'cdn.brainerce.com' },
7
+ { protocol: 'https', hostname: '*.brainerce.com' },
8
+ ],
9
+ },
10
+ async headers() {
11
+ return [
12
+ {
13
+ source: '/(.*)',
14
+ headers: [
15
+ {
16
+ key: 'Strict-Transport-Security',
17
+ value: 'max-age=63072000; includeSubDomains; preload',
18
+ },
19
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
20
+ { key: 'X-Frame-Options', value: 'DENY' },
21
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
22
+ {
23
+ key: 'Permissions-Policy',
24
+ value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
25
+ },
26
+ ],
27
+ },
28
+ ];
29
+ },
30
+ };
31
+
32
+ export default nextConfig;
@@ -15,7 +15,8 @@
15
15
  "react": "^19.0.0",
16
16
  "react-dom": "^19.0.0",
17
17
  "clsx": "^2.1.0",
18
- "tailwind-merge": "^2.2.0"
18
+ "tailwind-merge": "^2.2.0",
19
+ "isomorphic-dompurify": "^3.8.0"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@types/node": "^20.0.0",
@@ -1,14 +1,15 @@
1
- import { NextResponse } from 'next/server';
2
-
3
- const TOKEN_COOKIE = 'brainerce_customer_token';
4
- const LOGGED_IN_COOKIE = 'brainerce_logged_in';
5
-
6
- /**
7
- * Logout endpoint. Clears auth cookies.
8
- */
9
- export async function POST() {
10
- const response = NextResponse.json({ success: true });
11
- response.cookies.delete(TOKEN_COOKIE);
12
- response.cookies.delete(LOGGED_IN_COOKIE);
13
- return response;
14
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { checkCsrf } from '@/lib/csrf';
3
+
4
+ const TOKEN_COOKIE = 'brainerce_customer_token';
5
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
6
+
7
+ export async function POST(request: NextRequest) {
8
+ const csrfError = checkCsrf(request);
9
+ if (csrfError) return csrfError;
10
+
11
+ const response = NextResponse.json({ success: true });
12
+ response.cookies.delete(TOKEN_COOKIE);
13
+ response.cookies.delete(LOGGED_IN_COOKIE);
14
+ return response;
15
+ }
@@ -1,59 +1,66 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
-
3
- const TOKEN_COOKIE = 'brainerce_customer_token';
4
- const LOGGED_IN_COOKIE = 'brainerce_logged_in';
5
- const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
6
-
7
- function isSecure(): boolean {
8
- return process.env.NODE_ENV === 'production';
9
- }
10
-
11
- /**
12
- * OAuth callback handler.
13
- * The backend redirects here with ?token=jwt&oauth_success=true after OAuth code exchange.
14
- * We set the httpOnly cookie and redirect to the client-side callback page (without the token).
15
- */
16
- export async function GET(request: NextRequest) {
17
- const { searchParams } = request.nextUrl;
18
- const token = searchParams.get('token');
19
- const oauthSuccess = searchParams.get('oauth_success');
20
- const oauthError = searchParams.get('oauth_error');
21
-
22
- // Build redirect URL to client-side callback page
23
- const redirectUrl = new URL('/auth/callback', request.url);
24
-
25
- if (oauthError) {
26
- redirectUrl.searchParams.set('oauth_error', oauthError);
27
- return NextResponse.redirect(redirectUrl);
28
- }
29
-
30
- if (oauthSuccess === 'true' && token) {
31
- redirectUrl.searchParams.set('oauth_success', 'true');
32
-
33
- const response = NextResponse.redirect(redirectUrl);
34
-
35
- // Set httpOnly cookie with the token
36
- response.cookies.set(TOKEN_COOKIE, token, {
37
- httpOnly: true,
38
- secure: isSecure(),
39
- sameSite: 'lax',
40
- path: '/',
41
- maxAge: COOKIE_MAX_AGE,
42
- });
43
-
44
- // Set indicator cookie (readable by client JS)
45
- response.cookies.set(LOGGED_IN_COOKIE, '1', {
46
- httpOnly: false,
47
- secure: isSecure(),
48
- sameSite: 'lax',
49
- path: '/',
50
- maxAge: COOKIE_MAX_AGE,
51
- });
52
-
53
- return response;
54
- }
55
-
56
- // Fallback: no token or success flag
57
- redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
58
- return NextResponse.redirect(redirectUrl);
59
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ const TOKEN_COOKIE = 'brainerce_customer_token';
4
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
5
+ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
6
+
7
+ function isSecure(): boolean {
8
+ return process.env.NODE_ENV === 'production';
9
+ }
10
+
11
+ /**
12
+ * OAuth callback handler.
13
+ * The backend redirects here with ?token=jwt&oauth_success=true after OAuth code exchange.
14
+ * We set the httpOnly cookie and redirect to the client-side callback page (without the token).
15
+ */
16
+ export async function GET(request: NextRequest) {
17
+ const { searchParams } = request.nextUrl;
18
+ const token = searchParams.get('token');
19
+ const oauthSuccess = searchParams.get('oauth_success');
20
+ const oauthError = searchParams.get('oauth_error');
21
+
22
+ // Build redirect URL to client-side callback page
23
+ const redirectUrl = new URL('/auth/callback', request.url);
24
+
25
+ if (oauthError) {
26
+ redirectUrl.searchParams.set('oauth_error', oauthError);
27
+ const response = NextResponse.redirect(redirectUrl);
28
+ response.headers.set('Referrer-Policy', 'no-referrer');
29
+ return response;
30
+ }
31
+
32
+ if (oauthSuccess === 'true' && token) {
33
+ redirectUrl.searchParams.set('oauth_success', 'true');
34
+
35
+ const response = NextResponse.redirect(redirectUrl);
36
+
37
+ // Set httpOnly cookie with the token
38
+ response.cookies.set(TOKEN_COOKIE, token, {
39
+ httpOnly: true,
40
+ secure: isSecure(),
41
+ sameSite: 'lax',
42
+ path: '/',
43
+ maxAge: COOKIE_MAX_AGE,
44
+ });
45
+
46
+ // Set indicator cookie (readable by client JS)
47
+ response.cookies.set(LOGGED_IN_COOKIE, '1', {
48
+ httpOnly: false,
49
+ secure: isSecure(),
50
+ sameSite: 'lax',
51
+ path: '/',
52
+ maxAge: COOKIE_MAX_AGE,
53
+ });
54
+
55
+ // Prevent token leaking via Referer header on the downstream navigation
56
+ response.headers.set('Referrer-Policy', 'no-referrer');
57
+
58
+ return response;
59
+ }
60
+
61
+ // Fallback: no token or success flag
62
+ redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
63
+ const response = NextResponse.redirect(redirectUrl);
64
+ response.headers.set('Referrer-Policy', 'no-referrer');
65
+ return response;
66
+ }
@@ -1,77 +1,76 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { cookies, headers } from 'next/headers';
3
-
4
- const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
5
- /\/$/,
6
- ''
7
- );
8
-
9
- const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
10
-
11
- const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
12
- const CSRF_HEADER = 'x-requested-with';
13
- const CSRF_VALUE = 'brainerce';
14
-
15
- /**
16
- * BFF endpoint for password reset.
17
- * Reads the reset token from the httpOnly cookie (set by /api/auth/reset-callback)
18
- * and proxies the request to the backend. The token never touches client JS.
19
- */
20
- export async function POST(request: NextRequest) {
21
- // CSRF check
22
- const csrfHeader = request.headers.get(CSRF_HEADER);
23
- if (csrfHeader !== CSRF_VALUE) {
24
- return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
25
- }
26
-
27
- // Read reset token from httpOnly cookie
28
- const cookieStore = await cookies();
29
- const resetTokenCookie = cookieStore.get(RESET_TOKEN_COOKIE);
30
-
31
- if (!resetTokenCookie?.value) {
32
- return NextResponse.json(
33
- { error: 'No reset token found. Please request a new password reset link.' },
34
- { status: 400 }
35
- );
36
- }
37
-
38
- // Parse request body
39
- const body = await request.json();
40
- const { newPassword } = body;
41
-
42
- if (!newPassword) {
43
- return NextResponse.json({ error: 'New password is required' }, { status: 400 });
44
- }
45
-
46
- // Derive Origin from the incoming request so the backend's BrowserOriginGuard accepts it
47
- const requestHeaders = await headers();
48
- const host = requestHeaders.get('host') || 'localhost:3000';
49
- const proto = requestHeaders.get('x-forwarded-proto') || 'http';
50
- const origin = requestHeaders.get('origin') || `${proto}://${host}`;
51
-
52
- // Proxy to backend
53
- const backendUrl = `${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/reset-password`;
54
-
55
- const backendResponse = await fetch(backendUrl, {
56
- method: 'POST',
57
- headers: {
58
- 'Content-Type': 'application/json',
59
- Origin: origin,
60
- },
61
- body: JSON.stringify({
62
- token: resetTokenCookie.value,
63
- newPassword,
64
- }),
65
- });
66
-
67
- const data = await backendResponse.json();
68
-
69
- const response = NextResponse.json(data, {
70
- status: backendResponse.status,
71
- });
72
-
73
- // Always clear the reset token cookie after use (success or failure)
74
- response.cookies.delete(RESET_TOKEN_COOKIE);
75
-
76
- return response;
77
- }
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { cookies } from 'next/headers';
3
+ import { validatePassword } from '@/lib/validation';
4
+ import { checkCsrf } from '@/lib/csrf';
5
+
6
+ const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
7
+ /\/$/,
8
+ ''
9
+ );
10
+
11
+ const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
12
+
13
+ const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
14
+
15
+ /**
16
+ * BFF endpoint for password reset.
17
+ * Reads the reset token from the httpOnly cookie (set by /api/auth/reset-callback)
18
+ * and proxies the request to the backend. The token never touches client JS.
19
+ */
20
+ export async function POST(request: NextRequest) {
21
+ const csrfError = checkCsrf(request);
22
+ if (csrfError) return csrfError;
23
+
24
+ // Read reset token from httpOnly cookie
25
+ const cookieStore = await cookies();
26
+ const resetTokenCookie = cookieStore.get(RESET_TOKEN_COOKIE);
27
+
28
+ if (!resetTokenCookie?.value) {
29
+ return NextResponse.json(
30
+ { error: 'No reset token found. Please request a new password reset link.' },
31
+ { status: 400 }
32
+ );
33
+ }
34
+
35
+ // Parse request body
36
+ const body = await request.json();
37
+ const { newPassword } = body;
38
+
39
+ const passwordError = validatePassword(newPassword);
40
+ if (passwordError) {
41
+ return NextResponse.json({ error: passwordError }, { status: 400 });
42
+ }
43
+
44
+ // Proxy to backend
45
+ const backendUrl = `${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/reset-password`;
46
+
47
+ const backendResponse = await fetch(backendUrl, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ Origin: request.nextUrl.origin,
52
+ },
53
+ body: JSON.stringify({
54
+ token: resetTokenCookie.value,
55
+ newPassword,
56
+ }),
57
+ });
58
+
59
+ let data: unknown;
60
+ try {
61
+ data = await backendResponse.json();
62
+ } catch {
63
+ const response = NextResponse.json({ error: 'Invalid response from backend' }, { status: 502 });
64
+ response.cookies.delete(RESET_TOKEN_COOKIE);
65
+ return response;
66
+ }
67
+
68
+ const response = NextResponse.json(data, {
69
+ status: backendResponse.status,
70
+ });
71
+
72
+ // Always clear the reset token cookie after use (success or failure)
73
+ response.cookies.delete(RESET_TOKEN_COOKIE);
74
+
75
+ return response;
76
+ }