@xano/developer-mcp 1.0.7 → 1.0.9
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 +28 -0
- package/dist/index.js +42 -1
- package/dist/templates/xanoscript-index.js +7 -0
- package/package.json +1 -1
- package/xanoscript_docs/README.md +38 -5
- package/xanoscript_docs/addons.md +285 -0
- package/xanoscript_docs/agents.md +84 -0
- package/xanoscript_docs/database.md +160 -0
- package/xanoscript_docs/debugging.md +342 -0
- package/xanoscript_docs/functions.md +172 -0
- package/xanoscript_docs/integrations.md +376 -7
- package/xanoscript_docs/performance.md +407 -0
- package/xanoscript_docs/realtime.md +382 -0
- package/xanoscript_docs/schema.md +292 -0
- package/xanoscript_docs/security.md +550 -0
- package/xanoscript_docs/streaming.md +318 -0
- package/xanoscript_docs/syntax.md +267 -0
- package/xanoscript_docs/triggers.md +354 -52
- package/xanoscript_docs/types.md +66 -21
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "functions/**/*.xs, apis/**/*.xs"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Security
|
|
6
|
+
|
|
7
|
+
Best practices for building secure XanoScript applications.
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Area | Key Practices |
|
|
12
|
+
|------|---------------|
|
|
13
|
+
| Authentication | Tokens, sessions, MFA |
|
|
14
|
+
| Authorization | Role checks, resource ownership |
|
|
15
|
+
| Input Validation | Type enforcement, sanitization |
|
|
16
|
+
| Data Protection | Encryption, hashing, secrets |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
### Auth Token Generation
|
|
23
|
+
|
|
24
|
+
```xs
|
|
25
|
+
security.create_auth_token {
|
|
26
|
+
table = "user"
|
|
27
|
+
id = $user.id
|
|
28
|
+
extras = {
|
|
29
|
+
role: $user.role,
|
|
30
|
+
permissions: $user.permissions
|
|
31
|
+
}
|
|
32
|
+
expiration = 86400 // 24 hours
|
|
33
|
+
} as $token
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Token Verification
|
|
37
|
+
|
|
38
|
+
```xs
|
|
39
|
+
// $auth is automatically populated from valid token
|
|
40
|
+
precondition ($auth.id != null) {
|
|
41
|
+
error_type = "accessdenied"
|
|
42
|
+
error = "Authentication required"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Refresh Tokens
|
|
47
|
+
|
|
48
|
+
```xs
|
|
49
|
+
function "refresh_auth" {
|
|
50
|
+
input {
|
|
51
|
+
text refresh_token
|
|
52
|
+
}
|
|
53
|
+
stack {
|
|
54
|
+
// Verify refresh token
|
|
55
|
+
db.query "refresh_token" {
|
|
56
|
+
where = $db.refresh_token.token == $input.refresh_token
|
|
57
|
+
&& $db.refresh_token.expires_at > now
|
|
58
|
+
&& $db.refresh_token.revoked == false
|
|
59
|
+
return = { type: "single" }
|
|
60
|
+
} as $stored_token
|
|
61
|
+
|
|
62
|
+
precondition ($stored_token != null) {
|
|
63
|
+
error_type = "accessdenied"
|
|
64
|
+
error = "Invalid or expired refresh token"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get user
|
|
68
|
+
db.get "user" {
|
|
69
|
+
field_name = "id"
|
|
70
|
+
field_value = $stored_token.user_id
|
|
71
|
+
} as $user
|
|
72
|
+
|
|
73
|
+
// Generate new access token
|
|
74
|
+
security.create_auth_token {
|
|
75
|
+
table = "user"
|
|
76
|
+
id = $user.id
|
|
77
|
+
extras = { role: $user.role }
|
|
78
|
+
expiration = 3600
|
|
79
|
+
} as $new_token
|
|
80
|
+
|
|
81
|
+
// Rotate refresh token
|
|
82
|
+
var $new_refresh { value = |uuid }
|
|
83
|
+
|
|
84
|
+
db.edit "refresh_token" {
|
|
85
|
+
field_name = "id"
|
|
86
|
+
field_value = $stored_token.id
|
|
87
|
+
data = {
|
|
88
|
+
token: $new_refresh,
|
|
89
|
+
expires_at: now|transform_timestamp:"+30 days"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
response = {
|
|
94
|
+
access_token: $new_token,
|
|
95
|
+
refresh_token: $new_refresh
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Session Management
|
|
101
|
+
|
|
102
|
+
```xs
|
|
103
|
+
function "create_session" {
|
|
104
|
+
input { int user_id }
|
|
105
|
+
stack {
|
|
106
|
+
var $session_id { value = |uuid }
|
|
107
|
+
|
|
108
|
+
db.add "session" {
|
|
109
|
+
data = {
|
|
110
|
+
id: $session_id,
|
|
111
|
+
user_id: $input.user_id,
|
|
112
|
+
ip_address: $env.$remote_ip,
|
|
113
|
+
user_agent: $env.$http_headers|get:"user-agent",
|
|
114
|
+
created_at: now,
|
|
115
|
+
last_activity: now,
|
|
116
|
+
expires_at: now|transform_timestamp:"+7 days"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
response = $session_id
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function "validate_session" {
|
|
124
|
+
input { text session_id }
|
|
125
|
+
stack {
|
|
126
|
+
db.query "session" {
|
|
127
|
+
where = $db.session.id == $input.session_id
|
|
128
|
+
&& $db.session.expires_at > now
|
|
129
|
+
return = { type: "single" }
|
|
130
|
+
} as $session
|
|
131
|
+
|
|
132
|
+
precondition ($session != null) {
|
|
133
|
+
error_type = "accessdenied"
|
|
134
|
+
error = "Invalid or expired session"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Update last activity
|
|
138
|
+
db.edit "session" {
|
|
139
|
+
field_name = "id"
|
|
140
|
+
field_value = $session.id
|
|
141
|
+
data = { last_activity: now }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
response = $session
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Multi-Factor Authentication
|
|
149
|
+
|
|
150
|
+
```xs
|
|
151
|
+
function "verify_mfa" {
|
|
152
|
+
input {
|
|
153
|
+
int user_id
|
|
154
|
+
text code filters=digitOk|min:6|max:6
|
|
155
|
+
}
|
|
156
|
+
stack {
|
|
157
|
+
db.get "user" {
|
|
158
|
+
field_name = "id"
|
|
159
|
+
field_value = $input.user_id
|
|
160
|
+
} as $user
|
|
161
|
+
|
|
162
|
+
// Verify TOTP code
|
|
163
|
+
security.verify_totp {
|
|
164
|
+
secret = $user.mfa_secret
|
|
165
|
+
code = $input.code
|
|
166
|
+
window = 1
|
|
167
|
+
} as $is_valid
|
|
168
|
+
|
|
169
|
+
precondition ($is_valid) {
|
|
170
|
+
error_type = "accessdenied"
|
|
171
|
+
error = "Invalid MFA code"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
response = { verified: true }
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Authorization
|
|
181
|
+
|
|
182
|
+
### Role-Based Access Control
|
|
183
|
+
|
|
184
|
+
```xs
|
|
185
|
+
precondition ($auth.role == "admin") {
|
|
186
|
+
error_type = "accessdenied"
|
|
187
|
+
error = "Admin access required"
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Permission Checks
|
|
192
|
+
|
|
193
|
+
```xs
|
|
194
|
+
function "check_permission" {
|
|
195
|
+
input {
|
|
196
|
+
text permission
|
|
197
|
+
}
|
|
198
|
+
stack {
|
|
199
|
+
var $has_permission {
|
|
200
|
+
value = $auth.permissions|contains:$input.permission
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
precondition ($has_permission) {
|
|
204
|
+
error_type = "accessdenied"
|
|
205
|
+
error = "Missing permission: " ~ $input.permission
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Usage
|
|
211
|
+
function.run "check_permission" {
|
|
212
|
+
input = { permission: "users.delete" }
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Resource Ownership
|
|
217
|
+
|
|
218
|
+
```xs
|
|
219
|
+
// Verify user owns the resource
|
|
220
|
+
db.get "document" {
|
|
221
|
+
field_name = "id"
|
|
222
|
+
field_value = $input.document_id
|
|
223
|
+
} as $document
|
|
224
|
+
|
|
225
|
+
precondition ($document.owner_id == $auth.id) {
|
|
226
|
+
error_type = "accessdenied"
|
|
227
|
+
error = "You do not own this document"
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Hierarchical Access
|
|
232
|
+
|
|
233
|
+
```xs
|
|
234
|
+
function "can_access_resource" {
|
|
235
|
+
input {
|
|
236
|
+
int resource_id
|
|
237
|
+
text action
|
|
238
|
+
}
|
|
239
|
+
stack {
|
|
240
|
+
// Check direct ownership
|
|
241
|
+
db.query "resource" {
|
|
242
|
+
where = $db.resource.id == $input.resource_id
|
|
243
|
+
&& $db.resource.owner_id == $auth.id
|
|
244
|
+
return = { type: "exists" }
|
|
245
|
+
} as $is_owner
|
|
246
|
+
|
|
247
|
+
// Check team membership
|
|
248
|
+
db.query "team_member" {
|
|
249
|
+
join = {
|
|
250
|
+
resource: {
|
|
251
|
+
table: "resource",
|
|
252
|
+
where: $db.resource.team_id == $db.team_member.team_id
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
where = $db.resource.id == $input.resource_id
|
|
256
|
+
&& $db.team_member.user_id == $auth.id
|
|
257
|
+
return = { type: "exists" }
|
|
258
|
+
} as $is_team_member
|
|
259
|
+
|
|
260
|
+
// Check permissions
|
|
261
|
+
var $can_access {
|
|
262
|
+
value = $is_owner || ($is_team_member && $input.action != "delete")
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
response = $can_access
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Input Validation
|
|
272
|
+
|
|
273
|
+
### Type Enforcement
|
|
274
|
+
|
|
275
|
+
```xs
|
|
276
|
+
input {
|
|
277
|
+
email email filters=trim|lower
|
|
278
|
+
text password filters=min:8|max:128
|
|
279
|
+
int age filters=min:0|max:150
|
|
280
|
+
text username filters=trim|lower|min:3|max:20|alphaNumOk
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### SQL Injection Prevention
|
|
285
|
+
|
|
286
|
+
```xs
|
|
287
|
+
// Safe: Use parameterized queries (default behavior)
|
|
288
|
+
db.query "user" {
|
|
289
|
+
where = $db.user.email == $input.email
|
|
290
|
+
} as $user
|
|
291
|
+
|
|
292
|
+
// If using direct_query, use arg parameter
|
|
293
|
+
db.direct_query {
|
|
294
|
+
sql = "SELECT * FROM users WHERE email = ? AND status = ?"
|
|
295
|
+
arg = [$input.email, "active"]
|
|
296
|
+
} as $users
|
|
297
|
+
|
|
298
|
+
// NEVER: String concatenation in SQL
|
|
299
|
+
// sql = "SELECT * FROM users WHERE email = '" ~ $input.email ~ "'"
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### XSS Prevention
|
|
303
|
+
|
|
304
|
+
```xs
|
|
305
|
+
// Escape HTML content before storage or display
|
|
306
|
+
var $safe_content {
|
|
307
|
+
value = $input.content|escape
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// For rich text, use allowlist sanitization
|
|
311
|
+
var $sanitized {
|
|
312
|
+
value = $input.html|html_sanitize:["p", "b", "i", "a"]
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Path Traversal Prevention
|
|
317
|
+
|
|
318
|
+
```xs
|
|
319
|
+
// Validate file paths
|
|
320
|
+
precondition (!($input.filename|contains:"..")) {
|
|
321
|
+
error_type = "inputerror"
|
|
322
|
+
error = "Invalid filename"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
precondition ($input.filename|regex_matches:"/^[a-zA-Z0-9_.-]+$/") {
|
|
326
|
+
error_type = "inputerror"
|
|
327
|
+
error = "Filename contains invalid characters"
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Data Protection
|
|
334
|
+
|
|
335
|
+
### Password Hashing
|
|
336
|
+
|
|
337
|
+
```xs
|
|
338
|
+
// Passwords are automatically hashed when using password type
|
|
339
|
+
db.add "user" {
|
|
340
|
+
data = {
|
|
341
|
+
email: $input.email,
|
|
342
|
+
password: $input.password // Auto-hashed
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Verify password
|
|
347
|
+
security.check_password {
|
|
348
|
+
text_password = $input.password
|
|
349
|
+
hash_password = $user.password
|
|
350
|
+
} as $is_valid
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Encryption at Rest
|
|
354
|
+
|
|
355
|
+
```xs
|
|
356
|
+
// Encrypt sensitive data before storage
|
|
357
|
+
security.encrypt {
|
|
358
|
+
data = $input.ssn
|
|
359
|
+
algorithm = "aes-256-cbc"
|
|
360
|
+
key = $env.ENCRYPTION_KEY
|
|
361
|
+
iv = $env.ENCRYPTION_IV
|
|
362
|
+
} as $encrypted_ssn
|
|
363
|
+
|
|
364
|
+
db.add "user" {
|
|
365
|
+
data = { ssn_encrypted: $encrypted_ssn }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Decrypt when needed
|
|
369
|
+
security.decrypt {
|
|
370
|
+
data = $user.ssn_encrypted
|
|
371
|
+
algorithm = "aes-256-cbc"
|
|
372
|
+
key = $env.ENCRYPTION_KEY
|
|
373
|
+
iv = $env.ENCRYPTION_IV
|
|
374
|
+
} as $ssn
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Secrets Management
|
|
378
|
+
|
|
379
|
+
```xs
|
|
380
|
+
// Store secrets in environment variables
|
|
381
|
+
$env.API_SECRET_KEY
|
|
382
|
+
$env.DATABASE_URL
|
|
383
|
+
$env.ENCRYPTION_KEY
|
|
384
|
+
|
|
385
|
+
// NEVER hardcode secrets
|
|
386
|
+
// api_key = "sk_live_abc123" // BAD
|
|
387
|
+
|
|
388
|
+
// Mark sensitive inputs
|
|
389
|
+
input {
|
|
390
|
+
text api_key {
|
|
391
|
+
sensitive = true // Masked in logs
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### JWT Security
|
|
397
|
+
|
|
398
|
+
```xs
|
|
399
|
+
// Sign with strong algorithm
|
|
400
|
+
security.jws_encode {
|
|
401
|
+
claims = {
|
|
402
|
+
sub: $user.id|to_text,
|
|
403
|
+
role: $user.role,
|
|
404
|
+
iat: now|to_seconds,
|
|
405
|
+
exp: (now|to_seconds) + 3600
|
|
406
|
+
}
|
|
407
|
+
key = $env.JWT_SECRET
|
|
408
|
+
signature_algorithm = "HS256"
|
|
409
|
+
} as $token
|
|
410
|
+
|
|
411
|
+
// Verify with algorithm check
|
|
412
|
+
security.jws_decode {
|
|
413
|
+
token = $input.token
|
|
414
|
+
key = $env.JWT_SECRET
|
|
415
|
+
signature_algorithm = "HS256"
|
|
416
|
+
} as $claims
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Rate Limiting & Abuse Prevention
|
|
422
|
+
|
|
423
|
+
### API Rate Limiting
|
|
424
|
+
|
|
425
|
+
```xs
|
|
426
|
+
redis.ratelimit {
|
|
427
|
+
key = "api:" ~ $auth.id
|
|
428
|
+
max = 100
|
|
429
|
+
ttl = 60
|
|
430
|
+
error = "Rate limit exceeded"
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Login Attempt Limiting
|
|
435
|
+
|
|
436
|
+
```xs
|
|
437
|
+
function "check_login_attempts" {
|
|
438
|
+
input { text email }
|
|
439
|
+
stack {
|
|
440
|
+
var $key { value = "login_attempts:" ~ $input.email|md5 }
|
|
441
|
+
|
|
442
|
+
redis.get { key = $key } as $attempts
|
|
443
|
+
|
|
444
|
+
precondition (($attempts|to_int ?? 0) < 5) {
|
|
445
|
+
error_type = "accessdenied"
|
|
446
|
+
error = "Too many login attempts. Try again in 15 minutes."
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function "record_failed_login" {
|
|
452
|
+
input { text email }
|
|
453
|
+
stack {
|
|
454
|
+
var $key { value = "login_attempts:" ~ $input.email|md5 }
|
|
455
|
+
redis.incr { key = $key, by = 1 }
|
|
456
|
+
redis.expire { key = $key, ttl = 900 }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function "clear_login_attempts" {
|
|
461
|
+
input { text email }
|
|
462
|
+
stack {
|
|
463
|
+
redis.del { key = "login_attempts:" ~ $input.email|md5 }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Request Size Limits
|
|
469
|
+
|
|
470
|
+
```xs
|
|
471
|
+
// Enforce in input validation
|
|
472
|
+
input {
|
|
473
|
+
text content filters=max:10000
|
|
474
|
+
file upload filters=max_size:10485760 // 10MB
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Security Headers
|
|
481
|
+
|
|
482
|
+
### CORS Configuration
|
|
483
|
+
|
|
484
|
+
Configure CORS in API settings. For dynamic CORS:
|
|
485
|
+
|
|
486
|
+
```xs
|
|
487
|
+
// Validate origin
|
|
488
|
+
var $allowed_origins { value = ["https://app.example.com", "https://admin.example.com"] }
|
|
489
|
+
|
|
490
|
+
precondition ($allowed_origins|contains:$env.$http_headers|get:"origin") {
|
|
491
|
+
error_type = "accessdenied"
|
|
492
|
+
error = "Origin not allowed"
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Audit Logging
|
|
499
|
+
|
|
500
|
+
### Log Security Events
|
|
501
|
+
|
|
502
|
+
```xs
|
|
503
|
+
function "audit_log" {
|
|
504
|
+
input {
|
|
505
|
+
text action
|
|
506
|
+
text resource_type
|
|
507
|
+
int? resource_id
|
|
508
|
+
json? details
|
|
509
|
+
}
|
|
510
|
+
stack {
|
|
511
|
+
db.add "audit_log" {
|
|
512
|
+
data = {
|
|
513
|
+
user_id: $auth.id,
|
|
514
|
+
action: $input.action,
|
|
515
|
+
resource_type: $input.resource_type,
|
|
516
|
+
resource_id: $input.resource_id,
|
|
517
|
+
details: $input.details,
|
|
518
|
+
ip_address: $env.$remote_ip,
|
|
519
|
+
user_agent: $env.$http_headers|get:"user-agent",
|
|
520
|
+
created_at: now
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Usage
|
|
527
|
+
function.run "audit_log" {
|
|
528
|
+
input = {
|
|
529
|
+
action: "delete",
|
|
530
|
+
resource_type: "user",
|
|
531
|
+
resource_id: $deleted_user.id,
|
|
532
|
+
details: { reason: $input.reason }
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Best Practices Summary
|
|
540
|
+
|
|
541
|
+
1. **Validate all inputs** - Use types and filters
|
|
542
|
+
2. **Check authorization** - Verify permissions for every operation
|
|
543
|
+
3. **Use parameterized queries** - Never concatenate user input into SQL
|
|
544
|
+
4. **Hash passwords** - Use built-in password type
|
|
545
|
+
5. **Encrypt sensitive data** - SSN, payment info, etc.
|
|
546
|
+
6. **Store secrets in env vars** - Never hardcode
|
|
547
|
+
7. **Rate limit APIs** - Prevent abuse
|
|
548
|
+
8. **Log security events** - Audit trail for compliance
|
|
549
|
+
9. **Use HTTPS** - Always (handled by platform)
|
|
550
|
+
10. **Rotate tokens** - Implement refresh token flow
|