aiplang 2.5.0 → 2.6.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/bin/aiplang.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.5.0'
8
+ const VERSION = '2.6.0'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "AI-first full-stack language. Frontend + Backend + DB + Auth in one file. Competes with Laravel.",
5
5
  "keywords": [
6
6
  "aiplang",
@@ -77,7 +77,27 @@ function applyAction(data, target, action) {
77
77
  const pm = action.match(/^@([a-zA-Z_]+)\.push\(\$result\)$/)
78
78
  if (pm) { set(pm[1], [...(get(pm[1]) || []), data]); return }
79
79
  const fm = action.match(/^@([a-zA-Z_]+)\.filter\((.+)\)$/)
80
- if (fm) { try { set(fm[1], (get(fm[1])||[]).filter(new Function('item', `return (${fm[2]})(item)`))) } catch {} return }
80
+ if (fm) {
81
+ // Safe filter: @list.filter(item.status=active) style — no eval/new Function
82
+ try {
83
+ const expr = fm[2].trim()
84
+ const filtered = (get(fm[1]) || []).filter(item => {
85
+ // Support simple: field=value or field!=value
86
+ const eq = expr.match(/^([a-zA-Z_.]+)\s*(!?=)\s*(.+)$/)
87
+ if (eq) {
88
+ const [, field, op, val] = eq
89
+ const parts = field.split('.')
90
+ let v = item
91
+ for (const p of parts) v = v?.[p]
92
+ const strV = String(v ?? '')
93
+ return op === '!=' ? strV !== val.trim() : strV === val.trim()
94
+ }
95
+ return true
96
+ })
97
+ set(fm[1], filtered)
98
+ } catch {}
99
+ return
100
+ }
81
101
  const am = action.match(/^@([a-zA-Z_]+)\s*=\s*\$result$/)
82
102
  if (am) { set(am[1], data); return }
83
103
  }
package/server/server.js CHANGED
@@ -53,6 +53,14 @@ const ic = n => ({bolt:'⚡',rocket:'🚀',shield:'🛡',chart:'📊',star:'
53
53
 
54
54
  // ── JWT ───────────────────────────────────────────────────────────
55
55
  let JWT_SECRET = process.env.JWT_SECRET || 'aiplang-secret-dev'
56
+ // Warn loudly if using dev secret in what looks like production
57
+ if (!process.env.JWT_SECRET) {
58
+ if (process.env.NODE_ENV === 'production') {
59
+ console.error('[aiplang] FATAL: JWT_SECRET not set in production. Set JWT_SECRET env var.')
60
+ process.exit(1)
61
+ }
62
+ console.warn('[aiplang] WARNING: JWT_SECRET not set. Using insecure dev default. Set JWT_SECRET in .env')
63
+ }
56
64
  let JWT_EXPIRE = '7d'
57
65
  const generateJWT = (user) => jwt.sign({ id: user.id, email: user.email, role: user.role || 'user' }, JWT_SECRET, { expiresIn: JWT_EXPIRE })
58
66
  const verifyJWT = (token) => { try { return jwt.verify(token, JWT_SECRET) } catch { return null } }
@@ -103,7 +111,7 @@ async function sendMail(opts) {
103
111
  return { messageId: 'mock-' + uuid() }
104
112
  }
105
113
  return MAIL_TRANSPORTER.sendMail({
106
- from: MAIL_CONFIG?.from || 'noreply@aiplang.app',
114
+ from: MAIL_CONFIG?.from || process.env.MAIL_FROM || 'noreply@localhost',
107
115
  ...opts
108
116
  })
109
117
  }
@@ -638,7 +646,7 @@ function resolveVar(expr, ctx) {
638
646
  }
639
647
  function evalMath(expr,ctx){try{const r=expr.replace(/\$[\w.]+/g,m=>resolveVar(m,ctx)||0);return Function('"use strict";return('+r+')')()}catch{return 0}}
640
648
  function sanitize(o){if(!o)return o;const s={...o};delete s.password;return s}
641
- function resolveEnv(v){if(!v)return v;if(v.startsWith('$'))return process.env[v.slice(1)]||v;return v}
649
+ function resolveEnv(v){if(!v)return v;if(v.startsWith('$'))return process.env[v.slice(1)]||null;return v}
642
650
 
643
651
  // ═══════════════════════════════════════════════════════════════════
644
652
  // AUTO ADMIN PANEL
@@ -744,9 +752,13 @@ td{padding:.875rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.04);color:#
744
752
  </div>
745
753
  <script>
746
754
  const prefix = '${prefix}'
747
- const token = localStorage.getItem('admin_token') || ''
755
+ // Token from cookie (set by login) or fallback to localStorage for compat
756
+ function getAdminToken() {
757
+ const cookie = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('aiplang_admin='))
758
+ return cookie ? cookie.split('=')[1] : (localStorage.getItem('admin_token') || '')
759
+ }
748
760
  async function api(method, path, body) {
749
- const r = await fetch(prefix + '/api' + path, {method, headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},body:body?JSON.stringify(body):undefined})
761
+ const r = await fetch(prefix + '/api' + path, {method, headers:{'Content-Type':'application/json','Authorization':'Bearer '+getAdminToken()},body:body?JSON.stringify(body):undefined})
750
762
  return r.json()
751
763
  }
