create-brainerce-store 1.28.13 → 1.28.17

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 (24) hide show
  1. package/dist/index.js +1 -1
  2. package/messages/en.json +390 -389
  3. package/messages/he.json +390 -389
  4. package/package.json +46 -46
  5. package/templates/nextjs/base/next.config.ts +47 -47
  6. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -15
  7. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -66
  8. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -76
  9. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +235 -229
  10. package/templates/nextjs/base/src/app/checkout/page.tsx +975 -975
  11. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +73 -76
  12. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +529 -501
  13. package/templates/nextjs/base/src/app/products/page.tsx +475 -482
  14. package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -138
  15. package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -245
  16. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -416
  17. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +656 -656
  18. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +88 -88
  19. package/templates/nextjs/base/src/lib/brainerce.ts.ejs +6 -2
  20. package/templates/nextjs/base/src/lib/csrf.ts +11 -11
  21. package/templates/nextjs/base/src/lib/nonce.ts +10 -10
  22. package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -45
  23. package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -93
  24. package/templates/nextjs/base/src/lib/validation.ts +37 -37
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "create-brainerce-store",
3
- "version": "1.28.13",
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.17",
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,47 +1,47 @@
1
- import type { NextConfig } from 'next';
2
-
3
- const nextConfig: NextConfig = {
4
- // isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files
5
- // from its own package directory. Webpack bundling breaks those relative
6
- // lookups — loading it externally from node_modules keeps the paths intact.
7
- serverExternalPackages: ['isomorphic-dompurify'],
8
- images: {
9
- // The storefront is a consumer of the Brainerce API — it has to render
10
- // whatever image URLs the API returns. In practice those URLs can be on
11
- // cdn.brainerce.com OR on an upstream merchant host (WooCommerce, Shopify,
12
- // self-hosted) depending on whether the product's image-import job has
13
- // completed on the backend. Rather than hard-fail on unknown hosts, skip
14
- // the server-side optimizer entirely and let the browser fetch each image
15
- // directly from origin. No server-side fetching → no SSRF or DoS surface
16
- // on this Next server. Trade-off: no webp/resize/lazy optimization, so
17
- // LCP is marginally worse. Acceptable; the storefront is not the right
18
- // layer to enforce a hostname policy.
19
- unoptimized: true,
20
- },
21
- async headers() {
22
- return [
23
- {
24
- source: '/(.*)',
25
- headers: [
26
- {
27
- key: 'Strict-Transport-Security',
28
- value: 'max-age=63072000; includeSubDomains; preload',
29
- },
30
- { key: 'X-Content-Type-Options', value: 'nosniff' },
31
- // SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom)
32
- // can redirect the iframe back to /payment-complete on the storefront
33
- // itself after a successful charge — the postMessage relay needs the
34
- // parent frame to be able to render our own same-origin page.
35
- { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
36
- { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
37
- {
38
- key: 'Permissions-Policy',
39
- value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
40
- },
41
- ],
42
- },
43
- ];
44
- },
45
- };
46
-
47
- export default nextConfig;
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {
4
+ // isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files
5
+ // from its own package directory. Webpack bundling breaks those relative
6
+ // lookups — loading it externally from node_modules keeps the paths intact.
7
+ serverExternalPackages: ['isomorphic-dompurify'],
8
+ images: {
9
+ // The storefront is a consumer of the Brainerce API — it has to render
10
+ // whatever image URLs the API returns. In practice those URLs can be on
11
+ // cdn.brainerce.com OR on an upstream merchant host (WooCommerce, Shopify,
12
+ // self-hosted) depending on whether the product's image-import job has
13
+ // completed on the backend. Rather than hard-fail on unknown hosts, skip
14
+ // the server-side optimizer entirely and let the browser fetch each image
15
+ // directly from origin. No server-side fetching → no SSRF or DoS surface
16
+ // on this Next server. Trade-off: no webp/resize/lazy optimization, so
17
+ // LCP is marginally worse. Acceptable; the storefront is not the right
18
+ // layer to enforce a hostname policy.
19
+ unoptimized: true,
20
+ },
21
+ async headers() {
22
+ return [
23
+ {
24
+ source: '/(.*)',
25
+ headers: [
26
+ {
27
+ key: 'Strict-Transport-Security',
28
+ value: 'max-age=63072000; includeSubDomains; preload',
29
+ },
30
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
31
+ // SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom)
32
+ // can redirect the iframe back to /payment-complete on the storefront
33
+ // itself after a successful charge — the postMessage relay needs the
34
+ // parent frame to be able to render our own same-origin page.
35
+ { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
36
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
37
+ {
38
+ key: 'Permissions-Policy',
39
+ value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
40
+ },
41
+ ],
42
+ },
43
+ ];
44
+ },
45
+ };
46
+
47
+ export default nextConfig;
@@ -1,15 +1,15 @@
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
+ 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,66 +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
- 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
+ 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,76 +1,76 @@
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
- }
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
+ }