create-brainerce-store 1.27.6 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/index.js +93 -11
  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 +29 -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 +86 -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 +91 -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.0",
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,14 +158,17 @@ 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 cmd = process.platform === "win32" ? `${pkgManager}.cmd` : pkgManager;
144
168
  const args = pkgManager === "yarn" ? [] : ["install"];
145
- const child = spawn(pkgManager, args, {
169
+ const child = (0, import_child_process.spawn)(cmd, args, {
146
170
  cwd: projectDir,
147
- stdio: "ignore",
148
- shell: true
171
+ stdio: "ignore"
149
172
  });
150
173
  child.on("close", (code) => {
151
174
  if (code === 0) {
@@ -267,6 +290,19 @@ var import_ejs = __toESM(require("ejs"));
267
290
  function getDirection(language) {
268
291
  return language === "he" ? "rtl" : "ltr";
269
292
  }
293
+ function stripControlChars(value) {
294
+ return value.replace(/\p{Cc}/gu, "");
295
+ }
296
+ function toJsStringLiteral(value) {
297
+ return JSON.stringify(value);
298
+ }
299
+ function toEnvLiteral(value) {
300
+ const clean = stripControlChars(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$");
301
+ return `"${clean}"`;
302
+ }
303
+ function isValidLocale(locale) {
304
+ return typeof locale === "string" && /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})?$/.test(locale);
305
+ }
270
306
  function getFontConfig(language, theme) {
271
307
  const isHebrew = language === "he";
272
308
  if (theme === "luxury") {
@@ -312,20 +348,30 @@ async function scaffold(options) {
312
348
  const direction = getDirection(options.language);
313
349
  const fontConfig = getFontConfig(options.language, theme);
314
350
  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;
351
+ const rawSupportedLocales = options.i18n?.supportedLocales || [options.language];
352
+ const supportedLocales = rawSupportedLocales.filter(isValidLocale);
353
+ if (supportedLocales.length === 0) {
354
+ throw new Error("No valid locales provided");
355
+ }
356
+ const rawDefaultLocale = options.i18n?.defaultLocale || options.language;
357
+ const defaultLocale = isValidLocale(rawDefaultLocale) ? rawDefaultLocale : options.language;
358
+ const isMultiLocale = options.i18n?.enabled === true && supportedLocales.length > 1;
359
+ const cleanStoreName = stripControlChars(options.storeName);
360
+ const cleanCurrency = stripControlChars(options.currency);
361
+ const cleanApiBaseUrl = stripControlChars(options.apiBaseUrl || "https://api.brainerce.com");
318
362
  const templateVars = {
319
363
  projectName: options.projectName,
320
364
  connectionId: options.connectionId,
321
- storeName: options.storeName,
322
- currency: options.currency,
365
+ storeNameJs: toJsStringLiteral(cleanStoreName),
366
+ titleTemplateJs: toJsStringLiteral(`%s | ${cleanStoreName}`),
367
+ storeNameEnv: toEnvLiteral(cleanStoreName),
368
+ currencyEnv: toEnvLiteral(cleanCurrency),
369
+ apiBaseUrlEnv: toEnvLiteral(cleanApiBaseUrl),
323
370
  language: options.language,
324
371
  direction,
325
372
  fontImport: fontConfig.fontImport,
326
373
  fontVariable: fontConfig.fontVariable,
327
374
  ogLocale,
328
- apiBaseUrl: options.apiBaseUrl || "https://api.brainerce.com",
329
375
  i18nEnabled: isMultiLocale,
330
376
  supportedLocales: JSON.stringify(supportedLocales),
331
377
  defaultLocale
@@ -588,11 +634,47 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
588
634
  }
589
635
  let connectionId = options.connectionId;
590
636
  const explicitApiUrl = (options.apiUrl || process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
637
+ const apiUrlError = validateApiUrl(explicitApiUrl);
638
+ if (apiUrlError) {
639
+ logger.error(apiUrlError);
640
+ process.exit(1);
641
+ }
642
+ if (explicitApiUrl && explicitApiUrl !== KNOWN_API_URLS.production && explicitApiUrl !== KNOWN_API_URLS.staging) {
643
+ logger.warn(
644
+ `Using non-standard API URL: ${explicitApiUrl} \u2014 make sure you trust this endpoint.`
645
+ );
646
+ }
591
647
  const candidateApiUrls = explicitApiUrl ? [explicitApiUrl] : [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
648
+ const VALID_LANGUAGES = ["en", "he"];
649
+ const VALID_PKG_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
650
+ const VALID_FRAMEWORKS = ["nextjs"];
651
+ const VALID_THEMES = ["minimal", "luxury", "playful"];
592
652
  let language = options.language;
653
+ if (language && !VALID_LANGUAGES.includes(language)) {
654
+ logger.error(
655
+ `Invalid --language "${language}". Expected one of: ${VALID_LANGUAGES.join(", ")}`
656
+ );
657
+ process.exit(1);
658
+ }
593
659
  let framework = options.framework;
660
+ if (!VALID_FRAMEWORKS.includes(framework)) {
661
+ logger.error(
662
+ `Invalid --framework "${framework}". Expected one of: ${VALID_FRAMEWORKS.join(", ")}`
663
+ );
664
+ process.exit(1);
665
+ }
594
666
  let theme = options.theme;
667
+ if (!VALID_THEMES.includes(theme)) {
668
+ logger.error(`Invalid --theme "${theme}". Expected one of: ${VALID_THEMES.join(", ")}`);
669
+ process.exit(1);
670
+ }
595
671
  let pkgManager = options.pkgManager;
672
+ if (pkgManager && !VALID_PKG_MANAGERS.includes(pkgManager)) {
673
+ logger.error(
674
+ `Invalid --pkg-manager "${pkgManager}". Expected one of: ${VALID_PKG_MANAGERS.join(", ")}`
675
+ );
676
+ process.exit(1);
677
+ }
596
678
  const skipGit = options.git === false;
597
679
  const skipInstall = options.install === false;
598
680
  let storeInfo = null;