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.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +52 -24
- package/templates/nextjs/base/.eslintrc.json +51 -57
- package/templates/nextjs/base/AGENTS.md.ejs +114 -81
- package/templates/nextjs/base/CLAUDE.md.ejs +125 -92
- package/templates/nextjs/base/next.config.ts +103 -86
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +104 -97
- package/templates/nextjs/base/src/app/agents.md/route.ts +4 -3
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +65 -59
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +255 -242
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +317 -308
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +277 -276
- package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +4 -3
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +22 -12
- package/templates/nextjs/base/src/app/error.tsx.ejs +53 -0
- package/templates/nextjs/base/src/app/faq/page.tsx.ejs +46 -46
- package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +25 -26
- package/templates/nextjs/base/src/app/layout.tsx.ejs +273 -257
- package/templates/nextjs/base/src/app/llms.txt/route.ts +4 -3
- package/templates/nextjs/base/src/app/opengraph-image.tsx +1 -1
- package/templates/nextjs/base/src/app/page.tsx +65 -64
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +98 -92
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +22 -11
- package/templates/nextjs/base/src/app/robots.ts +3 -2
- package/templates/nextjs/base/src/app/sitemap.ts +4 -3
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +294 -292
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +60 -59
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +60 -61
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +2 -1
- package/templates/nextjs/base/src/core/lib/brainerce.server.ts +81 -0
- package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +60 -110
- package/templates/nextjs/base/src/core/lib/site-url.ts +230 -0
- package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +301 -285
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +97 -96
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +142 -141
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +139 -138
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +295 -267
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +72 -71
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +83 -82
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +120 -119
- package/templates/nextjs/ui-canvas/product/faq-section.tsx.ejs +54 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +267 -266
- 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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 =
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
process.env.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|