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.
- package/dist/index.js +93 -11
- package/messages/en.json +389 -382
- package/messages/he.json +389 -382
- package/package.json +46 -46
- package/templates/nextjs/base/.env.local.ejs +3 -3
- package/templates/nextjs/base/next.config.ts +32 -31
- package/templates/nextjs/base/package.json.ejs +2 -1
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -14
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -59
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -77
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +229 -198
- package/templates/nextjs/base/src/app/checkout/page.tsx +3 -1
- package/templates/nextjs/base/src/app/layout.tsx.ejs +29 -13
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +501 -501
- package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -131
- package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -232
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -415
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +656 -592
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +86 -72
- package/templates/nextjs/base/src/lib/csrf.ts +11 -0
- package/templates/nextjs/base/src/lib/nonce.ts +10 -0
- package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -0
- package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -0
- package/templates/nextjs/base/src/lib/validation.ts +37 -0
- package/templates/nextjs/base/src/middleware.ts.ejs +91 -8
- 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.
|
|
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
|
|
167
|
+
const cmd = process.platform === "win32" ? `${pkgManager}.cmd` : pkgManager;
|
|
144
168
|
const args = pkgManager === "yarn" ? [] : ["install"];
|
|
145
|
-
const child = spawn(
|
|
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
|
|
316
|
-
const supportedLocales =
|
|
317
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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;
|