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 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.4.1'
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=parseFlux(src)
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 parseFlux(src) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "description": "AI-first full-stack language. Frontend + Backend + DB + Auth in one file. Competes with Laravel.",
5
5
  "keywords": [
6
6
  "aiplang",
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
- function dbRun(sql, params = []) { _db.run(sql, params); persistDB() }
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@aiplang.app',
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) sql += ` ORDER BY ${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.1.3',
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,