create-brainerce-store 1.27.6 → 1.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/index.js +120 -22
  2. package/messages/en.json +389 -382
  3. package/messages/he.json +389 -382
  4. package/package.json +46 -46
  5. package/templates/nextjs/base/.env.local.ejs +3 -3
  6. package/templates/nextjs/base/next.config.ts +32 -31
  7. package/templates/nextjs/base/package.json.ejs +2 -1
  8. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -14
  9. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -59
  10. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -77
  11. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +229 -198
  12. package/templates/nextjs/base/src/app/checkout/page.tsx +3 -1
  13. package/templates/nextjs/base/src/app/layout.tsx.ejs +31 -13
  14. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +501 -501
  15. package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -131
  16. package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -232
  17. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -415
  18. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +656 -592
  19. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +88 -72
  20. package/templates/nextjs/base/src/lib/csrf.ts +11 -0
  21. package/templates/nextjs/base/src/lib/nonce.ts +10 -0
  22. package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -0
  23. package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -0
  24. package/templates/nextjs/base/src/lib/validation.ts +37 -0
  25. package/templates/nextjs/base/src/middleware.ts.ejs +103 -8
  26. package/templates/nextjs/base/tsconfig.tsbuildinfo +1 -0
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "create-brainerce-store",
34
- version: "1.27.6",
34
+ version: "1.28.1",
35
35
  description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
36
36
  bin: {
37
37
  "create-brainerce-store": "dist/index.js"
@@ -96,6 +96,9 @@ function validateConnectionId(id) {
96
96
  if (id.length < 6) {
97
97
  return "Connection ID is too short";
98
98
  }
99
+ if (id.length > 100) {
100
+ return "Connection ID is too long (max 100 characters)";
101
+ }
99
102
  if (!/^vc_[a-zA-Z0-9]+$/.test(id)) {
100
103
  return "Connection ID contains invalid characters";
101
104
  }
@@ -119,6 +122,23 @@ function validateProjectName(name) {
119
122
  }
120
123
  return null;
121
124
  }
125
+ function validateApiUrl(url) {
126
+ if (!url) return null;
127
+ let parsed;
128
+ try {
129
+ parsed = new URL(url);
130
+ } catch {
131
+ return `Invalid --api-url "${url}": not a valid URL`;
132
+ }
133
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
134
+ return `Invalid --api-url "${url}": protocol must be http(s)`;
135
+ }
136
+ const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname.endsWith(".localhost");
137
+ if (parsed.protocol === "http:" && !isLocal) {
138
+ return `Invalid --api-url "${url}": http:// is only allowed for localhost`;
139
+ }
140
+ return null;
141
+ }
122
142
 
123
143
  // src/utils/package-manager.ts
124
144
  var import_child_process = require("child_process");
@@ -138,20 +158,34 @@ function detectPackageManager() {
138
158
  }
139
159
  return "npm";
140
160
  }