752
764
  async function loadModel(name, page=1) {
@@ -784,7 +796,11 @@ function renderAdminLogin(prefix) {
784
796
  async function login(){
785
797
  const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:document.getElementById('email').value,password:document.getElementById('pass').value})})
786
798
  const d=await r.json()
787
- if(d.token){localStorage.setItem('admin_token',d.token);location.href='${prefix}'}
799
+ if(d.token){
800
+ localStorage.setItem('admin_token',d.token);
801
+ document.cookie='aiplang_admin='+d.token+';path=/;SameSite=Strict;max-age=86400';
802
+ location.href='${prefix}'
803
+ }
788
804
  else document.getElementById('err').textContent=d.error||'Invalid credentials'
789
805
  }
790
806
  document.addEventListener('keydown',e=>{if(e.key==='Enter')login()})
@@ -804,8 +820,14 @@ class AiplangServer {
804
820
 
805
821
  // Multipart — don't pre-parse, let S3 upload handler do it
806
822
  const isMultipart = (req.headers['content-type'] || '').includes('multipart/form-data')
807
- if (req.method !== 'GET' && req.method !== 'DELETE' && !isMultipart) req.body = await parseBody(req)
808
- else if (!isMultipart) req.body = {}
823
+ const hasJsonCT = (req.headers['content-type'] || '').includes('application/json')
824
+ if (req.method !== 'GET' && req.method !== 'DELETE' && !isMultipart) {
825
+ req.body = await parseBody(req)
826
+ if (req.body.__tooBig) {
827
+ res.writeHead(413, { 'Content-Type': 'application/json' })
828
+ res.end(JSON.stringify({ error: 'Request body too large' })); return
829
+ }
830
+ } else if (!isMultipart) req.body = {}
809
831
 
810
832
  const parsed = url.parse(req.url, true)
811
833
  req.query = parsed.query; req.path = parsed.pathname
@@ -825,6 +847,11 @@ class AiplangServer {
825
847
  // CORS — use plugin config if set, otherwise allow all
826
848
  const origins = this._corsOrigins || ['*']
827
849
  const origin = req.headers['origin'] || ''
850
+ // Warn once if using wildcard CORS in production
851
+ if (origins.includes('*') && process.env.NODE_ENV === 'production' && !this._corsWarned) {
852
+ this._corsWarned = true
853
+ console.warn('[aiplang] WARNING: CORS is set to * (allow all origins). Use ~use cors origins=https://yourdomain.com in production')
854
+ }
828
855
  const allowOrigin = origins.includes('*') ? '*' : (origins.includes(origin) ? origin : origins[0])
829
856
  res.setHeader('Access-Control-Allow-Origin', allowOrigin)
830
857
  res.setHeader('Access-Control-Allow-Methods','GET,POST,PUT,PATCH,DELETE,OPTIONS')
@@ -865,8 +892,25 @@ function matchRoute(pattern, reqPath) {
865
892
  return params
866
893
  }
867
894
  function extractToken(req) { const a=req.headers.authorization; return a?.startsWith('Bearer ')?a.slice(7):null }
895
+ const MAX_BODY_BYTES = parseInt(process.env.MAX_BODY_BYTES || '1048576') // 1MB default
868
896
  async function parseBody(req) {
869
- return new Promise(r=>{let d='';req.on('data',c=>d+=c);req.on('end',()=>{try{r(JSON.parse(d))}catch{r({})}});req.on('error',()=>r({}))})
897
+ return new Promise((resolve) => {
898
+ const chunks = []
899
+ let size = 0, done = false
900
+ req.on('data', chunk => {
901
+ if (done) return
902
+ size += chunk.length
903
+ if (size > MAX_BODY_BYTES) { done = true; resolve({ __tooBig: true }); return }
904
+ chunks.push(chunk)
905
+ })
906
+ req.on('end', () => {
907
+ if (done) return
908
+ done = true
909
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString())) }
910
+ catch { resolve({}) }
911
+ })
912
+ req.on('error', () => { if (!done) { done = true; resolve({}) } })
913
+ })
870
914
  }
