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.
- package/aiplang-knowledge.md +85 -326
- package/bin/aiplang.js +1 -1
- package/package.json +5 -5
- package/runtime/aiplang-runtime.js +2 -5
- package/server/server.js +20 -14
package/aiplang-knowledge.md
CHANGED
|
@@ -1,338 +1,97 @@
|
|
|
1
|
-
# aiplang —
|
|
1
|
+
# aiplang — syntax reference
|
|
2
|
+
Generate ONLY aiplang code. No React, HTML, or other frameworks. No explanations unless asked.
|
|
2
3
|
|
|
3
|
-
|
|
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
|
|
23
|
-
~db sqlite ./app.db
|
|
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
|
|
207
|
-
id
|
|
208
|
-
|
|
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
|
|
217
|
-
~
|
|
218
|
-
~
|
|
219
|
-
~
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
|
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
|
-
//
|
|
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, {
|
|
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('/
|
|
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.
|
|
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) {
|
|
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.
|
|
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,
|