@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.
@@ -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