871
915
 
872
916
  // ═══════════════════════════════════════════════════════════════════
@@ -954,7 +998,7 @@ function setupS3(config) {
954
998
  endpoint: config.endpoint ? resolveEnv(config.endpoint) : null,
955
999
  }
956
1000
 
957
- const isMock = !S3_CONFIG.key || S3_CONFIG.key.startsWith('$') || S3_CONFIG.key.includes('mock')
1001
+ const isMock = !S3_CONFIG.key || S3_CONFIG.key === null || S3_CONFIG.key.startsWith('$') || S3_CONFIG.key.includes('mock')
958
1002
  if (isMock) {
959
1003
  console.log('[aiplang] S3: mock mode (set AWS_ACCESS_KEY_ID for real storage)')
960
1004
  S3_CLIENT = null
@@ -1289,6 +1333,19 @@ async function startServer(aipFile, port = 3000) {
1289
1333
  const app = parseApp(src)
1290
1334
  const srv = new AiplangServer()
1291
1335
 
1336
+ // Validate required env vars up front
1337
+ const missingEnvs = []
1338
+ for (const envDef of app.env) {
1339
+ if (envDef.required && !process.env[envDef.name]) {
1340
+ missingEnvs.push(envDef.name)
1341
+ }
1342
+ }
1343
+ if (missingEnvs.length) {
1344
+ console.error(`[aiplang] FATAL: Missing required env vars: ${missingEnvs.join(', ')}`)
1345
+ console.error('[aiplang] Set them in .env or export them before starting')
1346
+ process.exit(1)
1347
+ }
1348
+
1292
1349
  // Auth setup
1293
1350
  if (app.auth) {
1294
1351
  JWT_SECRET = resolveEnv(app.auth.secret) || JWT_SECRET
@@ -1372,7 +1429,7 @@ async function startServer(aipFile, port = 3000) {
1372
1429
 
1373
1430
  // Health
1374
1431
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
1375
- status:'ok', version:'2.1.3',
1432
+ status:'ok', version:'2.6.0',
1376
1433
  models: app.models.map(m=>m.name),
1377
1434
  routes: app.apis.length, pages: app.pages.length,
1378
1435
  admin: app.admin?.prefix || null,
@@ -1415,7 +1472,7 @@ function setupStripe(config) {
1415
1472
  STRIPE_CONFIG = config
1416
1473
  const key = resolveEnv(config.key) || ''
1417
1474
  // Use mock if key is placeholder, test/mock value, or SDK unavailable
1418
- const isMock = !key || key.startsWith('$') || key === 'sk_test_mock' || key.includes('mock')
1475
+ const isMock = !key || key === null || key.startsWith('$') || key === 'sk_test_mock' || key.includes('mock')
1419
1476
  if (isMock) {
1420
1477
  console.log('[aiplang] Stripe: mock mode (set STRIPE_SECRET_KEY for real payments)')
1421
1478
  STRIPE = null // will use mockStripe()