create-brainerce-store 1.65.0 → 1.67.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 (49) hide show
  1. package/dist/index.js +2 -2
  2. package/messages/en.json +505 -502
  3. package/messages/he.json +505 -502
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.env.local.ejs +45 -24
  6. package/templates/nextjs/base/.eslintrc.json +51 -57
  7. package/templates/nextjs/base/AGENTS.md.ejs +107 -81
  8. package/templates/nextjs/base/CLAUDE.md.ejs +118 -92
  9. package/templates/nextjs/base/next.config.ts +103 -86
  10. package/templates/nextjs/base/scripts/fetch-store-info.mjs +104 -97
  11. package/templates/nextjs/base/src/app/agents.md/route.ts +4 -3
  12. package/templates/nextjs/base/src/app/api/auth/me/route.ts +65 -59
  13. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +255 -242
  14. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +311 -308
  15. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +277 -276
  16. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +4 -3
  17. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +6 -5
  18. package/templates/nextjs/base/src/app/faq/page.tsx.ejs +46 -46
  19. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +25 -26
  20. package/templates/nextjs/base/src/app/layout.tsx.ejs +273 -257
  21. package/templates/nextjs/base/src/app/llms.txt/route.ts +4 -3
  22. package/templates/nextjs/base/src/app/opengraph-image.tsx +1 -1
  23. package/templates/nextjs/base/src/app/page.tsx +65 -64
  24. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +92 -92
  25. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +10 -5
  26. package/templates/nextjs/base/src/app/robots.ts +3 -2
  27. package/templates/nextjs/base/src/app/sitemap.ts +4 -3
  28. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +294 -292
  29. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +60 -59
  30. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +60 -61
  31. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +16 -2
  32. package/templates/nextjs/base/src/core/lib/brainerce.server.ts +81 -0
  33. package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +60 -110
  34. package/templates/nextjs/base/src/core/lib/site-url.ts +197 -0
  35. package/templates/nextjs/base/src/ui/product/faq-section.tsx.ejs +46 -0
  36. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  37. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  38. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +301 -285
  39. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +97 -96
  40. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +142 -141
  41. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +139 -138
  42. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +295 -267
  43. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  44. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +72 -71
  45. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +83 -82
  46. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +120 -119
  47. package/templates/nextjs/ui-canvas/product/faq-section.tsx.ejs +54 -0
  48. package/templates/nextjs/ui-canvas/product/review-form.tsx +267 -266
  49. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
