@xano/developer-mcp 1.0.58 → 1.0.59
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 +15 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/xanoscript_docs.d.ts +9 -0
- package/dist/tools/xanoscript_docs.js +27 -0
- package/dist/xanoscript.d.ts +5 -1
- package/dist/xanoscript.js +70 -6
- package/dist/xanoscript.test.js +5 -3
- package/dist/xanoscript_docs/README.md +9 -43
- package/dist/xanoscript_docs/addons.md +0 -2
- package/dist/xanoscript_docs/agents.md +2 -35
- package/dist/xanoscript_docs/apis.md +3 -6
- package/dist/xanoscript_docs/branch.md +0 -2
- package/dist/xanoscript_docs/database.md +3 -7
- package/dist/xanoscript_docs/debugging.md +1 -264
- package/dist/xanoscript_docs/docs_index.json +22 -0
- package/dist/xanoscript_docs/essentials.md +1 -9
- package/dist/xanoscript_docs/frontend.md +1 -138
- package/dist/xanoscript_docs/functions.md +3 -7
- package/dist/xanoscript_docs/mcp-servers.md +1 -2
- package/dist/xanoscript_docs/middleware.md +1 -3
- package/dist/xanoscript_docs/performance.md +8 -198
- package/dist/xanoscript_docs/realtime.md +11 -161
- package/dist/xanoscript_docs/run.md +2 -184
- package/dist/xanoscript_docs/schema.md +1 -3
- package/dist/xanoscript_docs/security.md +82 -313
- package/dist/xanoscript_docs/streaming.md +2 -37
- package/dist/xanoscript_docs/survival.md +161 -0
- package/dist/xanoscript_docs/syntax.md +0 -6
- package/dist/xanoscript_docs/tables.md +3 -5
- package/dist/xanoscript_docs/tasks.md +1 -3
- package/dist/xanoscript_docs/tools.md +1 -3
- package/dist/xanoscript_docs/triggers.md +3 -69
- package/dist/xanoscript_docs/types.md +3 -4
- package/dist/xanoscript_docs/unit-testing.md +1 -55
- package/dist/xanoscript_docs/workflow-tests.md +8 -35
- package/dist/xanoscript_docs/working.md +667 -0
- package/dist/xanoscript_docs/workspace.md +0 -2
- package/package.json +1 -1
|
@@ -8,19 +8,6 @@ Best practices for building secure XanoScript applications.
|
|
|
8
8
|
|
|
9
9
|
> **TL;DR:** Always check `$auth.id` for protected endpoints. Use `security.create_auth_token` for JWT. Validate all inputs with `filters=`. Hash passwords with `password` type. Use `$env.SECRET_NAME` for secrets.
|
|
10
10
|
|
|
11
|
-
## Section Index
|
|
12
|
-
|
|
13
|
-
- [Authentication](#authentication) - Tokens, sessions, refresh tokens
|
|
14
|
-
- [Authorization](#authorization) - Role checks, ownership
|
|
15
|
-
- [Input Validation](#input-validation) - Type enforcement, sanitization
|
|
16
|
-
- [Data Protection](#data-protection) - Encryption, hashing, secrets
|
|
17
|
-
- [Rate Limiting](#rate-limiting--abuse-prevention) - API limits, abuse prevention
|
|
18
|
-
- [Security Headers](#security-headers) - CORS configuration
|
|
19
|
-
- [Audit Logging](#audit-logging) - Security event tracking
|
|
20
|
-
- [Best Practices Summary](#best-practices-summary) - Quick checklist
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
11
|
## Quick Reference
|
|
25
12
|
|
|
26
13
|
| Area | Key Practices |
|
|
@@ -62,104 +49,46 @@ precondition ($auth.id != null) {
|
|
|
62
49
|
|
|
63
50
|
### Refresh Tokens
|
|
64
51
|
|
|
52
|
+
Pattern: Store refresh tokens in a `refresh_token` table. On refresh, verify the token hasn't expired or been revoked, generate a new access token via `security.create_auth_token`, rotate the refresh token with `security.create_uuid`, and update the stored token.
|
|
53
|
+
|
|
65
54
|
```xs
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
error_type = "accessdenied"
|
|
81
|
-
error = "Invalid or expired refresh token"
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Get user
|
|
85
|
-
db.get "user" {
|
|
86
|
-
field_name = "id"
|
|
87
|
-
field_value = $stored_token.user_id
|
|
88
|
-
} as $user
|
|
89
|
-
|
|
90
|
-
// Generate new access token
|
|
91
|
-
security.create_auth_token {
|
|
92
|
-
table = "user"
|
|
93
|
-
id = $user.id
|
|
94
|
-
extras = { role: $user.role }
|
|
95
|
-
expiration = 3600
|
|
96
|
-
} as $new_token
|
|
97
|
-
|
|
98
|
-
// Rotate refresh token
|
|
99
|
-
security.create_uuid as $new_refresh
|
|
100
|
-
|
|
101
|
-
db.edit "refresh_token" {
|
|
102
|
-
field_name = "id"
|
|
103
|
-
field_value = $stored_token.id
|
|
104
|
-
data = {
|
|
105
|
-
token: $new_refresh,
|
|
106
|
-
expires_at: now|transform_timestamp:"+30 days"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
response = {
|
|
111
|
-
access_token: $new_token,
|
|
112
|
-
refresh_token: $new_refresh
|
|
113
|
-
}
|
|
114
|
-
}
|
|
55
|
+
// Key operations in a refresh flow:
|
|
56
|
+
db.query "refresh_token" {
|
|
57
|
+
where = $db.refresh_token.token == $input.refresh_token
|
|
58
|
+
&& $db.refresh_token.expires_at > now
|
|
59
|
+
&& $db.refresh_token.revoked == false
|
|
60
|
+
return = { type: "single" }
|
|
61
|
+
} as $stored_token
|
|
62
|
+
|
|
63
|
+
security.create_auth_token {
|
|
64
|
+
table = "user"
|
|
65
|
+
id = $user.id
|
|
66
|
+
extras = { role: $user.role }
|
|
67
|
+
expiration = 3600
|
|
68
|
+
} as $new_token
|
|
115
69
|
```
|
|
116
70
|
|
|
117
71
|
### Session Management
|
|
118
72
|
|
|
73
|
+
Pattern: Use `security.create_uuid` for session IDs. Store in a `session` table with `user_id`, `ip_address` (`$env.$remote_ip`), `expires_at`, and `last_activity`. Validate by querying where `expires_at > now`.
|
|
74
|
+
|
|
119
75
|
```xs
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
id: $session_id,
|
|
128
|
-
user_id: $input.user_id,
|
|
129
|
-
ip_address: $env.$remote_ip,
|
|
130
|
-
user_agent: $env.$http_headers|get:"user-agent",
|
|
131
|
-
created_at: now,
|
|
132
|
-
last_activity: now,
|
|
133
|
-
expires_at: now|transform_timestamp:"+7 days"
|
|
134
|
-
}
|
|
135
|
-
}
|
|
76
|
+
// Create session
|
|
77
|
+
security.create_uuid as $session_id
|
|
78
|
+
db.add "session" {
|
|
79
|
+
data = {
|
|
80
|
+
id: $session_id,
|
|
81
|
+
user_id: $input.user_id,
|
|
82
|
+
expires_at: now|transform_timestamp:"+7 days"
|
|
136
83
|
}
|
|
137
|
-
response = $session_id
|
|
138
84
|
}
|
|
139
85
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return = { type: "single" }
|
|
147
|
-
} as $session
|
|
148
|
-
|
|
149
|
-
precondition ($session != null) {
|
|
150
|
-
error_type = "accessdenied"
|
|
151
|
-
error = "Invalid or expired session"
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Update last activity
|
|
155
|
-
db.edit "session" {
|
|
156
|
-
field_name = "id"
|
|
157
|
-
field_value = $session.id
|
|
158
|
-
data = { last_activity: now }
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
response = $session
|
|
162
|
-
}
|
|
86
|
+
// Validate session
|
|
87
|
+
db.query "session" {
|
|
88
|
+
where = $db.session.id == $input.session_id
|
|
89
|
+
&& $db.session.expires_at > now
|
|
90
|
+
return = { type: "single" }
|
|
91
|
+
} as $session
|
|
163
92
|
```
|
|
164
93
|
|
|
165
94
|
---
|
|
@@ -178,25 +107,9 @@ precondition ($auth.role == "admin") {
|
|
|
178
107
|
### Permission Checks
|
|
179
108
|
|
|
180
109
|
```xs
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
stack {
|
|
186
|
-
var $has_permission {
|
|
187
|
-
value = $auth.permissions|contains:$input.permission
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
precondition ($has_permission) {
|
|
191
|
-
error_type = "accessdenied"
|
|
192
|
-
error = "Missing permission: " ~ $input.permission
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Usage
|
|
198
|
-
function.run "check_permission" {
|
|
199
|
-
input = { permission: "users.delete" }
|
|
110
|
+
precondition ($auth.permissions|contains:"users.delete") {
|
|
111
|
+
error_type = "accessdenied"
|
|
112
|
+
error = "Missing permission: users.delete"
|
|
200
113
|
}
|
|
201
114
|
```
|
|
202
115
|
|
|
@@ -217,40 +130,19 @@ precondition ($document.owner_id == $auth.id) {
|
|
|
217
130
|
|
|
218
131
|
### Hierarchical Access
|
|
219
132
|
|
|
133
|
+
Check ownership first, then team membership via `join`. Combine with action-specific logic:
|
|
134
|
+
|
|
220
135
|
```xs
|
|
221
|
-
|
|
222
|
-
input
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return = { type: "exists" }
|
|
232
|
-
} as $is_owner
|
|
233
|
-
|
|
234
|
-
// Check team membership
|
|
235
|
-
db.query "team_member" {
|
|
236
|
-
join = {
|
|
237
|
-
resource: {
|
|
238
|
-
table: "resource",
|
|
239
|
-
where: $db.resource.team_id == $db.team_member.team_id
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
where = $db.resource.id == $input.resource_id
|
|
243
|
-
&& $db.team_member.user_id == $auth.id
|
|
244
|
-
return = { type: "exists" }
|
|
245
|
-
} as $is_team_member
|
|
246
|
-
|
|
247
|
-
// Check permissions
|
|
248
|
-
var $can_access {
|
|
249
|
-
value = $is_owner || ($is_team_member && $input.action != "delete")
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
response = $can_access
|
|
253
|
-
}
|
|
136
|
+
db.query "resource" {
|
|
137
|
+
where = $db.resource.id == $input.resource_id && $db.resource.owner_id == $auth.id
|
|
138
|
+
return = { type: "exists" }
|
|
139
|
+
} as $is_owner
|
|
140
|
+
|
|
141
|
+
db.query "team_member" {
|
|
142
|
+
join = { resource: { table: "resource", where: $db.resource.team_id == $db.team_member.team_id } }
|
|
143
|
+
where = $db.resource.id == $input.resource_id && $db.team_member.user_id == $auth.id
|
|
144
|
+
return = { type: "exists" }
|
|
145
|
+
} as $is_team_member
|
|
254
146
|
```
|
|
255
147
|
|
|
256
148
|
---
|
|
@@ -286,33 +178,15 @@ db.direct_query {
|
|
|
286
178
|
// sql = "SELECT * FROM users WHERE email = '" ~ $input.email ~ "'"
|
|
287
179
|
```
|
|
288
180
|
|
|
289
|
-
### XSS Prevention
|
|
181
|
+
### XSS / Path Traversal Prevention
|
|
290
182
|
|
|
291
183
|
```xs
|
|
292
|
-
|
|
293
|
-
var $safe_content {
|
|
294
|
-
value = $input.content|escape
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// For rich text, escape HTML entities
|
|
298
|
-
var $sanitized {
|
|
299
|
-
value = $input.html|escape
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Path Traversal Prevention
|
|
184
|
+
var $safe_content { value = $input.content|escape } // Escape HTML
|
|
304
185
|
|
|
305
|
-
|
|
306
|
-
// Validate file paths
|
|
307
|
-
precondition (!($input.filename|contains:"..")) {
|
|
186
|
+
precondition (!($input.filename|contains:"..")) { // Validate paths
|
|
308
187
|
error_type = "inputerror"
|
|
309
188
|
error = "Invalid filename"
|
|
310
189
|
}
|
|
311
|
-
|
|
312
|
-
precondition ($input.filename|regex_matches:"/^[a-zA-Z0-9_.-]+$/") {
|
|
313
|
-
error_type = "inputerror"
|
|
314
|
-
error = "Filename contains invalid characters"
|
|
315
|
-
}
|
|
316
190
|
```
|
|
317
191
|
|
|
318
192
|
---
|
|
@@ -337,104 +211,49 @@ security.check_password {
|
|
|
337
211
|
} as $is_valid
|
|
338
212
|
```
|
|
339
213
|
|
|
340
|
-
### Encryption
|
|
214
|
+
### Encryption
|
|
341
215
|
|
|
342
216
|
```xs
|
|
343
|
-
// Encrypt sensitive data
|
|
217
|
+
// Encrypt/decrypt sensitive data
|
|
344
218
|
security.encrypt {
|
|
345
219
|
data = $input.ssn
|
|
346
220
|
algorithm = "aes-256-cbc"
|
|
347
221
|
key = $env.ENCRYPTION_KEY
|
|
348
222
|
iv = $env.ENCRYPTION_IV
|
|
349
|
-
} as $
|
|
350
|
-
|
|
351
|
-
db.add "user" {
|
|
352
|
-
data = { ssn_encrypted: $encrypted_ssn }
|
|
353
|
-
}
|
|
223
|
+
} as $encrypted
|
|
354
224
|
|
|
355
|
-
// Decrypt when needed
|
|
356
225
|
security.decrypt {
|
|
357
|
-
data = $
|
|
226
|
+
data = $encrypted
|
|
358
227
|
algorithm = "aes-256-cbc"
|
|
359
228
|
key = $env.ENCRYPTION_KEY
|
|
360
229
|
iv = $env.ENCRYPTION_IV
|
|
361
|
-
} as $
|
|
230
|
+
} as $decrypted
|
|
362
231
|
```
|
|
363
232
|
|
|
364
233
|
### Secrets Management
|
|
365
234
|
|
|
366
235
|
```xs
|
|
367
|
-
//
|
|
236
|
+
// Use $env for secrets — NEVER hardcode
|
|
368
237
|
$env.API_SECRET_KEY
|
|
369
|
-
$env.DATABASE_URL
|
|
370
238
|
$env.ENCRYPTION_KEY
|
|
371
239
|
|
|
372
|
-
// NEVER hardcode secrets
|
|
373
|
-
// api_key = "sk_live_abc123" // BAD
|
|
374
|
-
|
|
375
240
|
// Mark sensitive inputs
|
|
376
241
|
input {
|
|
377
|
-
text api_key {
|
|
378
|
-
sensitive = true // Masked in logs
|
|
379
|
-
}
|
|
242
|
+
text api_key { sensitive = true } // Masked in logs
|
|
380
243
|
}
|
|
381
244
|
```
|
|
382
245
|
|
|
383
|
-
###
|
|
384
|
-
|
|
385
|
-
```xs
|
|
386
|
-
// Generate elliptic curve key
|
|
387
|
-
security.create_curve_key {
|
|
388
|
-
curve = "P-256"
|
|
389
|
-
format = "object"
|
|
390
|
-
} as $crypto_key
|
|
391
|
-
|
|
392
|
-
// Generate secret key for symmetric encryption
|
|
393
|
-
security.create_secret_key {
|
|
394
|
-
bits = 2048
|
|
395
|
-
format = "object"
|
|
396
|
-
} as $secret_key
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### JWE (Encrypted JWT)
|
|
400
|
-
|
|
401
|
-
```xs
|
|
402
|
-
// Encrypt JWT payload
|
|
403
|
-
security.jwe_encode {
|
|
404
|
-
headers = { "alg": "A256KW" }
|
|
405
|
-
claims = { data: "secret" }
|
|
406
|
-
key = $env.JWE_KEY
|
|
407
|
-
key_algorithm = "A256KW"
|
|
408
|
-
content_algorithm = "A256GCM"
|
|
409
|
-
ttl = 3600
|
|
410
|
-
} as $encrypted_token
|
|
411
|
-
|
|
412
|
-
// Decrypt JWE token
|
|
413
|
-
security.jwe_decode {
|
|
414
|
-
token = $encrypted_token
|
|
415
|
-
key = $env.JWE_KEY
|
|
416
|
-
key_algorithm = "A256KW"
|
|
417
|
-
content_algorithm = "A256GCM"
|
|
418
|
-
check_claims = { "iss": "my_app" }
|
|
419
|
-
} as $decoded_payload
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
### JWT Security
|
|
246
|
+
### JWT (JWS/JWE)
|
|
423
247
|
|
|
424
248
|
```xs
|
|
425
|
-
// Sign
|
|
249
|
+
// Sign JWT
|
|
426
250
|
security.jws_encode {
|
|
427
|
-
claims = {
|
|
428
|
-
sub: $user.id|to_text,
|
|
429
|
-
role: $user.role,
|
|
430
|
-
iat: now|to_seconds,
|
|
431
|
-
exp: (now|to_seconds) + 3600
|
|
432
|
-
}
|
|
251
|
+
claims = { sub: $user.id|to_text, role: $user.role }
|
|
433
252
|
key = $env.JWT_SECRET
|
|
434
253
|
signature_algorithm = "HS256"
|
|
435
254
|
} as $token
|
|
436
255
|
|
|
437
|
-
// Verify
|
|
256
|
+
// Verify JWT
|
|
438
257
|
security.jws_decode {
|
|
439
258
|
token = $input.token
|
|
440
259
|
key = $env.JWT_SECRET
|
|
@@ -442,6 +261,8 @@ security.jws_decode {
|
|
|
442
261
|
} as $claims
|
|
443
262
|
```
|
|
444
263
|
|
|
264
|
+
> See `xanoscript_docs({ topic: "integrations/utilities" })` for JWE, key generation, and other security utilities.
|
|
265
|
+
|
|
445
266
|
---
|
|
446
267
|
|
|
447
268
|
## Rate Limiting & Abuse Prevention
|
|
@@ -459,41 +280,19 @@ redis.ratelimit {
|
|
|
459
280
|
|
|
460
281
|
### Login Attempt Limiting
|
|
461
282
|
|
|
462
|
-
|
|
463
|
-
function "check_login_attempts" {
|
|
464
|
-
input { text email }
|
|
465
|
-
stack {
|
|
466
|
-
var $key { value = "login_attempts:" ~ $input.email|md5 }
|
|
467
|
-
|
|
468
|
-
redis.get { key = $key } as $attempts
|
|
283
|
+
Use Redis to track failed attempts per email with a TTL-based lockout:
|
|
469
284
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
285
|
+
```xs
|
|
286
|
+
var $key { value = "login_attempts:" ~ $input.email|md5 }
|
|
287
|
+
redis.get { key = $key } as $attempts
|
|
476
288
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
var $key { value = "login_attempts:" ~ $input.email|md5 }
|
|
481
|
-
|
|
482
|
-
redis.get { key = $key } as $current
|
|
483
|
-
redis.set {
|
|
484
|
-
key = $key
|
|
485
|
-
data = ($current|to_int ?? 0) + 1
|
|
486
|
-
ttl = 900
|
|
487
|
-
}
|
|
488
|
-
}
|
|
289
|
+
precondition (($attempts|to_int ?? 0) < 5) {
|
|
290
|
+
error_type = "accessdenied"
|
|
291
|
+
error = "Too many login attempts. Try again in 15 minutes."
|
|
489
292
|
}
|
|
490
293
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
stack {
|
|
494
|
-
redis.del { key = "login_attempts:" ~ $input.email|md5 }
|
|
495
|
-
}
|
|
496
|
-
}
|
|
294
|
+
// On failure: redis.set { key = $key, data = ($attempts|to_int ?? 0) + 1, ttl = 900 }
|
|
295
|
+
// On success: redis.del { key = $key }
|
|
497
296
|
```
|
|
498
297
|
|
|
499
298
|
### Request Size Limits
|
|
@@ -528,39 +327,16 @@ precondition ($allowed_origins|contains:$env.$http_headers|get:"origin") {
|
|
|
528
327
|
|
|
529
328
|
## Audit Logging
|
|
530
329
|
|
|
531
|
-
|
|
330
|
+
Log security events to an `audit_log` table with `user_id` (`$auth.id`), `action`, `resource_type`, `ip_address` (`$env.$remote_ip`), and `created_at`.
|
|
532
331
|
|
|
533
332
|
```xs
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
stack {
|
|
542
|
-
db.add "audit_log" {
|
|
543
|
-
data = {
|
|
544
|
-
user_id: $auth.id,
|
|
545
|
-
action: $input.action,
|
|
546
|
-
resource_type: $input.resource_type,
|
|
547
|
-
resource_id: $input.resource_id,
|
|
548
|
-
details: $input.details,
|
|
549
|
-
ip_address: $env.$remote_ip,
|
|
550
|
-
user_agent: $env.$http_headers|get:"user-agent",
|
|
551
|
-
created_at: now
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Usage
|
|
558
|
-
function.run "audit_log" {
|
|
559
|
-
input = {
|
|
560
|
-
action: "delete",
|
|
561
|
-
resource_type: "user",
|
|
562
|
-
resource_id: $deleted_user.id,
|
|
563
|
-
details: { reason: $input.reason }
|
|
333
|
+
db.add "audit_log" {
|
|
334
|
+
data = {
|
|
335
|
+
user_id: $auth.id,
|
|
336
|
+
action: $input.action,
|
|
337
|
+
resource_type: $input.resource_type,
|
|
338
|
+
ip_address: $env.$remote_ip,
|
|
339
|
+
created_at: now
|
|
564
340
|
}
|
|
565
341
|
}
|
|
566
342
|
```
|
|
@@ -569,16 +345,9 @@ function.run "audit_log" {
|
|
|
569
345
|
|
|
570
346
|
## Best Practices Summary
|
|
571
347
|
|
|
572
|
-
1. **Validate all inputs** - Use types and filters
|
|
573
|
-
2. **Check authorization** - Verify permissions for every operation
|
|
574
|
-
3. **
|
|
575
|
-
4. **Hash passwords** - Use built-in password type
|
|
576
|
-
5. **Encrypt sensitive data** - SSN, payment info, etc.
|
|
577
|
-
6. **Store secrets in env vars** - Never hardcode
|
|
578
|
-
7. **Rate limit APIs** - Prevent abuse
|
|
579
|
-
8. **Log security events** - Audit trail for compliance
|
|
580
|
-
9. **Use HTTPS** - Always (handled by platform)
|
|
581
|
-
10. **Rotate tokens** - Implement refresh token flow
|
|
348
|
+
1. **Validate all inputs** - Use types and filters; never concatenate user input into SQL
|
|
349
|
+
2. **Check authorization** - Verify `$auth.id` and permissions for every operation
|
|
350
|
+
3. **Store secrets in env vars** - Never hardcode; use `$env.SECRET_NAME`
|
|
582
351
|
|
|
583
352
|
---
|
|
584
353
|
|
|
@@ -276,39 +276,6 @@ function "import_large_csv" {
|
|
|
276
276
|
}
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
### ETL Pipeline
|
|
280
|
-
|
|
281
|
-
```xs
|
|
282
|
-
function "etl_events" {
|
|
283
|
-
input { file events_file }
|
|
284
|
-
stack {
|
|
285
|
-
stream.from_jsonl {
|
|
286
|
-
value = $input.events_file
|
|
287
|
-
} as $stream
|
|
288
|
-
|
|
289
|
-
foreach ($stream) {
|
|
290
|
-
each as $event {
|
|
291
|
-
// Transform
|
|
292
|
-
var $transformed {
|
|
293
|
-
value = {
|
|
294
|
-
event_type: $event.type|to_lower,
|
|
295
|
-
user_id: $event.user_id|to_int,
|
|
296
|
-
metadata: $event.data|json_encode,
|
|
297
|
-
occurred_at: $event.timestamp|to_timestamp
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Load
|
|
302
|
-
db.add "processed_event" {
|
|
303
|
-
data = $transformed
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
response = { status: "complete" }
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
279
|
### Chunked Export
|
|
313
280
|
|
|
314
281
|
```xs
|
|
@@ -334,10 +301,8 @@ query "stream_large_dataset" {
|
|
|
334
301
|
## Best Practices
|
|
335
302
|
|
|
336
303
|
1. **Use streaming for large files** - Prevents memory exhaustion
|
|
337
|
-
2. **
|
|
338
|
-
3. **
|
|
339
|
-
4. **Set appropriate chunk sizes** - Balance memory and performance
|
|
340
|
-
5. **Use JSONL for structured data** - Easier to parse than multi-line JSON
|
|
304
|
+
2. **Handle errors gracefully** - Log failures without stopping stream
|
|
305
|
+
3. **Use JSONL for structured data** - Easier to parse than multi-line JSON
|
|
341
306
|
|
|
342
307
|
---
|
|
343
308
|
|