aiplang 2.6.0 → 2.6.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.
@@ -1,338 +1,97 @@
1
- # aiplang — Claude Project Knowledge
1
+ # aiplang — syntax reference
2
+ Generate ONLY aiplang code. No React, HTML, or other frameworks. No explanations unless asked.
2
3
 
3
- You are an aiplang code generator. When asked to build any web app, page, or component, respond ONLY with valid aiplang syntax. No explanation unless asked. No React, no HTML, no other frameworks.
4
-
5
- ---
6
-
7
- ## aiplang syntax reference
8
-
9
- ### File structure
10
- ```
11
- ~theme ... (optional global theme vars)
12
- %id theme /route (page declaration — required)
13
- @var = default (reactive state)
14
- ~mount GET /api => @var (fetch on load)
15
- ~interval 10000 GET /api => @var (polling)
16
- blocks...
17
- --- (page separator)
18
- ```
19
-
20
- ### Backend (full-stack)
4
+ ## File structure
21
5
  ```
22
- ~env DATABASE_URL required
23
- ~db sqlite ./app.db (or postgres $DATABASE_URL)
24
- ~auth jwt $JWT_SECRET expire=7d
25
- ~mail smtp host=smtp.mailgun.com user=$MAIL_USER pass=$MAIL_PASS
26
- ~admin /admin (auto admin panel)
27
- ~middleware cors | rate-limit 100/min | log
28
-
29
- model User {
30
- id : uuid : pk auto
31
- name : text : required
32
- email : text : required unique
33
- password : text : required hashed
34
- plan : enum : starter,pro,enterprise : default=starter
35
- role : enum : user,admin : default=user
36
- ~soft-delete
37
- }
38
-
39
- api POST /api/auth/register {
40
- ~validate name required | email required email | password min=8
41
- ~unique User email $body.email | 409
42
- ~hash password
43
- insert User($body)
44
- ~mail $inserted.email "Welcome!" "Your account is ready."
45
- return jwt($inserted) 201
46
- }
47
-
48
- api GET /api/users {
49
- ~guard admin
50
- ~query page=1 limit=20
51
- return User.paginate($page, $limit)
52
- }
53
-
54
- api DELETE /api/users/:id {
55
- ~guard auth | admin
56
- delete User($id)
57
- }
58
- ```
59
-
60
- ### Page declaration
61
- `%id theme /route`
62
- - themes: `dark` | `light` | `acid` | `#bg,#text,#accent`
63
-
64
- ### Global theme
65
- `~theme accent=#7c3aed radius=1.5rem font=Syne bg=#0a0a0a text=#fff surface=#111 navbg=#000 spacing=6rem`
66
-
67
- ### State & data
68
- ```
69
- @users = []
70
- @stats = {}
71
- ~mount GET /api/users => @users
72
- ~interval 30000 GET /api/stats => @stats
73
- ```
74
-
75
- ### S3 Storage
76
- ```
77
- ~s3 $AWS_ACCESS_KEY_ID secret=$AWS_SECRET_ACCESS_KEY bucket=$S3_BUCKET region=us-east-1
78
- ~s3 bucket=my-bucket region=us-east-1 prefix=uploads/ maxSize=5mb allow=image/jpeg,image/png,application/pdf
79
-
80
- # Cloudflare R2 compatible
81
- ~s3 $R2_KEY secret=$R2_SECRET bucket=my-bucket endpoint=https://xxx.r2.cloudflarestorage.com
82
-
83
- # MinIO local
84
- ~s3 $KEY secret=$SECRET bucket=local endpoint=http://localhost:9000
85
- ```
86
- Auto-generated routes: `POST /api/upload` | `DELETE /api/upload/:key` | `GET /api/upload/presign?key=x`
87
-
88
- Mock mode in dev: saves to `./uploads/` folder, serves via `/uploads/filename`.
89
-
90
- ### Plugin System
91
- ```
92
- # Built-in plugins (no file needed)
93
- ~use logger format=tiny
94
- ~use cors origins=https://myapp.com,https://www.myapp.com
95
- ~use rate-limit max=100 window=60s
96
- ~use helmet
97
- ~use compression
98
-
99
- # Local file plugin
100
- ~plugin ./plugins/my-plugin.js
101
-
102
- # npm package plugin
103
- ~plugin my-aiplang-plugin
104
- ```
105
-
106
- Plugin interface:
107
- ```js
108
- // plugins/my-plugin.js
109
- module.exports = {
110
- name: 'my-plugin',
111
- setup(server, app, utils) {
112
- // server.addRoute('GET', '/api/custom', handler)
113
- // utils.emit, utils.on, utils.dispatch, utils.dbRun, utils.uuid
114
- // utils.s3Upload, utils.generateJWT — all available
115
- }
116
- }
117
-
118
- // Factory with options
119
- module.exports = (opts) => ({
120
- name: 'my-plugin',
121
- setup(server, app, { opts, emit }) { ... }
122
- })
123
- ```
124
-
125
- ### Stripe Payments
126
- ```
127
- ~stripe $STRIPE_SECRET_KEY webhook=$STRIPE_WEBHOOK_SECRET success=/dashboard cancel=/pricing
128
- ~plan starter=price_xxx pro=price_yyy enterprise=price_zzz
129
- ```
130
- Auto-generated routes: `POST /api/stripe/checkout` | `POST /api/stripe/portal` | `GET /api/stripe/subscription` | `DELETE /api/stripe/subscription` | `POST /api/stripe/webhook`
131
-
132
- Webhooks handled: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`, `invoice.payment_succeeded`
133
-
134
- Guard: `~guard subscribed` — requires active subscription
135
-
136
- ### All blocks
137
-
138
- **nav** — `nav{Brand>/path:Link>/path:Link}` (auto mobile hamburger)
139
-
140
- **hero** — `hero{Title|Subtitle>/path:CTA>/path:CTA2}` or `hero{Title|Sub>/path:CTA|img:https://url}` (split layout)
141
-
142
- **stats** — `stats{@stats.users:Users|@stats.mrr:MRR|99.9%:Uptime}`
143
-
144
- **rowN** — `row3{rocket>Fast>Zero config.|shield>Secure>SOC2.|chart>Smart>Real-time.} animate:stagger`
145
-
146
- **sect** — `sect{Title|Optional body text}`
147
-
148
- **table** — `table @users { Name:name | Email:email | Plan:plan | edit PUT /api/users/{id} | delete /api/users/{id} | empty: No data. }`
149
-
150
- **form** — `form POST /api/users => @users.push($result) { Name:text:Alice | Email:email | Plan:select:starter,pro,enterprise }`
151
-
152
- **form with redirect** — `form POST /api/auth/login => redirect /dashboard { Email:email | Password:password }`
153
-
154
- **pricing** — `pricing{Starter>Free>3 projects>/signup:Get started|Pro>$29/mo>Unlimited>/signup:Start trial|Enterprise>Custom>SSO>/contact:Talk}`
155
-
156
- **faq** — `faq{How to start?>Sign up free.|Cancel anytime?>Yes, one click.}`
157
-
158
- **testimonial** — `testimonial{Alice Chen, CEO @ Acme|"Changed how we ship."|img:https://i.pravatar.cc/64?img=5}`
159
-
160
- **gallery** — `gallery{https://img1.jpg | https://img2.jpg | https://img3.jpg}`
161
-
162
- **btn** — `btn{Export CSV > GET /api/export}` or `btn{Delete all > DELETE /api/items > confirm:Are you sure?}`
163
-
164
- **select** — `select @filterVar { All | Active | Inactive }`
165
-
166
- **raw** — `raw{<div style="...">Any HTML, embeds, custom components</div>}`
167
-
168
- **if** — `if @user { sect{Welcome back!} }`
169
-
170
- **foot** — `foot{© 2025 AppName>/privacy:Privacy>/terms:Terms}`
171
-
172
- ### Block modifiers (suffix on any block)
173
- ```
174
- hero{...} animate:blur-in
175
- row3{...} animate:stagger class:my-section
176
- sect{...} animate:fade-up
177
- ```
178
- Animations: `fade-up` `fade-in` `blur-in` `slide-left` `slide-right` `zoom-in` `stagger`
179
-
180
- ### Multiple pages
181
- ```
182
- %home dark /
183
- nav{...}
184
- hero{...}
185
- ---
186
- %dashboard dark /dashboard
187
- @users = []
188
- ~mount GET /api/users => @users
189
- table @users { ... }
190
- ---
191
- %login dark /login
192
- form POST /api/auth/login => redirect /dashboard { ... }
193
- ```
194
-
195
- ---
196
-
197
- ## Complete examples
198
-
199
- ### SaaS with 4 pages
200
- ```
201
- ~theme accent=#2563eb
202
- ~db sqlite ./app.db
6
+ ~env VAR required # env validation
7
+ ~db sqlite ./app.db # or: postgres $DATABASE_URL
203
8
  ~auth jwt $JWT_SECRET expire=7d
9
+ ~mail smtp host=x user=$U pass=$P
10
+ ~s3 $KEY secret=$S bucket=$B region=us-east-1 prefix=uploads/ maxSize=10mb
11
+ ~stripe $KEY webhook=$WH success=/ok cancel=/pricing
12
+ ~plan free=price_x pro=price_y
204
13
  ~admin /admin
14
+ ~use cors origins=https://x.com
15
+ ~use rate-limit max=100 window=60s
16
+ ~use helmet | ~use logger | ~use compression
17
+ ~plugin ./my-plugin.js
205
18
 
206
- model User {
207
- id : uuid : pk auto
208
- name : text : required
209
- email : text : required unique
210
- password : text : required hashed
211
- plan : enum : starter,pro,enterprise : default=starter
212
- role : enum : user,admin : default=user
19
+ model Name {
20
+ id : uuid : pk auto
21
+ field : type : modifier
213
22
  ~soft-delete
23
+ ~belongs OtherModel
214
24
  }
25
+ # types: uuid text int float bool timestamp json enum
26
+ # modifiers: pk auto required unique hashed default=val index
215
27
 
216
- api POST /api/auth/register {
217
- ~validate name required | email required email | password min=8
218
- ~unique User email $body.email | 409
219
- ~hash password
220
- insert User($body)
221
- return jwt($inserted) 201
222
- }
223
-
224
- api POST /api/auth/login {
225
- $user = User.findBy(email=$body.email)
226
- ~check password $body.password $user.password | 401
28
+ api METHOD /path/:id {
29
+ ~guard auth | admin | subscribed | owner
30
+ ~validate field required | field email | field min=8 | field numeric
31
+ ~query page=1 limit=20
32
+ ~unique Model field $body.field | 409
33
+ ~hash field
34
+ ~check password $body.pw $user.pw | 401
35
+ ~mail $user.email "Subject" "Body"
36
+ ~dispatch jobName $body
37
+ ~emit event.name $body
38
+ $var = Model.findBy(field=$body.field)
39
+ insert Model($body)
40
+ update Model($id, $body)
41
+ delete Model($id)
42
+ restore Model($id)
43
+ return $inserted | $updated | $auth.user | Model.all(order=created_at desc)
44
+ return Model.paginate($page, $limit)
45
+ return Model.count() | Model.sum(field) | Model.avg(field)
227
46
  return jwt($user) 200
228
47
  }
229
48
 
230
- api GET /api/users {
231
- ~guard admin
232
- return User.paginate(1, 20)
233
- }
234
-
235
- api GET /api/stats {
236
- return User.count()
237
- }
238
-
239
- %home dark /
240
-
241
- @stats = {}
242
- ~mount GET /api/stats => @stats
49
+ %id theme /route # dark | light | acid | #bg,#text,#accent
50
+ ~theme accent=#hex bg=#hex text=#hex font=Name radius=1rem surface=#hex navbg=#hex
51
+ @var = [] # state: [] or {} or "string" or 0
52
+ ~mount GET /api => @var
53
+ ~interval 10000 GET /api => @var
243
54
 
244
- nav{MySaaS>/pricing:Pricing>/login:Sign in>/signup:Get started}
245
- hero{Ship faster with AI|Zero config. Deploy in seconds.>/signup:Start free>/demo:View demo} animate:blur-in
246
- stats{@stats:Users|99.9%:Uptime|$49:Starting price}
247
- row3{rocket>Deploy instantly>Push to git, live in seconds.|shield>Enterprise ready>SOC2, GDPR, SSO built-in.|chart>Full observability>Real-time errors and performance.} animate:stagger
248
- testimonial{Sarah Chen, CEO @ Acme|"Cut deployment time by 90%."|img:https://i.pravatar.cc/64?img=47} animate:fade-up
249
- pricing{Starter>Free>3 projects>/signup:Get started|Pro>$29/mo>Unlimited>/signup:Start trial|Enterprise>Custom>SSO>/contact:Talk}
250
- faq{How to start?>Sign up free, no credit card.|Cancel anytime?>Yes, one click, no questions.}
251
- foot{© 2025 MySaaS>/privacy:Privacy>/terms:Terms}
252
-
253
- ---
254
-
255
- %dashboard dark /dashboard
256
-
257
- @users = []
258
- @stats = {}
259
- ~mount GET /api/users => @users
260
- ~mount GET /api/stats => @stats
261
- ~interval 30000 GET /api/stats => @stats
262
-
263
- nav{MySaaS>/settings:Settings>/logout:Sign out}
264
- stats{@stats:Total users|@stats:Active|$0:MRR}
265
- sect{User database}
266
- table @users { Name:name | Email:email | Plan:plan | Status:status | edit PUT /api/users/{id} | delete /api/users/{id} | empty: No users yet. }
267
- sect{Add user}
268
- form POST /api/users => @users.push($result) { Full name:text:Alice Johnson | Email:email:alice@co.com | Plan:select:starter,pro,enterprise }
269
- foot{MySaaS Dashboard © 2025}
270
-
271
- ---
272
-
273
- %login dark /login
274
-
275
- nav{MySaaS>/signup:Create account}
276
- hero{Welcome back|Sign in to continue.}
277
- form POST /api/auth/login => redirect /dashboard { Email:email:you@company.com | Password:password: }
278
- foot{© 2025 MySaaS>/signup:Create account}
279
-
280
- ---
281
-
282
- %signup dark /signup
283
-
284
- nav{MySaaS>/login:Sign in}
285
- hero{Start for free|No credit card required.}
286
- form POST /api/auth/register => redirect /dashboard { Full name:text:Alice | Email:email:alice@co.com | Password:password: }
287
- foot{© 2025 MySaaS>/login:Already have an account?}
288
- ```
289
-
290
- ### Landing page with custom theme
291
- ```
292
- ~theme accent=#f59e0b radius=2rem font=Syne bg=#0c0a09 text=#fafaf9 surface=#1c1917
293
-
294
- %home dark /
295
-
296
- nav{Acme Studio>/work:Work>/blog:Blog>/contact:Contact}
297
- hero{We build things that matter|Creative studio based in São Paulo.>/work:View our work>/contact:Get in touch|img:https://images.unsplash.com/photo-1497366216548?w=800} animate:fade-in
298
- row3{globe>Global clients>Teams in 30+ countries.|star>Award winning>12 design awards.|check>On-time delivery>98% on schedule.} animate:stagger
299
- testimonial{Marco Silva, CTO @ FinTech BR|"From prototype to production in 6 weeks."|img:https://i.pravatar.cc/64?img=12}
300
- gallery{https://images.unsplash.com/photo-1600880292203?w=400|https://images.unsplash.com/photo-1522202176988?w=400|https://images.unsplash.com/photo-1497366412874?w=400}
301
- foot{© 2025 Acme Studio>/privacy:Privacy>/instagram:Instagram}
302
- ```
303
-
304
- ---
305
-
306
- ## Generation rules
307
-
308
- 1. Always start with `%id theme /route`
309
- 2. Use `dark` theme unless specified otherwise
310
- 3. For dynamic data, always declare `@var = []` or `@var = {}` and use `~mount`
311
- 4. Tables with data should always have `edit` and `delete` unless readonly
312
- 5. Forms should have `=> @list.push($result)` or `=> redirect /path`
313
- 6. Use real icon names: bolt rocket shield chart star check globe gear fire money bell mail user
314
- 7. Multiple pages separated by `---`
315
- 8. Add `animate:fade-up` or `animate:stagger` to key sections
316
- 9. `~theme` always comes before `%` declarations
317
- 10. Never generate explanations — only aiplang code
318
- 11. For full-stack apps, add `~db`, `~auth`, `model` and `api` blocks before pages
319
-
320
- ---
321
-
322
- ## Running
323
-
324
- ```bash
325
- # Install
326
- npm install -g aiplang
327
-
328
- # Frontend only (static site)
329
- aiplang serve # dev → localhost:3000
330
- aiplang build pages/ # compile → dist/
331
-
332
- # Full-stack (Node.js backend)
333
- aiplang start app.aiplang
334
-
335
- # Go binary (production, v2)
336
- aiplangd dev app.aiplang
337
- aiplangd build app.aiplang
338
- ```
55
+ blocks...
56
+ --- # page separator
57
+ ```
58
+
59
+ ## All blocks
60
+ ```
61
+ nav{Brand>/path:Link>/path:Link}
62
+ hero{Title|Sub>/path:CTA} | hero{Title|Sub>/path:CTA|img:https://url}
63
+ stats{@val:Label|99%:Uptime|$0:Free}
64
+ row2{icon>Title>Body} | row3{...} | row4{...}
65
+ sect{Title|Optional body}
66
+ table @list { Col:field | edit PUT /api/{id} | delete /api/{id} | empty: msg }
67
+ form POST /api => @list.push($result) { Label:type:placeholder | Label:select:a,b,c }
68
+ form POST /api => redirect /path { Label:type | Label:password }
69
+ pricing{Name>Price>Desc>/path:CTA|Name>Price>Desc>/path:CTA}
70
+ faq{Question?>Answer.|Q2?>A2.}
71
+ testimonial{Name, Role @ Co|"Quote."|img:https://url}
72
+ gallery{https://img1|https://img2|https://img3}
73
+ btn{Label > METHOD /api/path} | btn{Label > DELETE /api > confirm:Sure?}
74
+ select @filterVar { All | Active | Inactive }
75
+ if @var { blocks }
76
+ raw{<div>any HTML</div>}
77
+ foot{© 2025 Name>/path:Link}
78
+ ```
79
+
80
+ ## Block modifiers (any block)
81
+ `animate:fade-up | fade-in | blur-in | slide-left | slide-right | zoom-in | stagger`
82
+ `class:my-class`
83
+
84
+ ## S3 auto-routes
85
+ `POST /api/upload` · `DELETE /api/upload/:key` · `GET /api/upload/presign?key=x`
86
+
87
+ ## Stripe auto-routes
88
+ `POST /api/stripe/checkout` · `POST /api/stripe/portal` · `GET /api/stripe/subscription`
89
+
90
+ ## Rules
91
+ 1. Dark theme default
92
+ 2. `@var = []` + `~mount` for all dynamic data
93
+ 3. Tables always have `edit` + `delete` unless readonly
94
+ 4. Forms: `=> @list.push($result)` or `=> redirect /path`
95
+ 5. `~theme` before `%` declarations
96
+ 6. Separate pages with `---`
97
+ 7. Full-stack: `~db` + `~auth` + `model` + `api` before pages
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.6.0'
8
+ const VERSION = '2.6.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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.6.0",
3
+ "version": "2.6.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",
@@ -38,12 +38,12 @@
38
38
  "node": ">=16"
39
39
  },
40
40
  "dependencies": {
41
+ "@aws-sdk/client-s3": "^3.0.0",
42
+ "@aws-sdk/s3-request-presigner": "^3.0.0",
41
43
  "bcryptjs": "^2.4.3",
42
44
  "jsonwebtoken": "^9.0.2",
43
- "nodemailer": "^6.9.0",
45
+ "nodemailer": "^8.0.3",
44
46
  "sql.js": "^1.10.3",
45
- "stripe": "^14.0.0",
46
- "@aws-sdk/client-s3": "^3.0.0",
47
- "@aws-sdk/s3-request-presigner": "^3.0.0"
47
+ "stripe": "^14.0.0"
48
48
  }
49
49
  }
@@ -100,14 +100,11 @@ class State {
100
100
  }
101
101
 
102
102
  _recompute() {
103
+ // Safe computed: only supports @var.path expressions, no arbitrary code
103
104
  if (!this._computedExprs) return
104
105
  for (const [name, expr] of Object.entries(this._computedExprs)) {
105
106
  try {
106
- const fn = new Function(
107
- ...Object.keys(this._data).map(k => '_'+k),
108
- `try { return (${expr}) } catch(e) { return null }`
109
- )
110
- const val = fn(...Object.keys(this._data).map(k => this._data[k]))
107
+ const val = this.eval(expr.trim())
111
108
  if (JSON.stringify(this._computed[name]) !== JSON.stringify(val)) {
112
109
  this._computed[name] = val
113
110
  this._notify('$'+name)
package/server/server.js CHANGED
@@ -752,13 +752,14 @@ td{padding:.875rem 1.25rem;border-bottom:1px solid rgba(255,255,255,.04);color:#
752
752
  </div>
753
753
  <script>
754
754
  const prefix = '${prefix}'
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
- }
755
+ // API calls use credentials:include HttpOnly cookie is sent automatically
760
756
  async function api(method, path, body) {
761
- const r = await fetch(prefix + '/api' + path, {method, headers:{'Content-Type':'application/json','Authorization':'Bearer '+getAdminToken()},body:body?JSON.stringify(body):undefined})
757
+ const r = await fetch(prefix + '/api' + path, {
758
+ method,
759
+ headers: {'Content-Type':'application/json'},
760
+ credentials: 'same-origin',
761
+ body: body ? JSON.stringify(body) : undefined
762
+ })
762
763
  return r.json()
763
764
  }
764
765
  async function loadModel(name, page=1) {
@@ -794,13 +795,9 @@ function renderAdminLogin(prefix) {
794
795
  <button onclick="login()">Sign in</button></div>
795
796
  <script>
796
797
  async function login(){
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})})
798
+ const r=await fetch('${prefix}/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:document.getElementById('email').value,password:document.getElementById('pass').value}),credentials:'same-origin'})
798
799
  const d=await r.json()
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
- }
800
+ if(d.ok){location.href='${prefix}'}
804
801
  else document.getElementById('err').textContent=d.error||'Invalid credentials'
805
802
  }
806
803
  document.addEventListener('keydown',e=>{if(e.key==='Enter')login()})
@@ -891,7 +888,16 @@ function matchRoute(pattern, reqPath) {
891
888
  for(let i=0;i<pp.length;i++){if(pp[i].startsWith(':'))params[pp[i].slice(1)]=rp[i];else if(pp[i]!==rp[i])return null}
892
889
  return params
893
890
  }
894
- function extractToken(req) { const a=req.headers.authorization; return a?.startsWith('Bearer ')?a.slice(7):null }
891
+ function extractToken(req) {
892
+ // 1. Bearer token from Authorization header
893
+ const a = req.headers.authorization
894
+ if (a?.startsWith('Bearer ')) return a.slice(7)
895
+ // 2. HttpOnly cookie (admin panel)
896
+ const cookies = req.headers['cookie'] || ''
897
+ const adminCookie = cookies.split(';').map(s=>s.trim()).find(s=>s.startsWith('aiplang_admin='))
898
+ if (adminCookie) return adminCookie.slice('aiplang_admin='.length)
899
+ return null
900
+ }
895
901
  const MAX_BODY_BYTES = parseInt(process.env.MAX_BODY_BYTES || '1048576') // 1MB default
896
902
  async function parseBody(req) {
897
903
  return new Promise((resolve) => {
@@ -1429,7 +1435,7 @@ async function startServer(aipFile, port = 3000) {
1429
1435
 
1430
1436
  // Health
1431
1437
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
1432
- status:'ok', version:'2.6.0',
1438
+ status:'ok', version:'2.6.1',
1433
1439
  models: app.models.map(m=>m.name),
1434
1440
  routes: app.apis.length, pages: app.pages.length,
1435
1441
  admin: app.admin?.prefix || null,