Auth, JWT & Rate Limiting
Decorators
A decorator runs before a handler. It can reject the request with fail STATUS "msg" (short-circuiting to an error response) and/or assign the typed bindings in its injects (...) clause, which then become variables inside the handler. This keeps auth a declaration rather than boilerplate in every handler.
type Claims = { sub: string, role: string }
decorator require_role(role: string) injects (user: Claims) {
let header = request_header("Authorization") else { fail 401 "missing authorization" }
user = try jwt_verify(Claims, trimPrefix(header, "Bearer "), env("JWT_SECRET"))
if user.role != role { fail 403 "forbidden" }
}
@require_role("admin")
route GET "/admin" -> Claims {
respond user # `user` injected by the decorator, typed as Claims
}Decorators stack and run top-to-bottom; each is inlined into the handler, so there is no per-request indirection.
JWT & request helpers
jwt_sign(claims, secret) -> string— sign a claims record as an HS256 token, so a service can issue its own tokens, not just verify them.jwt_verify(Type, token, secret) -> Type!— HS256-verifies the signature (constant-time) and decodes the payload JSON into your claims record. It also enforces the standardexp(expiry) andnbf(not-before) claims when your record carries them as int fields, so an expired token is rejected automatically (a failedtry→ 401).jwt_decode(Type, token) -> Type?— decodes without verifying.request_header(name) -> string?— read a request header.client_ip() -> string— readsX-Forwarded-For/X-Real-IP.
Passwords & sessions
password_hash(pw) -> string computes a bcrypt hash with a random salt; verify a candidate against a stored hash with password_verify(pw, hash) -> bool (constant-time). Together with jwt_sign this is a complete login: hash on register, verify on login, then hand back a signed token.
type Credentials = { username: string, password: string }
type Claims = { sub: string, exp: int }
type Session = { token: string }
state accounts { var hashes: [string: string] = [:] }
let secret = env("JWT_SECRET")
route POST "/register" (body creds: Credentials) -> Session {
accounts.hashes[creds.username] = password_hash(creds.password)
let claims = Claims { sub: creds.username, exp: now_ms() / 1000 + 3600 }
respond Session { token: jwt_sign(claims, secret) }
}
route POST "/login" (body creds: Credentials) -> Session {
let stored = accounts.hashes[creds.username] ?? ""
if !password_verify(creds.password, stored) { fail 401 "invalid credentials" }
let claims = Claims { sub: creds.username, exp: now_ms() / 1000 + 3600 }
respond Session { token: jwt_sign(claims, secret) } # verified later by jwt_verify
}Crypto & encoding
Lower-level primitives, all in the standard prelude:
random_bytes(n) -> string—ncryptographically-secure random bytes (CSPRNG).uuid() -> string— a random (v4) UUID.sha256(s) -> string— SHA-256 digest, hex-encoded.hmac_sha256(msg, key) -> string— HMAC-SHA-256, hex-encoded.base64_encode/base64_decodeandbase64url_encode/base64url_decode— standard and URL-safe Base64.hex_encode/hex_decode— hex. All decoders return""on malformed input.
hash() is Wyhash — fast but non-cryptographic; reach for password_hash or sha256 whenever the result is security-sensitive.
Rate limiting
Rate limiting is written in Weld itself on top of decorators + state + a map — no built-in primitive. A [string: Bucket] map keyed by client IP holds a per-client fixed-window counter:
type Bucket = { count: int, window_start: int }
state limiter { var buckets: [string: Bucket] = [:] }
decorator rate_limit(max: int, window_ms: int) {
let ip = client_ip()
let now = now_ms()
let b = limiter.buckets[ip] ?? Bucket { count: 0, window_start: now }
var count = b.count
var start = b.window_start
if now - start > window_ms { start = now count = 0 }
count = count + 1
limiter.buckets[ip] = Bucket { count: count, window_start: start }
if count > max { fail 429 "rate limit exceeded for {ip}" }
}
@rate_limit(60, 60000) # 60 requests / minute, per client IP
route GET "/api/data" { respond "ok" }