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.
- package/dist/index.js +120 -22
- 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 +31 -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 +88 -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 +103 -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.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
|
|
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:
|
|
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
|
-
|
|
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
|
|
316
|
-
const supportedLocales =
|
|
317
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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 {
|
|
799
|
+
const { execFileSync } = require("child_process");
|
|
707
800
|
const cwd = scaffoldInPlace ? "." : projectName;
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
|
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
|
);
|