161
+ var ALLOWED_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
141
162
  async function installDependencies(projectDir, pkgManager) {
163
+ if (!ALLOWED_PACKAGE_MANAGERS.includes(pkgManager)) {
164
+ throw new Error(`Unsupported package manager: ${pkgManager}`);
165
+ }
142
166
  return new Promise((resolve, reject) => {
143
- const { spawn } = require("child_process");
167
+ const isWindows = process.platform === "win32";
144
168
  const args = pkgManager === "yarn" ? [] : ["install"];
145
- const child = spawn(pkgManager, args, {
169
+ const child = (0, import_child_process.spawn)(pkgManager, args, {
146
170
  cwd: projectDir,
147
- stdio: "ignore",
148
- shell: true
171
+ stdio: ["ignore", "ignore", "pipe"],
172
+ shell: isWindows
173
+ });
174
+ let stderrBuf = "";
175
+ child.stderr?.on("data", (chunk) => {
176
+ stderrBuf += chunk.toString();
177
+ if (stderrBuf.length > 8192) {
178
+ stderrBuf = stderrBuf.slice(-8192);
179
+ }
149
180
  });
150
181
  child.on("close", (code) => {
151
182
  if (code === 0) {
152
183
  resolve();
153
184
  } else {
154
- reject(new Error(`${pkgManager} install exited with code ${code}`));
185
+ const tail = stderrBuf.trim();
186
+ const detail = tail ? `
187
+ ${tail}` : "";
188
+ reject(new Error(`${pkgManager} install exited with code ${code}${detail}`));
155
189
  }
156
190
  });
157
191
  child.on("error", (err) => {
@@ -267,6 +301,19 @@ var import_ejs = __toESM(require("ejs"));
267
301
  function getDirection(language) {
268
302
  return language === "he" ? "rtl" : "ltr";
269
303
  }
304
+ function stripControlChars(value) {
305
+ return value.replace(/\p{Cc}/gu, "");
306
+ }
307
+ function toJsStringLiteral(value) {
308
+ return JSON.stringify(value);
309
+ }
310
+ function toEnvLiteral(value) {
311
+ const clean = stripControlChars(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$");
312
+ return `"${clean}"`;
313
+ }
314
+ function isValidLocale(locale) {
315
+ return typeof locale === "string" && /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})?$/.test(locale);
316
+ }
270
317
  function getFontConfig(language, theme) {
271
318
  const isHebrew = language === "he";
272
319
  if (theme === "luxury") {
@@ -312,20 +359,30 @@ async function scaffold(options) {
312
359
  const direction = getDirection(options.language);
313
360
  const fontConfig = getFontConfig(options.language, theme);
314
361
  const ogLocale = options.language === "he" ? "he_IL" : "en_US";
315
- const isMultiLocale = options.i18n?.enabled === true && options.i18n.supportedLocales.length > 1;
316
- const supportedLocales = options.i18n?.supportedLocales || [options.language];
317
- const defaultLocale = options.i18n?.defaultLocale || options.language;
362
+ const rawSupportedLocales = options.i18n?.supportedLocales || [options.language];
363
+ const supportedLocales = rawSupportedLocales.filter(isValidLocale);
364
+ if (supportedLocales.length === 0) {
365
+ throw new Error("No valid locales provided");
366
+ }
367
+ const rawDefaultLocale = options.i18n?.defaultLocale || options.language;
368
+ const defaultLocale = isValidLocale(rawDefaultLocale) ? rawDefaultLocale : options.language;
369
+ const isMultiLocale = options.i18n?.enabled === true && supportedLocales.length > 1;
370
+ const cleanStoreName = stripControlChars(options.storeName);
371
+ const cleanCurrency = stripControlChars(options.currency);
372
+ const cleanApiBaseUrl = stripControlChars(options.apiBaseUrl || "https://api.brainerce.com");
318
373
  const templateVars = {
319
374
  projectName: options.projectName,
320
375
  connectionId: options.connectionId,
321
- storeName: options.storeName,
322
- currency: options.currency,
376
+ storeNameJs: toJsStringLiteral(cleanStoreName),
377
+ titleTemplateJs: toJsStringLiteral(`%s | ${cleanStoreName}`),
378
+ storeNameEnv: toEnvLiteral(cleanStoreName),
379
+ currencyEnv: toEnvLiteral(cleanCurrency),
380
+ apiBaseUrlEnv: toEnvLiteral(cleanApiBaseUrl),
323
381
  language: options.language,
324
382
  direction,
325
383
  fontImport: fontConfig.fontImport,
326
384
  fontVariable: fontConfig.fontVariable,
327
385
  ogLocale,
328
- apiBaseUrl: options.apiBaseUrl || "https://api.brainerce.com",
329
386
  i18nEnabled: isMultiLocale,
330
387
  supportedLocales: JSON.stringify(supportedLocales),
331
388
  defaultLocale
@@ -588,11 +645,47 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
588
645
  }
589
646
  let connectionId = options.connectionId;
590
647
  const explicitApiUrl = (options.apiUrl || process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
648
+ const apiUrlError = validateApiUrl(explicitApiUrl);
649
+ if (apiUrlError) {
650
+ logger.error(apiUrlError);
651
+ process.exit(1);
652
+ }
653
+ if (explicitApiUrl && explicitApiUrl !== KNOWN_API_URLS.production && explicitApiUrl !== KNOWN_API_URLS.staging) {
654
+ logger.warn(
655
+ `Using non-standard API URL: ${explicitApiUrl} \u2014 make sure you trust this endpoint.`
656
+ );
657
+ }
591
658
  const candidateApiUrls = explicitApiUrl ? [explicitApiUrl] : [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
659
+ const VALID_LANGUAGES = ["en", "he"];
660
+ const VALID_PKG_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
661
+ const VALID_FRAMEWORKS = ["nextjs"];
662
+ const VALID_THEMES = ["minimal", "luxury", "playful"];
592
663
  let language = options.language;
664
+ if (language && !VALID_LANGUAGES.includes(language)) {
665
+ logger.error(
666
+ `Invalid --language "${language}". Expected one of: ${VALID_LANGUAGES.join(", ")}`
667
+ );
668
+ process.exit(1);
669
+ }
593
670
  let framework = options.framework;
671
+ if (!VALID_FRAMEWORKS.includes(framework)) {
672
+ logger.error(
673
+ `Invalid --framework "${framework}". Expected one of: ${VALID_FRAMEWORKS.join(", ")}`
674
+ );
675
+ process.exit(1);
676
+ }
594
677
  let theme = options.theme;
678
+ if (!VALID_THEMES.includes(theme)) {
679
+ logger.error(`Invalid --theme "${theme}". Expected one of: ${VALID_THEMES.join(", ")}`);
680
+ process.exit(1);
681
+ }
595
682
  let pkgManager = options.pkgManager;
683
+ if (pkgManager && !VALID_PKG_MANAGERS.includes(pkgManager)) {
684
+ logger.error(
685
+ `Invalid --pkg-manager "${pkgManager}". Expected one of: ${VALID_PKG_MANAGERS.join(", ")}`
686
+ );
687
+ process.exit(1);
688
+ }
596
689
  const skipGit = options.git === false;
597
690
  const skipInstall = options.install === false;
598
691
  let storeInfo = null;
@@ -703,17 +796,21 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
703
796
  const gitSpinner = createSpinner("Initializing git...");
704
797
  gitSpinner.start();
705
798
  try {
706
- const { execSync: execSync2 } = require("child_process");
799
+ const { execFileSync } = require("child_process");
707
800
  const cwd = scaffoldInPlace ? "." : projectName;
708
- execSync2("git init", { cwd, stdio: "ignore" });
709
- execSync2("git add -A", { cwd, stdio: "ignore" });
710
- execSync2('git commit -m "Initial commit from create-brainerce-store"', {
711
- cwd,
712
- stdio: "ignore"
713
- });
801
+ const gitOpts = { cwd, stdio: ["ignore", "ignore", "pipe"] };
802
+ execFileSync("git", ["init"], gitOpts);
803
+ execFileSync("git", ["add", "-A"], gitOpts);
804
+ execFileSync(
805
+ "git",
806
+ ["commit", "-m", "Initial commit from create-brainerce-store"],
807
+ gitOpts
808
+ );
714
809
  gitSpinner.succeed("Git initialized");
715
- } catch {
716
- gitSpinner.fail("Git initialization failed (git may not be installed)");
810
+ } catch (err) {
811
+ gitSpinner.fail("Git initialization failed");
812
+ const detail = err && typeof err === "object" && "stderr" in err ? String(err.stderr).trim() : err instanceof Error ? err.message : "";
813
+ if (detail) logger.warn(detail);
717
814
  }
718
815
  }
719
816
  if (!skipInstall) {
@@ -722,8 +819,9 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
722
819
  try {
723
820
  await installDependencies(scaffoldInPlace ? "." : projectName, pkgManager);
724
821
  installSpinner.succeed("Dependencies installed");
725
- } catch {
822
+ } catch (err) {
726
823
  installSpinner.fail("Dependency installation failed");
824
+ if (err instanceof Error && err.message) logger.warn(err.message);
727
825
  logger.warn(
728
826
  scaffoldInPlace ? `Run \`${pkgManager} install\` manually.` : `Run \`cd ${projectName} && ${pkgManager} install\` manually.`
729
827
  );