@@ -1,86 +1,103 @@
1
- import type { NextConfig } from 'next';
2
- import { OPTIMIZED_IMAGE_HOSTS } from './src/core/lib/image-hosts';
3
-
4
- // Build-time invariant — fail loud if NEXT_PUBLIC_STORE_CURRENCY is missing
5
- // for a production build. Next.js inlines NEXT_PUBLIC_* into the client
6
- // bundle at `next build` time; once inlined, the value cannot be patched at
7
- // deploy time. A missing env here is the root cause of non-USD stores ending
8
- // up with `$5,096` baked into their HTML (and indexed by Googlebot).
9
- //
10
- // In dev (`next dev`) we only warn, so local-only experiments don't break.
11
- if (!process.env.NEXT_PUBLIC_STORE_CURRENCY) {
12
- const msg =
13
- 'NEXT_PUBLIC_STORE_CURRENCY is not set.\n' +
14
- 'Next.js inlines NEXT_PUBLIC_* vars into the client bundle at build time;\n' +
15
- 'a missing value will silently fall back to USD in storefront price displays\n' +
16
- 'and ship that to search-engine crawlers. Set it in .env.local (written by\n' +
17
- 'create-brainerce-store) or in your CI / hosting build env (Coolify / Vercel).';
18
- if (process.env.NODE_ENV === 'production') {
19
- throw new Error(`[next.config] ${msg}`);
20
- } else {
21
- console.warn(`[next.config] warning: ${msg}`);
22
- }
23
- }
24
-
25
- const nextConfig: NextConfig = {
26
- // isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files
27
- // from its own package directory. Webpack bundling breaks those relative
28
- // lookups loading it externally from node_modules keeps the paths intact.
29
- //
30
- // brainerce (the SDK) is imported across both the server and client
31
- // compilation boundaries in ~60 files, including the [locale]/layout.tsx
32
- // route (see generateStaticParams below) whose static params are resolved
33
- // by a separate jest-worker child process in `next dev` (webpack). That
34
- // worker requires the compiled page against the vendor-chunks manifest
35
- // while the main dev server may still be writing it, which can throw
36
- // "Cannot find module './vendor-chunks/brainerce.js'". Excluding it from
37
- // server bundling (loaded externally from node_modules instead) avoids the
38
- // race.
39
- serverExternalPackages: ['isomorphic-dompurify', 'brainerce'],
40
- images: {
41
- // The storefront is a consumer of the Brainerce API — it has to render
42
- // whatever image URLs the API returns. In practice those URLs are
43
- // usually on cdn.brainerce.com, but a product/variant can still carry a
44
- // raw upstream-merchant URL (WooCommerce, Shopify, self-hosted) while
45
- // its image-import job is pending or failed. Rather than allowlist every
46
- // possible merchant host (or hard-fail on them), only cdn.brainerce.com
47
- // goes through the server-side optimizer; components render product/blog
48
- // images via `CdnImage` (src/ui/shared/cdn-image.tsx), which
49
- // detects any other host and renders it `unoptimized` — an unresized
50
- // direct-from-origin fetch, same as this template's prior behavior —
51
- // instead of next/image throwing on an unconfigured hostname. No
52
- // server-side fetching of unknown hosts no SSRF/DoS surface on this
53
- // Next server.
54
- remotePatterns: OPTIMIZED_IMAGE_HOSTS.map((hostname) => ({
55
- protocol: 'https' as const,
56
- hostname,
57
- pathname: '/**',
58
- })),
59
- },
60
- async headers() {
61
- return [
62
- {
63
- source: '/(.*)',
64
- headers: [
65
- {
66
- key: 'Strict-Transport-Security',
67
- value: 'max-age=63072000; includeSubDomains; preload',
68
- },
69
- { key: 'X-Content-Type-Options', value: 'nosniff' },
70
- // SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom)
71
- // can redirect the iframe back to /payment-complete on the storefront
72
- // itself after a successful charge — the postMessage relay needs the
73
- // parent frame to be able to render our own same-origin page.
74
- { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
75
- { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
76
- {
77
- key: 'Permissions-Policy',
78
- value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
79
- },
80
- ],
81
- },
82
- ];
83
- },
84
- };
85
-
86
- export default nextConfig;
1
+ import type { NextConfig } from 'next';
2
+ import { OPTIMIZED_IMAGE_HOSTS } from './src/core/lib/image-hosts';
3
+
4
+ // Build-time invariant — fail loud if NEXT_PUBLIC_STORE_CURRENCY is missing
5
+ // for a production build. Next.js inlines NEXT_PUBLIC_* into the client
6
+ // bundle at `next build` time; once inlined, the value cannot be patched at
7
+ // deploy time. A missing env here is the root cause of non-USD stores ending
8
+ // up with `$5,096` baked into their HTML (and indexed by Googlebot).
9
+ //
10
+ // In dev (`next dev`) we only warn, so local-only experiments don't break.
11
+ if (!process.env.NEXT_PUBLIC_STORE_CURRENCY) {
12
+ const msg =
13
+ 'NEXT_PUBLIC_STORE_CURRENCY is not set.\n' +
14
+ 'Next.js inlines NEXT_PUBLIC_* vars into the client bundle at build time;\n' +
15
+ 'a missing value will silently fall back to USD in storefront price displays\n' +
16
+ 'and ship that to search-engine crawlers. Set it in .env.local (written by\n' +
17
+ 'create-brainerce-store) or in your CI / hosting build env (Coolify / Vercel).';
18
+ if (process.env.NODE_ENV === 'production') {
19
+ throw new Error(`[next.config] ${msg}`);
20
+ } else {
21
+ console.warn(`[next.config] warning: ${msg}`);
22
+ }
23
+ }
24
+
25
+ // Advisory only NEVER throw here. A missing site URL is normal and expected:
26
+ // AI builders (ChatGPT, Lovable, Bolt) and container hosts build the project
27
+ // before the deploy domain exists, and the storefront resolves its own origin
28
+ // from the request at runtime (src/core/lib/site-url.ts). Failing the build
29
+ // would break exactly the flow this exists to support. The loud signal lives
30
+ // at runtime instead, where a genuinely unresolvable origin is detectable.
31
+ if (!process.env.SITE_URL && !process.env.NEXT_PUBLIC_SITE_URL) {
32
+ console.warn(
33
+ [
34
+ '[next.config] SITE_URL is not set. Canonical URLs, sitemap.xml and robots.txt',
35
+ 'will be derived from each request (forwarded host), which works but is',
36
+ 'request-dependent. Set SITE_URL=https://your-domain.com in your hosting',
37
+ "provider's environment variables once your domain is known.",
38
+ ].join('\n')
39
+ );
40
+ }
41
+
42
+ const nextConfig: NextConfig = {
43
+ // isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files
44
+ // from its own package directory. Webpack bundling breaks those relative
45
+ // lookups loading it externally from node_modules keeps the paths intact.
46
+ //
47
+ // brainerce (the SDK) is imported across both the server and client
48
+ // compilation boundaries in ~60 files, including the [locale]/layout.tsx
49
+ // route (see generateStaticParams below) whose static params are resolved
50
+ // by a separate jest-worker child process in `next dev` (webpack). That
51
+ // worker requires the compiled page against the vendor-chunks manifest
52
+ // while the main dev server may still be writing it, which can throw
53
+ // "Cannot find module './vendor-chunks/brainerce.js'". Excluding it from
54
+ // server bundling (loaded externally from node_modules instead) avoids the
55
+ // race.
56
+ serverExternalPackages: ['isomorphic-dompurify', 'brainerce'],
57
+ images: {
58
+ // The storefront is a consumer of the Brainerce API — it has to render
59
+ // whatever image URLs the API returns. In practice those URLs are
60
+ // usually on cdn.brainerce.com, but a product/variant can still carry a
61
+ // raw upstream-merchant URL (WooCommerce, Shopify, self-hosted) while
62
+ // its image-import job is pending or failed. Rather than allowlist every
63
+ // possible merchant host (or hard-fail on them), only cdn.brainerce.com
64
+ // goes through the server-side optimizer; components render product/blog
65
+ // images via `CdnImage` (src/ui/shared/cdn-image.tsx), which
66
+ // detects any other host and renders it `unoptimized` — an unresized
67
+ // direct-from-origin fetch, same as this template's prior behavior —
68
+ // instead of next/image throwing on an unconfigured hostname. No
69
+ // server-side fetching of unknown hosts → no SSRF/DoS surface on this
70
+ // Next server.
71
+ remotePatterns: OPTIMIZED_IMAGE_HOSTS.map((hostname) => ({
72
+ protocol: 'https' as const,
73
+ hostname,
74
+ pathname: '/**',
75
+ })),
76
+ },
77
+ async headers() {
78
+ return [
79
+ {
80
+ source: '/(.*)',
81
+ headers: [
82
+ {
83
+ key: 'Strict-Transport-Security',
84
+ value: 'max-age=63072000; includeSubDomains; preload',
85
+ },
86
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
87
+ // SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom)
88
+ // can redirect the iframe back to /payment-complete on the storefront
89
+ // itself after a successful charge — the postMessage relay needs the
90
+ // parent frame to be able to render our own same-origin page.
91
+ { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
92
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
93
+ {
94
+ key: 'Permissions-Policy',
95
+ value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
96
+ },
97
+ ],
98
+ },
99
+ ];
100
+ },
101
+ };
102
+
103
+ export default nextConfig;
@@ -1,97 +1,104 @@
1
- /* eslint-disable no-console */
2
- /**
3
- * Setup script: fetches store info from Brainerce using the connection ID
4
- * and saves NEXT_PUBLIC_STORE_NAME (and other public fields) to .env.local.
5
- *
6
- * Run: node scripts/fetch-store-info.mjs
7
- */
8
-
9
- import { readFileSync, writeFileSync, existsSync } from 'fs';
10
- import { join } from 'path';
11
-
12
- const envPath = join(process.cwd(), '.env.local');
13
-
14
- if (!existsSync(envPath)) {
15
- console.error(
16
- '❌ .env.local not found. Create it first with NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID set.'
17
- );
18
- process.exit(1);
19
- }
20
-
21
- const envContent = readFileSync(envPath, 'utf-8');
22
-
23
- function getVar(content, key) {
24
- const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
25
- return match ? match[1].trim() : null;
26
- }
27
-
28
- function setVar(content, key, value) {
29
- const regex = new RegExp(`^${key}=.*$`, 'm');
30
- if (regex.test(content)) {
31
- return content.replace(regex, `${key}=${value}`);
32
- }
33
- return content.trimEnd() + `\n${key}=${value}\n`;
34
- }
35
-
36
- // Read either env var name. The new one is preferred; the old one is a soft
37
- // alias kept for backwards compatibility — the SDK accepts both.
38
- const connectionId =
39
- getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID') ||
40
- getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_CONNECTION_ID');
41
- const apiUrl = (getVar(envContent, 'BRAINERCE_API_URL') || 'https://api.brainerce.com').replace(
42
- /\/$/,
43
- ''
44
- );
45
-
46
- if (!connectionId) {
47
- console.error('❌ NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID is not set in .env.local');
48
- process.exit(1);
49
- }
50
-
51
- console.log(`Fetching store info for sales channel: ${connectionId} ...`);
52
-
53
- let storeInfo;
54
- try {
55
- // /api/vc/* enforces an Origin check: TEST channels accept local/tunnel
56
- // hosts, LIVE channels require the configured domain. Prefer the project's
57
- // own NEXT_PUBLIC_SITE_URL (the real domain for a LIVE store), falling back
58
- // to localhost for local dev against a TEST channel.
59
- const siteUrl = getVar(envContent, 'NEXT_PUBLIC_SITE_URL') || 'http://localhost:3000';
60
- const res = await fetch(`${apiUrl}/api/vc/${connectionId}/info`, {
61
- headers: { Origin: siteUrl },
62
- });
63
- if (!res.ok) {
64
- console.error(`❌ API returned ${res.status}: ${await res.text()}`);
65
- process.exit(1);
66
- }
67
- storeInfo = await res.json();
68
- } catch (err) {
69
- console.error(`❌ Failed to reach ${apiUrl}: ${err.message}`);
70
- process.exit(1);
71
- }
72
-
73
- const name = storeInfo.name;
74
- const currency = storeInfo.currency;
75
-
76
- if (!name) {
77
- console.error('❌ Store info response has no `name` field:', storeInfo);
78
- process.exit(1);
79
- }
80
- // Currency is NOT NULL in the backend schema — a response without it
81
- // is a real backend bug, not a "use the default" signal. Fail loud rather
82
- // than silently leaving the env stale (which would bake the previous
83
- // value into the next client bundle build).
84
- if (!currency) {
85
- console.error('❌ Store info response has no `currency` field:', storeInfo);
86
- process.exit(1);
87
- }
88
-
89
- let updated = envContent;
90
- updated = setVar(updated, 'NEXT_PUBLIC_STORE_NAME', name);
91
- updated = setVar(updated, 'NEXT_PUBLIC_STORE_CURRENCY', currency);
92
-
93
- writeFileSync(envPath, updated, 'utf-8');
94
-
95
- console.log(`✓ NEXT_PUBLIC_STORE_NAME=${name}`);
96
- console.log(`✓ NEXT_PUBLIC_STORE_CURRENCY=${currency}`);
97
- console.log('Done. Restart the dev server for changes to take effect.');
1
+ /* eslint-disable no-console */
2
+ /**
3
+ * Setup script: fetches store info from Brainerce using the connection ID
4
+ * and saves NEXT_PUBLIC_STORE_NAME (and other public fields) to .env.local.
5
+ *
6
+ * Run: node scripts/fetch-store-info.mjs
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ const envPath = join(process.cwd(), '.env.local');
13
+
14
+ if (!existsSync(envPath)) {
15
+ console.error(
16
+ '❌ .env.local not found. Create it first with NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID set.'
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ const envContent = readFileSync(envPath, 'utf-8');
22
+
23
+ function getVar(content, key) {
24
+ const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
25
+ return match ? match[1].trim() : null;
26
+ }
27
+
28
+ function setVar(content, key, value) {
29
+ const regex = new RegExp(`^${key}=.*$`, 'm');
30
+ if (regex.test(content)) {
31
+ return content.replace(regex, `${key}=${value}`);
32
+ }
33
+ return content.trimEnd() + `\n${key}=${value}\n`;
34
+ }
35
+
36
+ // Read either env var name. The new one is preferred; the old one is a soft
37
+ // alias kept for backwards compatibility — the SDK accepts both.
38
+ const connectionId =
39
+ getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID') ||
40
+ getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_CONNECTION_ID');
41
+ const apiUrl = (getVar(envContent, 'BRAINERCE_API_URL') || 'https://api.brainerce.com').replace(
42
+ /\/$/,
43
+ ''
44
+ );
45
+
46
+ if (!connectionId) {
47
+ console.error('❌ NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID is not set in .env.local');
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(`Fetching store info for sales channel: ${connectionId} ...`);
52
+
53
+ let storeInfo;
54
+ try {
55
+ // /api/vc/* enforces an Origin check: TEST channels accept local/tunnel
56
+ // hosts, LIVE channels require the configured domain. Prefer an explicitly
57
+ // configured site URL (the real domain for a LIVE store); localhost is only
58
+ // a last resort, and it is correct here in a way it is NOT correct at
59
+ // runtime this script runs on the developer's own machine, not on the
60
+ // host the storefront is deployed to.
61
+ const siteUrl =
62
+ process.env.SITE_URL ||
63
+ getVar(envContent, 'SITE_URL') ||
64
+ process.env.NEXT_PUBLIC_SITE_URL ||
65
+ getVar(envContent, 'NEXT_PUBLIC_SITE_URL') ||
66
+ 'http://localhost:3000';
67
+ const res = await fetch(`${apiUrl}/api/vc/${connectionId}/info`, {
68
+ headers: { Origin: siteUrl },
69
+ });
70
+ if (!res.ok) {
71
+ console.error(`❌ API returned ${res.status}: ${await res.text()}`);
72
+ process.exit(1);
73
+ }
74
+ storeInfo = await res.json();
75
+ } catch (err) {
76
+ console.error(`❌ Failed to reach ${apiUrl}: ${err.message}`);
77
+ process.exit(1);
78
+ }
79
+
80
+ const name = storeInfo.name;
81
+ const currency = storeInfo.currency;
82
+
83
+ if (!name) {
84
+ console.error('❌ Store info response has no `name` field:', storeInfo);
85
+ process.exit(1);
86
+ }
87
+ // Currency is NOT NULL in the backend schema — a response without it
88
+ // is a real backend bug, not a "use the default" signal. Fail loud rather
89
+ // than silently leaving the env stale (which would bake the previous
90
+ // value into the next client bundle build).
91
+ if (!currency) {
92
+ console.error('❌ Store info response has no `currency` field:', storeInfo);
93
+ process.exit(1);
94
+ }
95
+
96
+ let updated = envContent;
97
+ updated = setVar(updated, 'NEXT_PUBLIC_STORE_NAME', name);
98
+ updated = setVar(updated, 'NEXT_PUBLIC_STORE_CURRENCY', currency);
99
+
100
+ writeFileSync(envPath, updated, 'utf-8');
101
+
102
+ console.log(`✓ NEXT_PUBLIC_STORE_NAME=${name}`);
103
+ console.log(`✓ NEXT_PUBLIC_STORE_CURRENCY=${currency}`);
104
+ console.log('Done. Restart the dev server for changes to take effect.');
@@ -7,7 +7,8 @@
7
7
  * works. /llms.txt mirrors the same information in the older convention —
8
8
  * keep the two consistent.
9
9
  */
10
- import { getServerClient } from '@/core/lib/brainerce';
10
+ import { getServerClient } from '@/core/lib/brainerce.server';
11
+ import { getCanonicalSiteUrl } from '@/core/lib/site-url';
11
12
 
12
13
  export const revalidate = 3600;
13
14
 
@@ -18,7 +19,7 @@ interface CategoryNodeLike {
18
19
  }
19
20
 
20
21
  export async function GET() {
21
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
22
+ const baseUrl = await getCanonicalSiteUrl();
22
23
  // Machine-readable product feed served by the Brainerce API per channel —
23
24
  // the same artifact Google/Microsoft/Perplexity ingest; agents can read the
24
25
  // whole catalog in one structured fetch instead of crawling page by page.
@@ -27,7 +28,7 @@ export async function GET() {
27
28
  process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID ||
28
29
  process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID ||
29
30
  '';
30
- const client = getServerClient();
31
+ const client = await getServerClient();
31
32
 
32
33
  const [info, categoriesRes] = await Promise.all([
33
34
  client.getStoreInfo().catch(() => null),
@@ -1,59 +1,65 @@
1
- import { 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 =
10
- process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID ||
11
- process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID ||
12
- '';
13
-
14
- const TOKEN_COOKIE = 'brainerce_customer_token';
15
- const LOGGED_IN_COOKIE = 'brainerce_logged_in';
16
-
17
- /**
18
- * Auth status check endpoint.
19
- * Reads the httpOnly cookie, validates against backend, returns auth state.
20
- */
21
- export async function GET() {
22
- const cookieStore = await cookies();
23
- const tokenCookie = cookieStore.get(TOKEN_COOKIE);
24
-
25
- if (!tokenCookie?.value) {
26
- return NextResponse.json({ isLoggedIn: false });
27
- }
28
-
29
- // Derive Origin from the incoming request so the backend's BrowserOriginGuard accepts it
30
- const requestHeaders = await headers();
31
- const host = requestHeaders.get('host') || 'localhost:3000';
32
- const proto = requestHeaders.get('x-forwarded-proto') || 'http';
33
- const origin = requestHeaders.get('origin') || `${proto}://${host}`;
34
-
35
- try {
36
- // Validate token by calling backend profile endpoint
37
- const response = await fetch(`${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/me`, {
38
- headers: {
39
- Authorization: `Bearer ${tokenCookie.value}`,
40
- 'Content-Type': 'application/json',
41
- Origin: origin,
42
- },
43
- });
44
-
45
- if (!response.ok) {
46
- // Token is invalid or expired — clear cookies
47
- const res = NextResponse.json({ isLoggedIn: false });
48
- res.cookies.delete(TOKEN_COOKIE);
49
- res.cookies.delete(LOGGED_IN_COOKIE);
50
- return res;
51
- }
52
-
53
- const customer = await response.json();
54
- return NextResponse.json({ isLoggedIn: true, customer });
55
- } catch {
56
- // Backend unreachable — don't clear cookies, might be temporary
57
- return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
58
- }
59
- }
1
+ import { NextResponse } from 'next/server';
2
+ import { cookies, headers } from 'next/headers';
3
+ import { getRequestOrigin } from '@/core/lib/site-url';
4
+
5
+ const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
6
+ /\/$/,
7
+ ''
8
+ );
9
+
10
+ const CONNECTION_ID =
11
+ process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID ||
12
+ process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID ||
13
+ '';
14
+
15
+ const TOKEN_COOKIE = 'brainerce_customer_token';
16
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
17
+
18
+ /**
19
+ * Auth status check endpoint.
20
+ * Reads the httpOnly cookie, validates against backend, returns auth state.
21
+ */
22
+ export async function GET() {
23
+ const cookieStore = await cookies();
24
+ const tokenCookie = cookieStore.get(TOKEN_COOKIE);
25
+
26
+ if (!tokenCookie?.value) {
27
+ return NextResponse.json({ isLoggedIn: false });
28
+ }
29
+
30
+ // The backend compares this against the sales channel's configured domain, so
31
+ // it must be the storefront's real public origin. This route used to build it
32
+ // by hand and got three things wrong at once: it read `host` but not
33
+ // `x-forwarded-host` (an internal container host behind a proxy), it defaulted
34
+ // the protocol to `http` (never matching an `https://` configured domain), and
35
+ // it preferred the CLIENT-SUPPLIED `Origin` header — the same value the BFF
36
+ // proxy deliberately refuses to forward, because trusting it lets a caller
37
+ // pick which storefront the backend thinks it is talking to. The localhost
38
+ // fallback then made a Live channel reject every logged-in session check.
39
+ const origin = await getRequestOrigin(await headers());
40
+
41
+ try {
42
+ // Validate token by calling backend profile endpoint
43
+ const response = await fetch(`${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/me`, {
44
+ headers: {
45
+ Authorization: `Bearer ${tokenCookie.value}`,
46
+ 'Content-Type': 'application/json',
47
+ Origin: origin,
48
+ },
49
+ });
50
+
51
+ if (!response.ok) {
52
+ // Token is invalid or expired — clear cookies
53
+ const res = NextResponse.json({ isLoggedIn: false });
54
+ res.cookies.delete(TOKEN_COOKIE);
55
+ res.cookies.delete(LOGGED_IN_COOKIE);
56
+ return res;
57
+ }
58
+
59
+ const customer = await response.json();
60
+ return NextResponse.json({ isLoggedIn: true, customer });
61
+ } catch {
62
+ // Backend unreachable — don't clear cookies, might be temporary
63
+ return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
64
+ }
65
+ }