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 +1 -1
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +21 -1
- package/server/server.js +68 -11
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.
|
|
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
|
@@ -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) {
|
|
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@
|
|
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)]||
|
|
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
|
-
|
|
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 '+
|
|
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){
|
|
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
|
-
|
|
808
|
-
|
|
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(
|
|
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.
|
|
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()
|