aiplang 2.4.1 → 2.5.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/bin/aiplang.js +3 -3
- package/package.json +1 -1
- package/server/server.js +39 -4
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.5.1'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -454,7 +454,7 @@ if (cmd==='build') {
|
|
|
454
454
|
} else if(input.endsWith('.aiplang')&&fs.existsSync(input)){ files.push(input) }
|
|
455
455
|
if(!files.length){console.error(`\n ✗ No .aiplang files in: ${input}\n`);process.exit(1)}
|
|
456
456
|
const src=files.map(f=>fs.readFileSync(f,'utf8')).join('\n---\n')
|
|
457
|
-
const pages=
|
|
457
|
+
const pages=parsePages(src)
|
|
458
458
|
if(!pages.length){console.error('\n ✗ No pages found.\n');process.exit(1)}
|
|
459
459
|
fs.mkdirSync(outDir,{recursive:true})
|
|
460
460
|
console.log(`\n aiplang build v${VERSION} — ${files.length} file(s)\n`)
|
|
@@ -542,7 +542,7 @@ process.exit(1)
|
|
|
542
542
|
// PARSER
|
|
543
543
|
// ═════════════════════════════════════════════════════════════════
|
|
544
544
|
|
|
545
|
-
function
|
|
545
|
+
function parsePages(src) {
|
|
546
546
|
return src.split(/\n---\n/).map(s=>parsePage(s.trim())).filter(Boolean)
|
|
547
547
|
}
|
|
548
548
|
|
package/package.json
CHANGED
package/server/server.js
CHANGED
|
@@ -29,7 +29,15 @@ function persistDB() {
|
|
|
29
29
|
if (!_db || !DB_FILE || DB_FILE === ':memory:') return
|
|
30
30
|
try { fs.writeFileSync(DB_FILE, Buffer.from(_db.export())) } catch {}
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
let _dirty = false, _persistTimer = null
|
|
33
|
+
function dbRun(sql, params = []) {
|
|
34
|
+
_db.run(sql, params)
|
|
35
|
+
_dirty = true
|
|
36
|
+
if (!_persistTimer) _persistTimer = setTimeout(() => {
|
|
37
|
+
if (_dirty) { try { persistDB() } catch {} _dirty = false }
|
|
38
|
+
_persistTimer = null
|
|
39
|
+
}, 200)
|
|
40
|
+
}
|
|
33
41
|
function dbAll(sql, params = []) {
|
|
34
42
|
const stmt = _db.prepare(sql); stmt.bind(params)
|
|
35
43
|
const rows = []; while (stmt.step()) rows.push(stmt.getAsObject()); stmt.free()
|
|
@@ -95,7 +103,7 @@ async function sendMail(opts) {
|
|
|
95
103
|
return { messageId: 'mock-' + uuid() }
|
|
96
104
|
}
|
|
97
105
|
return MAIL_TRANSPORTER.sendMail({
|
|
98
|
-
from: MAIL_CONFIG?.from || 'noreply@
|
|
106
|
+
from: MAIL_CONFIG?.from || process.env.MAIL_FROM || 'noreply@localhost',
|
|
99
107
|
...opts
|
|
100
108
|
})
|
|
101
109
|
}
|
|
@@ -135,7 +143,10 @@ class Model {
|
|
|
135
143
|
if (this.softDelete) conditions.push('deleted_at IS NULL')
|
|
136
144
|
if (opts.where) { conditions.push(opts.where); if (opts.whereParams) params.push(...opts.whereParams) }
|
|
137
145
|
if (conditions.length) sql += ` WHERE ${conditions.join(' AND ')}`
|
|
138
|
-
if (opts.order)
|
|
146
|
+
if (opts.order) {
|
|
147
|
+
const safeOrder = /^[a-zA-Z_][a-zA-Z0-9_]*(\s+(asc|desc))?$/i
|
|
148
|
+
if (safeOrder.test(String(opts.order))) sql += ` ORDER BY ${opts.order}`
|
|
149
|
+
}
|
|
139
150
|
if (opts.limit) sql += ` LIMIT ${opts.limit}`
|
|
140
151
|
if (opts.offset) sql += ` OFFSET ${opts.offset}`
|
|
141
152
|
return dbAll(sql, params)
|
|
@@ -276,6 +287,15 @@ function migrateModels(models) {
|
|
|
276
287
|
if (!cols.some(c=>c.startsWith('updated_at'))) cols.push('updated_at TEXT')
|
|
277
288
|
if (model.softDelete) { if (!cols.some(c=>c.startsWith('deleted_at'))) cols.push('deleted_at TEXT') }
|
|
278
289
|
try { dbRun(`CREATE TABLE IF NOT EXISTS ${table} (${cols.join(', ')})`) } catch {}
|
|
290
|
+
// Auto-index on unique + indexed fields
|
|
291
|
+
for (const f of model.fields) {
|
|
292
|
+
const colName = toCol(f.name)
|
|
293
|
+
if (f.modifiers.includes('unique') || f.modifiers.includes('index')) {
|
|
294
|
+
try { dbRun(`CREATE INDEX IF NOT EXISTS idx_${table}_${colName} ON ${table}(${colName})`) } catch {}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Always index created_at for pagination performance
|
|
298
|
+
try { dbRun(`CREATE INDEX IF NOT EXISTS idx_${table}_created_at ON ${table}(created_at)`) } catch {}
|
|
279
299
|
console.log(`[aiplang] ✓ ${table} (${cols.length} cols${model.softDelete ? ', soft-delete' : ''})`)
|
|
280
300
|
MODEL_DEFS[model.name] = { softDelete: model.softDelete, timestamps: true }
|
|
281
301
|
}
|
|
@@ -796,6 +816,11 @@ class AiplangServer {
|
|
|
796
816
|
res.writeHead(429, { 'Content-Type': 'application/json' })
|
|
797
817
|
res.end(JSON.stringify({ error: 'Too many requests' })); return
|
|
798
818
|
}
|
|
819
|
+
// Auto rate-limit on auth endpoints
|
|
820
|
+
if (this._authRateLimit && this._authRateLimit(req)) {
|
|
821
|
+
res.writeHead(429, { 'Content-Type': 'application/json' })
|
|
822
|
+
res.end(JSON.stringify({ error: 'Too many requests. Try again in 1 minute.' })); return
|
|
823
|
+
}
|
|
799
824
|
|
|
800
825
|
// CORS — use plugin config if set, otherwise allow all
|
|
801
826
|
const origins = this._corsOrigins || ['*']
|
|
@@ -1294,6 +1319,16 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1294
1319
|
// Events
|
|
1295
1320
|
for (const ev of app.events) on(ev.event, (data) => console.log(`[aiplang:event] ${ev.event}:`, ev.action))
|
|
1296
1321
|
|
|
1322
|
+
// Auth rate limiting (automatic — 20 req/min per IP on /api/auth/*)
|
|
1323
|
+
const _authAttempts = {}
|
|
1324
|
+
srv._authRateLimit = (req) => {
|
|
1325
|
+
if (!req.path?.includes('/api/auth/')) return false
|
|
1326
|
+
const ip = req.socket?.remoteAddress || 'unknown'
|
|
1327
|
+
const key = `${ip}:${Math.floor(Date.now() / 60000)}`
|
|
1328
|
+
_authAttempts[key] = (_authAttempts[key] || 0) + 1
|
|
1329
|
+
return _authAttempts[key] > 20
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1297
1332
|
// Routes
|
|
1298
1333
|
for (const route of app.apis) {
|
|
1299
1334
|
compileRoute(route, srv)
|
|
@@ -1337,7 +1372,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1337
1372
|
|
|
1338
1373
|
// Health
|
|
1339
1374
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1340
|
-
status:'ok', version:'2.
|
|
1375
|
+
status:'ok', version:'2.5.0',
|
|
1341
1376
|
models: app.models.map(m=>m.name),
|
|
1342
1377
|
routes: app.apis.length, pages: app.pages.length,
|
|
1343
1378
|
admin: app.admin?.prefix || null,
|