@brainerce/mcp-server 1.1.0 → 1.2.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/bin/http.js CHANGED
@@ -1897,39 +1897,24 @@ var EMBEDDED_TEMPLATES = {
1897
1897
  "src/lib/brainerce.ts.ejs": `import { BrainerceClient } from 'brainerce';
1898
1898
 
1899
1899
  const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || 'vc_YOUR_CONNECTION_ID';
1900
- const API_URL = process.env.NEXT_PUBLIC_BRAINERCE_API_URL || 'https://api.brainerce.com';
1901
1900
 
1902
- // Singleton SDK client
1901
+ // Singleton SDK client \u2014 routes through same-origin BFF proxy for httpOnly cookie auth
1903
1902
  let clientInstance: BrainerceClient | null = null;
1904
1903
 
1905
1904
  export function getClient(): BrainerceClient {
1906
1905
  if (!clientInstance) {
1907
1906
  clientInstance = new BrainerceClient({
1908
1907
  connectionId: CONNECTION_ID,
1909
- baseUrl: API_URL,
1908
+ baseUrl: '/api/store', // same-origin proxy handles auth via httpOnly cookie
1909
+ proxyMode: true, // skip client-side token checks; proxy adds Authorization header
1910
1910
  });
1911
1911
  }
1912
1912
  return clientInstance;
1913
1913
  }
1914
1914
 
1915
- // Auth token helpers
1916
- const TOKEN_KEY = 'brainerce_customer_token';
1915
+ // Cart ID helpers (not a security token \u2014 safe in localStorage)
1917
1916
  const CART_ID_KEY = 'brainerce_cart_id';
1918
1917
 
1919
- export function getStoredToken(): string | null {
1920
- if (typeof window === 'undefined') return null;
1921
- return localStorage.getItem(TOKEN_KEY);
1922
- }
1923
-
1924
- export function setStoredToken(token: string | null): void {
1925
- if (typeof window === 'undefined') return;
1926
- if (token) {
1927
- localStorage.setItem(TOKEN_KEY, token);
1928
- } else {
1929
- localStorage.removeItem(TOKEN_KEY);
1930
- }
1931
- }
1932
-
1933
1918
  export function getStoredCartId(): string | null {
1934
1919
  if (typeof window === 'undefined') return null;
1935
1920
  return localStorage.getItem(CART_ID_KEY);
@@ -1944,14 +1929,158 @@ export function setStoredCartId(cartId: string | null): void {
1944
1929
  }
1945
1930
  }
1946
1931
 
1947
- // Initialize client with stored auth
1932
+ // Initialize client (no token hydration \u2014 auth handled by httpOnly cookie)
1948
1933
  export function initClient(): BrainerceClient {
1949
- const client = getClient();
1950
- const token = getStoredToken();
1951
- if (token) {
1952
- client.setCustomerToken(token);
1934
+ return getClient();
1935
+ }
1936
+ `,
1937
+ "src/lib/auth.ts": `/**
1938
+ * Client-side auth helpers that call the BFF proxy API routes.
1939
+ * All mutating requests include the CSRF header.
1940
+ * The token is managed server-side via httpOnly cookies \u2014 never exposed to JS.
1941
+ */
1942
+
1943
+ const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
1944
+
1945
+ const CSRF_HEADERS: Record<string, string> = {
1946
+ 'Content-Type': 'application/json',
1947
+ 'X-Requested-With': 'brainerce',
1948
+ };
1949
+
1950
+ interface LoginResult {
1951
+ customer: {
1952
+ id: string;
1953
+ email: string;
1954
+ firstName?: string;
1955
+ lastName?: string;
1956
+ emailVerified: boolean;
1957
+ };
1958
+ expiresAt: string;
1959
+ requiresVerification?: boolean;
1960
+ }
1961
+
1962
+ interface RegisterResult {
1963
+ customer: {
1964
+ id: string;
1965
+ email: string;
1966
+ firstName?: string;
1967
+ lastName?: string;
1968
+ emailVerified: boolean;
1969
+ };
1970
+ expiresAt: string;
1971
+ requiresVerification?: boolean;
1972
+ }
1973
+
1974
+ interface AuthStatus {
1975
+ isLoggedIn: boolean;
1976
+ customer?: {
1977
+ id: string;
1978
+ email: string;
1979
+ firstName?: string;
1980
+ lastName?: string;
1981
+ phone?: string;
1982
+ emailVerified: boolean;
1983
+ };
1984
+ error?: string;
1985
+ }
1986
+
1987
+ interface VerifyEmailResult {
1988
+ verified: boolean;
1989
+ message?: string;
1990
+ }
1991
+
1992
+ async function handleResponse<T>(response: Response): Promise<T> {
1993
+ const data = await response.json();
1994
+ if (!response.ok) {
1995
+ throw new Error(data.message || data.error || \`Request failed (\${response.status})\`);
1953
1996
  }
1954
- return client;
1997
+ return data as T;
1998
+ }
1999
+
2000
+ /**
2001
+ * Login via BFF proxy. The proxy sets the httpOnly cookie on success.
2002
+ */
2003
+ export async function proxyLogin(email: string, password: string): Promise<LoginResult> {
2004
+ const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/login\`, {
2005
+ method: 'POST',
2006
+ headers: CSRF_HEADERS,
2007
+ body: JSON.stringify({ email, password }),
2008
+ });
2009
+ return handleResponse<LoginResult>(response);
2010
+ }
2011
+
2012
+ /**
2013
+ * Register via BFF proxy. The proxy sets the httpOnly cookie on success.
2014
+ */
2015
+ export async function proxyRegister(data: {
2016
+ firstName: string;
2017
+ lastName: string;
2018
+ email: string;
2019
+ password: string;
2020
+ }): Promise<RegisterResult> {
2021
+ const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/register\`, {
2022
+ method: 'POST',
2023
+ headers: CSRF_HEADERS,
2024
+ body: JSON.stringify(data),
2025
+ });
2026
+ return handleResponse<RegisterResult>(response);
2027
+ }
2028
+
2029
+ /**
2030
+ * Check auth status. Reads httpOnly cookie server-side and validates with backend.
2031
+ */
2032
+ export async function checkAuthStatus(): Promise<AuthStatus> {
2033
+ const response = await fetch('/api/auth/me');
2034
+ return response.json();
2035
+ }
2036
+
2037
+ /**
2038
+ * Logout. Clears httpOnly auth cookies server-side.
2039
+ */
2040
+ export async function proxyLogout(): Promise<void> {
2041
+ await fetch('/api/auth/logout', {
2042
+ method: 'POST',
2043
+ headers: { 'X-Requested-With': 'brainerce' },
2044
+ });
2045
+ }
2046
+
2047
+ /**
2048
+ * Verify email via BFF proxy. The auth token is in the httpOnly cookie (set during login/register).
2049
+ * The proxy adds the Authorization header automatically.
2050
+ */
2051
+ export async function proxyVerifyEmail(code: string): Promise<VerifyEmailResult> {
2052
+ const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/verify-email\`, {
2053
+ method: 'POST',
2054
+ headers: CSRF_HEADERS,
2055
+ body: JSON.stringify({ code }),
2056
+ });
2057
+ return handleResponse<VerifyEmailResult>(response);
2058
+ }
2059
+
2060
+ /**
2061
+ * Resend verification email via BFF proxy.
2062
+ * Uses the auth token from the httpOnly cookie.
2063
+ */
2064
+ export async function proxyResendVerification(): Promise<{ message: string }> {
2065
+ const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/resend-verification\`, {
2066
+ method: 'POST',
2067
+ headers: CSRF_HEADERS,
2068
+ });
2069
+ return handleResponse<{ message: string }>(response);
2070
+ }
2071
+
2072
+ /**
2073
+ * Reset password via BFF proxy.
2074
+ * The reset token is in an httpOnly cookie (set by /api/auth/reset-callback when the user
2075
+ * clicked the email link). The proxy reads it server-side \u2014 the token never reaches client JS.
2076
+ */
2077
+ export async function proxyResetPassword(newPassword: string): Promise<{ message: string }> {
2078
+ const response = await fetch('/api/auth/reset-password', {
2079
+ method: 'POST',
2080
+ headers: CSRF_HEADERS,
2081
+ body: JSON.stringify({ newPassword }),
2082
+ });
2083
+ return handleResponse<{ message: string }>(response);
1955
2084
  }
1956
2085
  `,
1957
2086
  "src/lib/utils.ts": `import { clsx, type ClassValue } from 'clsx';
@@ -1964,9 +2093,10 @@ export function cn(...inputs: ClassValue[]) {
1964
2093
  "src/providers/store-provider.tsx.ejs": `'use client';
1965
2094
 
1966
2095
  import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
1967
- import type { StoreInfo, Cart } from 'brainerce';
2096
+ import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
1968
2097
  import { getCartTotals } from 'brainerce';
1969
- import { getClient, initClient, getStoredToken, setStoredToken, setStoredCartId } from '@/lib/brainerce';
2098
+ import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
2099
+ import { checkAuthStatus, proxyLogout } from '@/lib/auth';
1970
2100
 
1971
2101
  // ---- Store Info Context ----
1972
2102
  interface StoreInfoContextValue {
@@ -1987,17 +2117,17 @@ export function useStoreInfo() {
1987
2117
  interface AuthContextValue {
1988
2118
  isLoggedIn: boolean;
1989
2119
  authLoading: boolean;
1990
- token: string | null;
1991
- login: (token: string) => void;
1992
- logout: () => void;
2120
+ customer: CustomerProfile | null;
2121
+ login: () => Promise<void>;
2122
+ logout: () => Promise<void>;
1993
2123
  }
1994
2124
 
1995
2125
  const AuthContext = createContext<AuthContextValue>({
1996
2126
  isLoggedIn: false,
1997
2127
  authLoading: true,
1998
- token: null,
1999
- login: () => {},
2000
- logout: () => {},
2128
+ customer: null,
2129
+ login: async () => {},
2130
+ logout: async () => {},
2001
2131
  });
2002
2132
 
2003
2133
  export function useAuth() {
@@ -2029,26 +2159,46 @@ export function useCart() {
2029
2159
  export function StoreProvider({ children }: { children: React.ReactNode }) {
2030
2160
  const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
2031
2161
  const [storeLoading, setStoreLoading] = useState(true);
2032
- const [token, setToken] = useState<string | null>(null);
2162
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
2163
+ const [customer, setCustomer] = useState<CustomerProfile | null>(null);
2033
2164
  const [authLoading, setAuthLoading] = useState(true);
2034
2165
  const [cart, setCart] = useState<Cart | null>(null);
2035
2166
  const [cartLoading, setCartLoading] = useState(true);
2036
2167
 
2037
- // Initialize client and auth
2168
+ // Check auth status via httpOnly cookie (server-side validation)
2169
+ const refreshAuth = useCallback(async () => {
2170
+ try {
2171
+ const status = await checkAuthStatus();
2172
+ setIsLoggedIn(status.isLoggedIn);
2173
+ setCustomer(status.isLoggedIn ? (status.customer as CustomerProfile) : null);
2174
+ } catch {
2175
+ setIsLoggedIn(false);
2176
+ setCustomer(null);
2177
+ } finally {
2178
+ setAuthLoading(false);
2179
+ }
2180
+ }, []);
2181
+
2182
+ // Initialize client, check auth, and fetch store info
2038
2183
  useEffect(() => {
2039
2184
  const client = initClient();
2040
- const stored = getStoredToken();
2041
- if (stored) {
2042
- setToken(stored);
2185
+
2186
+ // Optimistic check: if brainerce_logged_in cookie exists, assume logged in
2187
+ // while we validate the actual token server-side
2188
+ if (typeof document !== 'undefined' && document.cookie.includes('brainerce_logged_in=1')) {
2189
+ setIsLoggedIn(true);
2043
2190
  }
2044
- setAuthLoading(false);
2045
2191
 
2192
+ // Validate auth token server-side
2193
+ refreshAuth();
2194
+
2195
+ // Fetch store info (public, no auth needed)
2046
2196
  client
2047
2197
  .getStoreInfo()
2048
2198
  .then(setStoreInfo)
2049
2199
  .catch(console.error)
2050
2200
  .finally(() => setStoreLoading(false));
2051
- }, []);
2201
+ }, [refreshAuth]);
2052
2202
 
2053
2203
  // Cart management
2054
2204
  const refreshCart = useCallback(async () => {
@@ -2071,24 +2221,26 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2071
2221
 
2072
2222
  useEffect(() => {
2073
2223
  refreshCart();
2074
- }, [refreshCart, token]);
2224
+ }, [refreshCart, isLoggedIn]);
2075
2225
 
2076
- const login = useCallback((newToken: string) => {
2077
- const client = getClient();
2078
- client.setCustomerToken(newToken);
2079
- setStoredToken(newToken);
2080
- setToken(newToken);
2226
+ // Called after successful login (cookie already set by proxy)
2227
+ const login = useCallback(async () => {
2228
+ // Refresh auth state from server (reads httpOnly cookie)
2229
+ await refreshAuth();
2081
2230
 
2082
2231
  // Merge guest session cart into customer cart
2232
+ const client = getClient();
2083
2233
  client.syncCartOnLogin().catch(console.error);
2084
- }, []);
2234
+ }, [refreshAuth]);
2235
+
2236
+ const logout = useCallback(async () => {
2237
+ // Clear httpOnly cookie server-side
2238
+ await proxyLogout();
2085
2239
 
2086
- const logout = useCallback(() => {
2087
2240
  const client = getClient();
2088
- client.clearCustomerToken();
2089
2241
  client.onLogout();
2090
- setStoredToken(null);
2091
- setToken(null);
2242
+ setIsLoggedIn(false);
2243
+ setCustomer(null);
2092
2244
  setCart(null);
2093
2245
  refreshCart();
2094
2246
  }, [refreshCart]);
@@ -2101,7 +2253,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
2101
2253
 
2102
2254
  return (
2103
2255
  <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
2104
- <AuthContext.Provider value={{ isLoggedIn: !!token, authLoading, token, login, logout }}>
2256
+ <AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
2105
2257
  <CartContext.Provider
2106
2258
  value={{ cart, cartLoading, refreshCart, itemCount, totals }}
2107
2259
  >
@@ -3921,8 +4073,8 @@ export default function OrderConfirmationPage() {
3921
4073
  import { useState } from 'react';
3922
4074
  import { useRouter } from 'next/navigation';
3923
4075
  import Link from 'next/link';
3924
- import { getClient } from '@/lib/brainerce';
3925
4076
  import { useAuth } from '@/providers/store-provider';
4077
+ import { proxyLogin } from '@/lib/auth';
3926
4078
  import { LoginForm } from '@/components/auth/login-form';
3927
4079
  import { OAuthButtons } from '@/components/auth/oauth-buttons';
3928
4080
  import { useTranslations } from '@/lib/translations';
@@ -3936,15 +4088,16 @@ export default function LoginPage() {
3936
4088
  async function handleLogin(email: string, password: string) {
3937
4089
  try {
3938
4090
  setError(null);
3939
- const client = getClient();
3940
- const result = await client.loginCustomer(email, password);
4091
+ const result = await proxyLogin(email, password);
3941
4092
 
3942
4093
  if (result.requiresVerification) {
3943
- router.push(\`/verify-email?token=\${encodeURIComponent(result.token)}\`);
4094
+ // Verification token is NOT the auth JWT \u2014 safe to pass in URL
4095
+ router.push('/verify-email');
3944
4096
  return;
3945
4097
  }
3946
4098
 
3947
- auth.login(result.token);
4099
+ // Cookie was set by the proxy; refresh auth state
4100
+ await auth.login();
3948
4101
  router.push('/');
3949
4102
  } catch (err) {
3950
4103
  const message = err instanceof Error ? err.message : 'Login failed. Please try again.';
@@ -3980,8 +4133,8 @@ export default function LoginPage() {
3980
4133
  import { useState } from 'react';
3981
4134
  import { useRouter } from 'next/navigation';
3982
4135
  import Link from 'next/link';
3983
- import { getClient } from '@/lib/brainerce';
3984
4136
  import { useAuth } from '@/providers/store-provider';
4137
+ import { proxyRegister } from '@/lib/auth';
3985
4138
  import { RegisterForm } from '@/components/auth/register-form';
3986
4139
  import { OAuthButtons } from '@/components/auth/oauth-buttons';
3987
4140
  import { useTranslations } from '@/lib/translations';
@@ -4000,20 +4153,16 @@ export default function RegisterPage() {
4000
4153
  }) {
4001
4154
  try {
4002
4155
  setError(null);
4003
- const client = getClient();
4004
- const result = await client.registerCustomer({
4005
- firstName: data.firstName,
4006
- lastName: data.lastName,
4007
- email: data.email,
4008
- password: data.password,
4009
- });
4156
+ const result = await proxyRegister(data);
4010
4157
 
4011
4158
  if (result.requiresVerification) {
4012
- router.push(\`/verify-email?token=\${encodeURIComponent(result.token)}\`);
4159
+ // Cookie already set by proxy; verify-email uses it for auth
4160
+ router.push('/verify-email');
4013
4161
  return;
4014
4162
  }
4015
4163
 
4016
- auth.login(result.token);
4164
+ // Cookie was set by the proxy; refresh auth state
4165
+ await auth.login();
4017
4166
  router.push('/');
4018
4167
  } catch (err) {
4019
4168
  const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
@@ -4047,10 +4196,10 @@ export default function RegisterPage() {
4047
4196
  "src/app/verify-email/page.tsx": `'use client';
4048
4197
 
4049
4198
  import { Suspense, useState, useRef, useEffect, useCallback } from 'react';
4050
- import { useRouter, useSearchParams } from 'next/navigation';
4199
+ import { useRouter } from 'next/navigation';
4051
4200
  import Link from 'next/link';
4052
- import { getClient } from '@/lib/brainerce';
4053
4201
  import { useAuth } from '@/providers/store-provider';
4202
+ import { proxyVerifyEmail, proxyResendVerification } from '@/lib/auth';
4054
4203
  import { LoadingSpinner } from '@/components/shared/loading-spinner';
4055
4204
  import { useTranslations } from '@/lib/translations';
4056
4205
 
@@ -4059,10 +4208,8 @@ const RESEND_COOLDOWN_SECONDS = 60;
4059
4208
 
4060
4209
  function VerifyEmailContent() {
4061
4210
  const router = useRouter();
4062
- const searchParams = useSearchParams();
4063
4211
  const auth = useAuth();
4064
4212
 
4065
- const token = searchParams.get('token');
4066
4213
  const t = useTranslations('auth');
4067
4214
 
4068
4215
  const [digits, setDigits] = useState<string[]>(Array(CODE_LENGTH).fill(''));
@@ -4090,18 +4237,17 @@ function VerifyEmailContent() {
4090
4237
 
4091
4238
  const handleSubmit = useCallback(
4092
4239
  async (code: string) => {
4093
- if (!token || code.length !== CODE_LENGTH || loading) return;
4240
+ if (code.length !== CODE_LENGTH || loading) return;
4094
4241
 
4095
4242
  try {
4096
4243
  setLoading(true);
4097
4244
  setError(null);
4098
- const client = getClient();
4099
- const result = await client.verifyEmail(code, token);
4245
+ // Auth token is in httpOnly cookie \u2014 proxy adds Authorization header
4246
+ const result = await proxyVerifyEmail(code);
4100
4247
 
4101
4248
  if (result.verified) {
4102
- // token field exists on newer SDK versions
4103
- const authToken = (result as unknown as { token?: string }).token || token;
4104
- auth.login(authToken);
4249
+ // Refresh auth state (cookie already set)
4250
+ await auth.login();
4105
4251
  setSuccess('Email verified successfully! Redirecting...');
4106
4252
  setTimeout(() => router.push('/'), 1500);
4107
4253
  } else {
@@ -4115,7 +4261,7 @@ function VerifyEmailContent() {
4115
4261
  setLoading(false);
4116
4262
  }
4117
4263
  },
4118
- [token, loading, auth, router]
4264
+ [loading, auth, router]
4119
4265
  );
4120
4266
 
4121
4267
  function handleDigitChange(index: number, value: string) {
@@ -4169,13 +4315,12 @@ function VerifyEmailContent() {
4169
4315
  }
4170
4316
 
4171
4317
  async function handleResend() {
4172
- if (!token || resending || cooldown > 0) return;
4318
+ if (resending || cooldown > 0) return;
4173
4319
 
4174
4320
  try {
4175
4321
  setResending(true);
4176
4322
  setError(null);
4177
- const client = getClient();
4178
- await client.resendVerificationEmail(token);
4323
+ await proxyResendVerification();
4179
4324
  setSuccess('Verification code sent! Check your email.');
4180
4325
  setCooldown(RESEND_COOLDOWN_SECONDS);
4181
4326
  // Clear digits for fresh entry
@@ -4195,37 +4340,6 @@ function VerifyEmailContent() {
4195
4340
  handleSubmit(code);
4196
4341
  }
4197
4342
 
4198
- // No token provided
4199
- if (!token) {
4200
- return (
4201
- <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
4202
- <div className="w-full max-w-md space-y-4 text-center">
4203
- <svg
4204
- className="text-muted-foreground mx-auto h-12 w-12"
4205
- fill="none"
4206
- viewBox="0 0 24 24"
4207
- stroke="currentColor"
4208
- >
4209
- <path
4210
- strokeLinecap="round"
4211
- strokeLinejoin="round"
4212
- strokeWidth={1.5}
4213
- d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
4214
- />
4215
- </svg>
4216
- <h1 className="text-foreground text-2xl font-bold">{t('verificationInvalid')}</h1>
4217
- <p className="text-muted-foreground text-sm">{t('verificationInvalidDesc')}</p>
4218
- <Link
4219
- href="/register"
4220
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
4221
- >
4222
- {t('goToRegister')}
4223
- </Link>
4224
- </div>
4225
- </div>
4226
- );
4227
- }
4228
-
4229
4343
  return (
4230
4344
  <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
4231
4345
  <div className="w-full max-w-md space-y-6">
@@ -4355,8 +4469,8 @@ function OAuthCallbackContent() {
4355
4469
  const t = useTranslations('auth');
4356
4470
 
4357
4471
  const oauthSuccess = searchParams.get('oauth_success');
4358
- const token = searchParams.get('token');
4359
4472
  const oauthError = searchParams.get('oauth_error');
4473
+ // Token is no longer in URL \u2014 it was set as httpOnly cookie by /api/auth/oauth-callback
4360
4474
 
4361
4475
  useEffect(() => {
4362
4476
  // Prevent double-processing in React StrictMode
@@ -4368,13 +4482,15 @@ function OAuthCallbackContent() {
4368
4482
  return;
4369
4483
  }
4370
4484
 
4371
- if (oauthSuccess === 'true' && token) {
4372
- auth.login(token);
4373
- router.push('/');
4485
+ if (oauthSuccess === 'true') {
4486
+ // Cookie was already set by the API route; refresh auth state
4487
+ auth.login().then(() => {
4488
+ router.push('/');
4489
+ });
4374
4490
  } else {
4375
4491
  setError(t('authFailedDesc'));
4376
4492
  }
4377
- }, [oauthSuccess, token, oauthError, auth, router]);
4493
+ }, [oauthSuccess, oauthError, auth, router, t]);
4378
4494
 
4379
4495
  if (error) {
4380
4496
  return (
@@ -4541,6 +4657,348 @@ export default function AccountPage() {
4541
4657
  </div>
4542
4658
  );
4543
4659
  }
4660
+ `,
4661
+ "src/app/api/store/[...path]/route.ts": `import { NextRequest, NextResponse } from 'next/server';
4662
+ import { cookies } from 'next/headers';
4663
+
4664
+ const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
4665
+ /\\/$/,
4666
+ ''
4667
+ );
4668
+
4669
+ const TOKEN_COOKIE = 'brainerce_customer_token';
4670
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
4671
+ const CSRF_HEADER = 'x-requested-with';
4672
+ const CSRF_VALUE = 'brainerce';
4673
+
4674
+ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
4675
+
4676
+ /** Auth endpoints whose responses contain tokens to intercept */
4677
+ const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
4678
+
4679
+ function isAuthEndpoint(path: string): boolean {
4680
+ return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
4681
+ }
4682
+
4683
+ function isSecure(): boolean {
4684
+ return process.env.NODE_ENV === 'production';
4685
+ }
4686
+
4687
+ function setAuthCookies(response: NextResponse, token: string): void {
4688
+ response.cookies.set(TOKEN_COOKIE, token, {
4689
+ httpOnly: true,
4690
+ secure: isSecure(),
4691
+ sameSite: 'lax',
4692
+ path: '/',
4693
+ maxAge: COOKIE_MAX_AGE,
4694
+ });
4695
+ response.cookies.set(LOGGED_IN_COOKIE, '1', {
4696
+ httpOnly: false,
4697
+ secure: isSecure(),
4698
+ sameSite: 'lax',
4699
+ path: '/',
4700
+ maxAge: COOKIE_MAX_AGE,
4701
+ });
4702
+ }
4703
+
4704
+ function clearAuthCookies(response: NextResponse): void {
4705
+ response.cookies.delete(TOKEN_COOKIE);
4706
+ response.cookies.delete(LOGGED_IN_COOKIE);
4707
+ }
4708
+
4709
+ async function proxyRequest(
4710
+ request: NextRequest,
4711
+ params: { path: string[] }
4712
+ ): Promise<NextResponse> {
4713
+ const method = request.method;
4714
+
4715
+ // CSRF protection for mutating requests
4716
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
4717
+ const csrfHeader = request.headers.get(CSRF_HEADER);
4718
+ if (csrfHeader !== CSRF_VALUE) {
4719
+ return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
4720
+ }
4721
+ }
4722
+
4723
+ // Build backend URL from path segments
4724
+ const pathSegments = params.path.join('/');
4725
+ const backendUrl = new URL(\`\${BACKEND_URL}/\${pathSegments}\`);
4726
+
4727
+ // Forward query parameters
4728
+ request.nextUrl.searchParams.forEach((value, key) => {
4729
+ backendUrl.searchParams.set(key, value);
4730
+ });
4731
+
4732
+ // Build headers for backend request
4733
+ const headers: Record<string, string> = {
4734
+ 'Content-Type': 'application/json',
4735
+ };
4736
+
4737
+ // Forward SDK version header if present
4738
+ const sdkVersion = request.headers.get('x-sdk-version');
4739
+ if (sdkVersion) {
4740
+ headers['X-SDK-Version'] = sdkVersion;
4741
+ }
4742
+
4743
+ // Add auth token from httpOnly cookie
4744
+ const cookieStore = await cookies();
4745
+ const tokenCookie = cookieStore.get(TOKEN_COOKIE);
4746
+ if (tokenCookie?.value) {
4747
+ headers['Authorization'] = \`Bearer \${tokenCookie.value}\`;
4748
+ }
4749
+
4750
+ // Forward request body for non-GET requests
4751
+ let body: string | undefined;
4752
+ if (method !== 'GET' && method !== 'HEAD') {
4753
+ try {
4754
+ body = await request.text();
4755
+ } catch {
4756
+ // No body
4757
+ }
4758
+ }
4759
+
4760
+ // Proxy the request to backend
4761
+ let backendResponse: Response;
4762
+ try {
4763
+ backendResponse = await fetch(backendUrl.toString(), {
4764
+ method,
4765
+ headers,
4766
+ body,
4767
+ });
4768
+ } catch (error) {
4769
+ return NextResponse.json({ error: 'Backend service unavailable' }, { status: 502 });
4770
+ }
4771
+
4772
+ // Read response body
4773
+ const responseText = await backendResponse.text();
4774
+
4775
+ // For auth endpoints: intercept token, set cookie, strip token from response
4776
+ if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
4777
+ try {
4778
+ const data = JSON.parse(responseText);
4779
+ if (data.token) {
4780
+ const token = data.token;
4781
+
4782
+ // Strip token from client response
4783
+ const { token: _stripped, ...safeData } = data;
4784
+
4785
+ const response = NextResponse.json(safeData, {
4786
+ status: backendResponse.status,
4787
+ });
4788
+ setAuthCookies(response, token);
4789
+ return response;
4790
+ }
4791
+ } catch {
4792
+ // Not JSON or no token field \u2014 pass through
4793
+ }
4794
+ }
4795
+
4796
+ // Handle 401 responses: clear auth cookies
4797
+ if (backendResponse.status === 401 && tokenCookie?.value) {
4798
+ const response = new NextResponse(responseText, {
4799
+ status: backendResponse.status,
4800
+ headers: {
4801
+ 'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
4802
+ },
4803
+ });
4804
+ clearAuthCookies(response);
4805
+ return response;
4806
+ }
4807
+
4808
+ // Pass through response as-is
4809
+ return new NextResponse(responseText, {
4810
+ status: backendResponse.status,
4811
+ headers: {
4812
+ 'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
4813
+ },
4814
+ });
4815
+ }
4816
+
4817
+ export async function GET(
4818
+ request: NextRequest,
4819
+ { params }: { params: Promise<{ path: string[] }> }
4820
+ ) {
4821
+ return proxyRequest(request, await params);
4822
+ }
4823
+
4824
+ export async function POST(
4825
+ request: NextRequest,
4826
+ { params }: { params: Promise<{ path: string[] }> }
4827
+ ) {
4828
+ return proxyRequest(request, await params);
4829
+ }
4830
+
4831
+ export async function PUT(
4832
+ request: NextRequest,
4833
+ { params }: { params: Promise<{ path: string[] }> }
4834
+ ) {
4835
+ return proxyRequest(request, await params);
4836
+ }
4837
+
4838
+ export async function PATCH(
4839
+ request: NextRequest,
4840
+ { params }: { params: Promise<{ path: string[] }> }
4841
+ ) {
4842
+ return proxyRequest(request, await params);
4843
+ }
4844
+
4845
+ export async function DELETE(
4846
+ request: NextRequest,
4847
+ { params }: { params: Promise<{ path: string[] }> }
4848
+ ) {
4849
+ return proxyRequest(request, await params);
4850
+ }
4851
+ `,
4852
+ "src/app/api/auth/oauth-callback/route.ts": `import { NextRequest, NextResponse } from 'next/server';
4853
+
4854
+ const TOKEN_COOKIE = 'brainerce_customer_token';
4855
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
4856
+ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
4857
+
4858
+ function isSecure(): boolean {
4859
+ return process.env.NODE_ENV === 'production';
4860
+ }
4861
+
4862
+ /**
4863
+ * OAuth callback handler.
4864
+ * The backend redirects here with ?token=jwt&oauth_success=true after OAuth code exchange.
4865
+ * We set the httpOnly cookie and redirect to the client-side callback page (without the token).
4866
+ */
4867
+ export async function GET(request: NextRequest) {
4868
+ const { searchParams } = request.nextUrl;
4869
+ const token = searchParams.get('token');
4870
+ const oauthSuccess = searchParams.get('oauth_success');
4871
+ const oauthError = searchParams.get('oauth_error');
4872
+
4873
+ // Build redirect URL to client-side callback page
4874
+ const redirectUrl = new URL('/auth/callback', request.url);
4875
+
4876
+ if (oauthError) {
4877
+ redirectUrl.searchParams.set('oauth_error', oauthError);
4878
+ return NextResponse.redirect(redirectUrl);
4879
+ }
4880
+
4881
+ if (oauthSuccess === 'true' && token) {
4882
+ redirectUrl.searchParams.set('oauth_success', 'true');
4883
+
4884
+ const response = NextResponse.redirect(redirectUrl);
4885
+
4886
+ // Set httpOnly cookie with the token
4887
+ response.cookies.set(TOKEN_COOKIE, token, {
4888
+ httpOnly: true,
4889
+ secure: isSecure(),
4890
+ sameSite: 'lax',
4891
+ path: '/',
4892
+ maxAge: COOKIE_MAX_AGE,
4893
+ });
4894
+
4895
+ // Set indicator cookie (readable by client JS)
4896
+ response.cookies.set(LOGGED_IN_COOKIE, '1', {
4897
+ httpOnly: false,
4898
+ secure: isSecure(),
4899
+ sameSite: 'lax',
4900
+ path: '/',
4901
+ maxAge: COOKIE_MAX_AGE,
4902
+ });
4903
+
4904
+ return response;
4905
+ }
4906
+
4907
+ // Fallback: no token or success flag
4908
+ redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
4909
+ return NextResponse.redirect(redirectUrl);
4910
+ }
4911
+ `,
4912
+ "src/app/api/auth/me/route.ts": `import { NextResponse } from 'next/server';
4913
+ import { cookies } from 'next/headers';
4914
+
4915
+ const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
4916
+ /\\/$/,
4917
+ ''
4918
+ );
4919
+
4920
+ const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
4921
+
4922
+ const TOKEN_COOKIE = 'brainerce_customer_token';
4923
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
4924
+
4925
+ /**
4926
+ * Auth status check endpoint.
4927
+ * Reads the httpOnly cookie, validates against backend, returns auth state.
4928
+ */
4929
+ export async function GET() {
4930
+ const cookieStore = await cookies();
4931
+ const tokenCookie = cookieStore.get(TOKEN_COOKIE);
4932
+
4933
+ if (!tokenCookie?.value) {
4934
+ return NextResponse.json({ isLoggedIn: false });
4935
+ }
4936
+
4937
+ try {
4938
+ // Validate token by calling backend profile endpoint
4939
+ const response = await fetch(\`\${BACKEND_URL}/api/vc/\${CONNECTION_ID}/customers/me\`, {
4940
+ headers: {
4941
+ Authorization: \`Bearer \${tokenCookie.value}\`,
4942
+ 'Content-Type': 'application/json',
4943
+ },
4944
+ });
4945
+
4946
+ if (!response.ok) {
4947
+ // Token is invalid or expired \u2014 clear cookies
4948
+ const res = NextResponse.json({ isLoggedIn: false });
4949
+ res.cookies.delete(TOKEN_COOKIE);
4950
+ res.cookies.delete(LOGGED_IN_COOKIE);
4951
+ return res;
4952
+ }
4953
+
4954
+ const customer = await response.json();
4955
+ return NextResponse.json({ isLoggedIn: true, customer });
4956
+ } catch {
4957
+ // Backend unreachable \u2014 don't clear cookies, might be temporary
4958
+ return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
4959
+ }
4960
+ }
4961
+ `,
4962
+ "src/app/api/auth/logout/route.ts": `import { NextResponse } from 'next/server';
4963
+
4964
+ const TOKEN_COOKIE = 'brainerce_customer_token';
4965
+ const LOGGED_IN_COOKIE = 'brainerce_logged_in';
4966
+
4967
+ /**
4968
+ * Logout endpoint. Clears auth cookies.
4969
+ */
4970
+ export async function POST() {
4971
+ const response = NextResponse.json({ success: true });
4972
+ response.cookies.delete(TOKEN_COOKIE);
4973
+ response.cookies.delete(LOGGED_IN_COOKIE);
4974
+ return response;
4975
+ }
4976
+ `,
4977
+ "src/middleware.ts": `import { NextRequest, NextResponse } from 'next/server';
4978
+
4979
+ const TOKEN_COOKIE = 'brainerce_customer_token';
4980
+
4981
+ /** Routes that require customer authentication */
4982
+ const PROTECTED_PATHS = ['/account'];
4983
+
4984
+ export function middleware(request: NextRequest) {
4985
+ const { pathname } = request.nextUrl;
4986
+ const isProtected = PROTECTED_PATHS.some((p) => pathname.startsWith(p));
4987
+
4988
+ if (isProtected) {
4989
+ const token = request.cookies.get(TOKEN_COOKIE);
4990
+ if (!token?.value) {
4991
+ const loginUrl = new URL('/login', request.url);
4992
+ return NextResponse.redirect(loginUrl);
4993
+ }
4994
+ }
4995
+
4996
+ return NextResponse.next();
4997
+ }
4998
+
4999
+ export const config = {
5000
+ matcher: ['/account/:path*'],
5001
+ };
4544
5002
  `,
4545
5003
  "src/components/products/product-card.tsx": `'use client';
4546
5004
 
@@ -5872,8 +6330,11 @@ declare global {
5872
6330
  }
5873
6331
 
5874
6332
  // Payment SDK script URLs \u2014 resolved per provider
6333
+ // SRI hashes must be regenerated when the provider updates their SDK
5875
6334
  const PAYMENT_SDK_URL = 'https://cdn.meshulam.co.il/sdk/gs.min.js';
6335
+ const PAYMENT_SDK_SRI = 'sha384-e1OYzZERZLgbR8Zw1I4Ww/J7qXGyEsZb7LF0z5W/sbnTsFwtxop7sKZsLGNp6CB1';
5876
6336
  const APPLE_PAY_SDK_URL = 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js';
6337
+ const APPLE_PAY_SDK_SRI = 'sha384-CG67HXmWBeyuRR/jqFsYr6dGEMFyuZw3/hRmcyACJRYHGb/1ebEkQZyzdJXDc9/u';
5877
6338
 
5878
6339
  interface PaymentStepProps {
5879
6340
  checkoutId: string;
@@ -5884,7 +6345,7 @@ interface PaymentStepProps {
5884
6345
  * Load a script tag if not already present in the DOM.
5885
6346
  * Resolves when the script loads (or immediately if already present).
5886
6347
  */
5887
- function loadScript(src: string, optional = false): Promise<void> {
6348
+ function loadScript(src: string, optional = false, integrity?: string): Promise<void> {
5888
6349
  return new Promise((resolve) => {
5889
6350
  if (document.querySelector(\`script[src="\${src}"]\`)) {
5890
6351
  resolve();
@@ -5893,6 +6354,10 @@ function loadScript(src: string, optional = false): Promise<void> {
5893
6354
  const script = document.createElement('script');
5894
6355
  script.src = src;
5895
6356
  script.async = true;
6357
+ if (integrity) {
6358
+ script.integrity = integrity;
6359
+ script.crossOrigin = 'anonymous';
6360
+ }
5896
6361
  script.onload = () => resolve();
5897
6362
  script.onerror = () => {
5898
6363
  if (optional) {
@@ -5985,10 +6450,10 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
5985
6450
 
5986
6451
  async function loadSdkScripts() {
5987
6452
  // Load Apple Pay SDK (optional)
5988
- await loadScript(APPLE_PAY_SDK_URL, true);
6453
+ await loadScript(APPLE_PAY_SDK_URL, true, APPLE_PAY_SDK_SRI);
5989
6454
 
5990
6455
  // Load payment SDK script
5991
- await loadScript(PAYMENT_SDK_URL);
6456
+ await loadScript(PAYMENT_SDK_URL, false, PAYMENT_SDK_SRI);
5992
6457
 
5993
6458
  // Wait for SDK global to be set by the script
5994
6459
  const available = await waitForPaymentSdkGlobal();
@@ -6651,7 +7116,7 @@ export function OAuthButtons({ className }: OAuthButtonsProps) {
6651
7116
  try {
6652
7117
  setRedirecting(provider);
6653
7118
  const client = getClient();
6654
- const redirectUrl = window.location.origin + '/auth/callback';
7119
+ const redirectUrl = window.location.origin + '/api/auth/oauth-callback';
6655
7120
  const result = await client.getOAuthAuthorizeUrl(provider, { redirectUrl });
6656
7121
  window.location.href = result.authorizationUrl;
6657
7122
  } catch (err) {
@@ -8286,6 +8751,39 @@ my-store/
8286
8751
  | \`app/auth/callback/page.tsx\` | Extracts OAuth token from URL params, stores in localStorage |
8287
8752
  | \`app/account/page.tsx\` | Protected page: getMyProfile() + getMyOrders() |
8288
8753
  | \`components/Header.tsx\` | Site header with nav, cart count badge, search autocomplete |
8754
+
8755
+ ## Content Security Policy (CSP)
8756
+
8757
+ Payment SDKs load iframes and scripts from external domains. Add these CSP headers in \`next.config.js\`:
8758
+
8759
+ \`\`\`js
8760
+ // next.config.js
8761
+ module.exports = {
8762
+ async headers() {
8763
+ return [{
8764
+ source: '/(.*)',
8765
+ headers: [{
8766
+ key: 'Content-Security-Policy',
8767
+ value: [
8768
+ "default-src 'self'",
8769
+ "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",
8770
+ "style-src 'self' 'unsafe-inline'",
8771
+ "img-src 'self' data: blob: https:",
8772
+ "font-src 'self' data:",
8773
+ "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",
8774
+ "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",
8775
+ "worker-src 'self' blob:",
8776
+ ].join('; '),
8777
+ }],
8778
+ }];
8779
+ },
8780
+ };
8781
+ \`\`\`
8782
+
8783
+ **Required domains by provider:**
8784
+ - **Grow**: \`*.meshulam.co.il\`, \`grow.link\`, \`*.grow.link\`, \`*.grow.security\`, \`*.creditguard.co.il\`
8785
+ - **Stripe**: \`js.stripe.com\`, \`hooks.stripe.com\`, \`*.stripe.com\`
8786
+ - **Google Pay**: \`pay.google.com\`, \`google.com\`
8289
8787
  `;
8290
8788
 
8291
8789
  // src/resources/project-template.ts