4bnode 4.1.6 → 4.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4bnode",
3
- "version": "4.1.6",
3
+ "version": "4.1.8",
4
4
  "description": "4bnode is a CLI-powered backend development platform with a built-in visual dashboard to generate, manage, and test Node.js/Express APIs faster.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -121,6 +121,7 @@ export function MailPage() {
121
121
  async function save() {
122
122
  let body;
123
123
  if (provider === "resend") {
124
+ if (!form.resendFrom.trim()) { toast("From address is required", "error"); return; }
124
125
  if (!form.apiKey.trim() && !(info.resend && info.resend.hasKey)) { toast("API key is required", "error"); return; }
125
126
  body = { provider: "resend", apiKey: form.apiKey.trim(), from: form.resendFrom.trim(), domainPassword };
126
127
  } else {
@@ -239,7 +240,7 @@ export function MailPage() {
239
240
  <label className="form-label">Authorization password</label>
240
241
  <input className="form-input" type="password" value=${domainPassword} onInput=${(e) => setDomainPassword(e.target.value)} placeholder="Required for 4brains.in" />
241
242
  </div>`}
242
- <button className="btn btn-primary" onClick=${save} disabled=${busy || (provider === "smtp" && !form.host.trim()) || (isOfficial && !domainPassword)}>
243
+ <button className="btn btn-primary" onClick=${save} disabled=${busy || (provider === "smtp" && !form.host.trim()) || (provider === "resend" && !form.resendFrom.trim()) || (isOfficial && !domainPassword)}>
243
244
  ${busy ? html`<span className="spinner" />` : html`<${LuCheck} size=${16} />`}
244
245
  ${busy ? "Saving..." : "Save & install"}
245
246
  </button>
@@ -1372,11 +1372,11 @@ async function verifyMailConfig(envPath, provider) {
1372
1372
  if (provider === 'resend') {
1373
1373
  const key = getEnvValue(envPath, 'RESEND_API_KEY');
1374
1374
  if (!key) throw new Error('Resend API key is missing.');
1375
- let Resend;
1376
- try { ({ Resend } = await import('resend')); } catch { return; }
1377
- const resend = new Resend(key);
1378
- const { error } = await resend.domains.list();
1379
- if (error) throw new Error(error.message || 'Resend rejected the API key.');
1375
+ // Resend has no safe no-op verify — domains.list() needs a full-access key and
1376
+ // rejects normal send-only keys, which would fail valid configs. So validate the
1377
+ // key shape + From here, and let the "Send test" button confirm deliverability.
1378
+ if (!/^re_/.test(key)) throw new Error('That does not look like a Resend API key — it should start with "re_".');
1379
+ if (!getEnvValue(envPath, 'MAIL_FROM')) throw new Error('A From address is required for Postmaster.');
1380
1380
  return;
1381
1381
  }
1382
1382
  const host = getEnvValue(envPath, 'SMTP_HOST');
@@ -1456,6 +1456,7 @@ router.post('/api/mail/setup', async (req, res) => {
1456
1456
  if (from !== undefined) updateEnvFile(envPath, 'MAIL_FROM', String(from || '').trim());
1457
1457
  if (apiKey) updateEnvFile(envPath, 'RESEND_API_KEY', String(apiKey).trim());
1458
1458
  if (!getEnvValue(envPath, 'RESEND_API_KEY')) return res.status(400).json({ error: 'A Resend API key is required.' });
1459
+ if (!getEnvValue(envPath, 'MAIL_FROM')) return res.status(400).json({ error: 'A From address is required.' });
1459
1460
  await installDeps('resend');
1460
1461
  } else {
1461
1462
  const { host, port, secure, user, pass, from } = req.body || {};
package/skeleton/index.js CHANGED
@@ -50,13 +50,31 @@ app.use(cors({ origin: "*" }));
50
50
  // ── Security (built-in by default) ───────────────────
51
51
  // Mounted AFTER the /_dev dashboard so its CDN/inline scripts aren't blocked by
52
52
  // helmet's CSP and the dashboard isn't throttled by the rate limiter.
53
- app.use(helmet());
53
+ // helmet() defaults to a strict Content-Security-Policy (script-src 'self',
54
+ // etc.) that blocks CDN-loaded scripts, inline <script> blocks, and cross-origin
55
+ // assets. We widen the policy so a typical frontend/docs page works while keeping
56
+ // the rest of helmet's protections. Tighten these directives for production.
57
+ app.use(helmet({
58
+ contentSecurityPolicy: {
59
+ directives: {
60
+ defaultSrc: ["'self'"],
61
+ scriptSrc: ["'self'", 'https:', "'unsafe-inline'"],
62
+ styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
63
+ imgSrc: ["'self'", 'data:', 'https:'],
64
+ connectSrc: ["'self'"],
65
+ },
66
+ },
67
+ crossOriginEmbedderPolicy: false,
68
+ // Allow other origins (e.g. a separate frontend host) to load assets served
69
+ // by this app. Default is 'same-origin'. Tighten to 'same-site' for production.
70
+ crossOriginResourcePolicy: { policy: 'cross-origin' },
71
+ }));
54
72
 
55
73
  // ── Rate limiting ────────────────────────────────────
56
74
  // Tune via .env: RATE_LIMIT_WINDOW_MS (ms) and RATE_LIMIT_MAX (per window).
57
75
  const apiLimiter = rateLimit({
58
76
  windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
59
- max: Number(process.env.RATE_LIMIT_MAX) || 100,
77
+ max: Number(process.env.RATE_LIMIT_MAX) || 500,
60
78
  standardHeaders: true,
61
79
  legacyHeaders: false,
62
80
  });