// el-auth — Built-in authentication and authorization for el-ui. // // Engram-aware sessions: server-side invalidation works even with stateless // JWTs because every session is also a graph node. // // Provider trait surface: // verify(token) -> AuthContext // issue(user, roles) -> token string // revoke(token) -> Bool // ── Errors ──────────────────────────────────────────────────────────────────── let ERR_INVALID_CREDS: String = "auth.invalid_credentials" let ERR_TOKEN_EXPIRED: String = "auth.token_expired" let ERR_TOKEN_INVALID: String = "auth.token_invalid" let ERR_SESSION_NOT_FOUND: String = "auth.session_not_found" let ERR_FORBIDDEN: String = "auth.forbidden" let ERR_CONFIG: String = "auth.config" // ── AuthContext + AuthUser ──────────────────────────────────────────────────── type AuthUser { id: String email: String display_name: String } type AuthContext { user_id: String email: String roles: String // JSON array of role names permissions: String // JSON array of permission strings session_id: String issued_at: String expires_at: String } fn auth_context_empty() -> AuthContext { { "user_id": "", "email": "", "roles": "[]", "permissions": "[]", "session_id": "", "issued_at": "", "expires_at": "" } } fn auth_context_has_permission(ctx: AuthContext, perm: String) -> Bool { str_contains(ctx.permissions, "\"" + perm + "\"") } fn auth_context_has_role(ctx: AuthContext, role: String) -> Bool { str_contains(ctx.roles, "\"" + role + "\"") } // ── Roles + permissions ─────────────────────────────────────────────────────── type Permission { resource: String action: String } fn permission_new(resource: String, action: String) -> Permission { { "resource": resource, "action": action } } fn permission_string(p: Permission) -> String { p.resource + ":" + p.action } // Role registry — maps role name -> JSON array of permission strings. fn role_registry_grant(registry_path: String, role: String, perm: String) -> Bool { let raw: String = fs_read(registry_path) let updated: String = json_array_append(raw, role, "\"" + perm + "\"") fs_write(registry_path, updated) } // ── JWT (HS256) ────────────────────────────────────────────────────────────── type JwtClaims { sub: String // user id iss: String // issuer aud: String // audience iat: Int // issued at (unix seconds) exp: Int // expires at (unix seconds) jti: String // unique token id } fn jwt_claims_new(user_id: String, issuer: String, audience: String, ttl_seconds: Int) -> JwtClaims { let now: Int = time_now_unix() { "sub": user_id, "iss": issuer, "aud": audience, "iat": now, "exp": now + ttl_seconds, "jti": uuid_v4() } } fn jwt_encode(claims: JwtClaims, secret: String) -> String { let header_b64: String = base64url_no_pad("{\"alg\":\"HS256\",\"typ\":\"JWT\"}") let payload_json: String = json_encode(claims) let payload_b64: String = base64url_no_pad(payload_json) let signing_input: String = header_b64 + "." + payload_b64 let sig: String = base64url_no_pad(hmac_sha256(secret, signing_input)) signing_input + "." + sig } fn jwt_decode(token: String, secret: String) -> AuthContext { let parts: String = token // [header].[payload].[sig] let dot1: Int = str_index_of(parts, ".") if dot1 < 0 { return auth_context_empty() } let rest: String = str_slice(parts, dot1 + 1, str_len(parts)) let dot2: Int = str_index_of(rest, ".") if dot2 < 0 { return auth_context_empty() } let header_b64: String = str_slice(parts, 0, dot1) let payload_b64: String = str_slice(rest, 0, dot2) let sig_b64: String = str_slice(rest, dot2 + 1, str_len(rest)) let signing_input: String = header_b64 + "." + payload_b64 let expected_sig: String = base64url_no_pad(hmac_sha256(secret, signing_input)) if !str_eq(expected_sig, sig_b64) { return auth_context_empty() } let payload_json: String = base64url_decode(payload_b64) let now: Int = time_now_unix() let exp: Int = str_to_int(json_get(payload_json, "exp")) if exp < now { return auth_context_empty() } { "user_id": json_get(payload_json, "sub"), "email": "", "roles": "[]", "permissions": "[]", "session_id": json_get(payload_json, "jti"), "issued_at": json_get(payload_json, "iat"), "expires_at": json_get(payload_json, "exp") } } // ── Engram-backed session store ────────────────────────────────────────────── // // Sessions are nodes of type "Session" connected to User via has_session. // Revocation = node delete. Verification = node lookup + expiry check. fn session_store_create(user_id: String, ttl_seconds: Int, ip: String) -> String { let now: String = time_now_iso() let exp: String = time_add_seconds(now, ttl_seconds) let id: String = uuid_v4() let body: String = "{\"id\":\"" + id + "\",\"user_id\":\"" + user_id + "\",\"created_at\":\"" + now + "\",\"expires_at\":\"" + exp + "\",\"ip_address\":\"" + ip + "\"}" let node_id: String = engram_node_create("Session", body) engram_edge_create(user_id, node_id, "has_session") node_id } fn session_store_verify(session_id: String) -> Bool { let raw: String = engram_node_get(session_id) if str_eq(raw, "") { return false } let exp: String = json_get(raw, "expires_at") !time_after(time_now_iso(), exp) } fn session_store_revoke(session_id: String) -> Bool { engram_node_delete(session_id) } // ── Middleware ──────────────────────────────────────────────────────────────── // // auth_middleware extracts the bearer token, decodes it, and populates the // AuthContext. Applied automatically by the @authenticate aspect (el-aop). fn extract_bearer(authorization_header: String) -> String { if str_starts_with(authorization_header, "Bearer ") { return str_slice(authorization_header, 7, str_len(authorization_header)) } "" } fn auth_middleware(authorization_header: String, jwt_secret: String) -> AuthContext { let token: String = extract_bearer(authorization_header) if str_eq(token, "") { return auth_context_empty() } jwt_decode(token, jwt_secret) } fn enforce_permission(ctx: AuthContext, required_perm: String) -> Bool { if str_eq(ctx.user_id, "") { return false } auth_context_has_permission(ctx, required_perm) } // ── Provider issue/verify ──────────────────────────────────────────────────── fn provider_issue(user: AuthUser, jwt_secret: String, issuer: String, audience: String, ttl: Int) -> String { let claims: JwtClaims = jwt_claims_new(user.id, issuer, audience, ttl) jwt_encode(claims, jwt_secret) } fn provider_verify(token: String, jwt_secret: String) -> AuthContext { let ctx: AuthContext = jwt_decode(token, jwt_secret) if str_eq(ctx.user_id, "") { return ctx } if !session_store_verify(ctx.session_id) { return auth_context_empty() } ctx } fn provider_revoke(session_id: String) -> Bool { session_store_revoke(session_id) } // ── Entry — smoke test ─────────────────────────────────────────────────────── let user: AuthUser = { "id": "u-001", "email": "will@neurontechnologies.ai", "display_name": "Will" } let token: String = provider_issue(user, "test-secret", "el-ui", "el-app", 3600) println("[el-auth] issued JWT for " + user.email)