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