aiplang 2.4.0 → 2.5.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/README.md CHANGED
@@ -8,7 +8,7 @@ cd my-app
8
8
  npx aiplang serve
9
9
  ```
10
10
 
11
- Ask Claude to generate a page → paste into `pages/home.flux` → see it live.
11
+ Ask Claude to generate a page → paste into `pages/home.aiplang` → see it live.
12
12
 
13
13
  ---
14
14
 
@@ -16,9 +16,9 @@ Ask Claude to generate a page → paste into `pages/home.flux` → see it live.
16
16
 
17
17
  **aiplang** is a web language designed to be generated by AI (Claude), not written by humans.
18
18
 
19
- A single `.flux` file describes a complete app: frontend, backend, database, auth, email, jobs.
19
+ A single `.aiplang` file describes a complete app: frontend, backend, database, auth, email, jobs.
20
20
 
21
- ```flux
21
+ ```aiplang
22
22
  ~db sqlite ./app.db
23
23
  ~auth jwt $JWT_SECRET expire=7d
24
24
  ~admin /admin
@@ -83,7 +83,7 @@ npx aiplang init --template landing # landing page template
83
83
  npx aiplang init --template crud # CRUD app template
84
84
  npx aiplang serve # dev server + hot reload → localhost:3000
85
85
  npx aiplang build pages/ --out dist/ # compile → static HTML
86
- npx aiplang start app.flux # full-stack server (Node.js)
86
+ npx aiplang start app.aiplang # full-stack server (Node.js)
87
87
  npx aiplang new dashboard # create new page template
88
88
  ```
89
89
 
@@ -111,13 +111,13 @@ All blocks accept: animate:fade-up class:my-class | raw{<html>} | foot{text>/pat
111
111
 
112
112
  ```
113
113
  aiplang/
114
- ├── packages/flux-lang/ ← npm package (aiplang CLI + runtime)
114
+ ├── packages/aiplang-pkg/ ← npm package (aiplang CLI + runtime)
115
115
  │ ├── bin/aiplang.js ← CLI: init, serve, build, new, start
116
116
  │ ├── runtime/aiplang-hydrate.js← 10KB reactive runtime
117
117
  │ ├── server/server.js ← full-stack Node.js server
118
118
  │ └── aiplang-knowledge.md ← Claude Project knowledge file
119
119
  ├── aiplang-go/ ← Go compiler + server (v2)
120
- │ ├── compiler/compiler.go ← .flux → AST parser
120
+ │ ├── compiler/compiler.go ← .aiplang → AST parser
121
121
  │ ├── server/server.go ← Go HTTP server
122
122
  │ └── cmd/aiplangd/main.go ← binary entrypoint
123
123
  ├── docs/ ← GitHub Pages (aiplang.io)
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.0'
8
+ const VERSION = '2.5.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)
@@ -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.0",
3
+ "version": "2.5.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",
@@ -456,7 +456,7 @@ class Renderer {
456
456
 
457
457
  render(page) {
458
458
  this.container.innerHTML = ''
459
- this.container.className = `flux-root flux-theme-${page.theme}`
459
+ this.container.className = `aiplang-root aiplang-theme-${page.theme}`
460
460
  for (const block of page.blocks) {
461
461
  const el = this.renderBlock(block)
462
462
  if (el) this.container.appendChild(el)
@@ -1069,9 +1069,9 @@ input,button,select{font-family:inherit}
1069
1069
 
1070
1070
  function boot(src, container) {
1071
1071
  // Inject CSS once
1072
- if (!document.getElementById('flux-css')) {
1072
+ if (!document.getElementById('aiplang-css')) {
1073
1073
  const style = document.createElement('style')
1074
- style.id = 'flux-css'
1074
+ style.id = 'aiplang-css'
1075
1075
  style.textContent = CSS
1076
1076
  document.head.appendChild(style)
1077
1077
  }
@@ -1089,9 +1089,9 @@ return { boot, parseFlux, State, Renderer, Router, QueryEngine }
1089
1089
 
1090
1090
  })()
1091
1091
 
1092
- // Auto-boot from <script type="text/flux">
1092
+ // Auto-boot from <script type="text/aiplang">
1093
1093
  document.addEventListener('DOMContentLoaded', () => {
1094
- const script = document.querySelector('script[type="text/flux"]')
1094
+ const script = document.querySelector('script[type="text/aiplang"]')
1095
1095
  if (script) {
1096
1096
  const targetSel = script.getAttribute('target') || '#app'
1097
1097
  const container = document.querySelector(targetSel)
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()
@@ -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)