// el-identity — Engram-native identity for el-ui. // // Identity in el-ui is a graph, not a table. Every entity is a node; every // relationship is an edge. Authentication is spreading activation from a // session token through the identity subgraph until it touches a User node. // // Edges: // User ──has_role──▶ Role ──grants──▶ Scope // User ──has_session──▶ Session ──authenticated_via──▶ OAuthToken // ── Edge type constants ────────────────────────────────────────────────────── let EDGE_HAS_ROLE: String = "has_role" let EDGE_HAS_SESSION: String = "has_session" let EDGE_AUTHENTICATED_VIA: String = "authenticated_via" let EDGE_GRANTS: String = "grants" // ── Node type constants ────────────────────────────────────────────────────── let NODE_USER: String = "User" let NODE_ROLE: String = "Role" let NODE_SCOPE: String = "Scope" let NODE_OAUTH_TOKEN: String = "OAuthToken" let NODE_SESSION: String = "Session" // ── User ───────────────────────────────────────────────────────────────────── type User { id: String email: String display_name: String created_at: String } fn user_new(email: String, display_name: String) -> User { let now: String = time_now_iso() let id: String = uuid_v4() { "id": id, "email": email, "display_name": display_name, "created_at": now } } // ── Role / Scope ───────────────────────────────────────────────────────────── type Role { id: String name: String permissions: String // JSON-encoded array; struct fields are flat in El today } type Scope { id: String name: String description: String } fn role_new(name: String) -> Role { { "id": uuid_v4(), "name": name, "permissions": "[]" } } fn role_has_permission(role: Role, perm: String) -> Bool { str_contains(role.permissions, "\"" + perm + "\"") } fn scope_new(name: String, description: String) -> Scope { { "id": uuid_v4(), "name": name, "description": description } } // ── Session ────────────────────────────────────────────────────────────────── type Session { id: String user_id: String created_at: String expires_at: String ip_address: String } fn session_new(user_id: String, ttl_seconds: Int, ip_address: String) -> Session { let now: String = time_now_iso() let exp: String = time_add_seconds(now, ttl_seconds) { "id": uuid_v4(), "user_id": user_id, "created_at": now, "expires_at": exp, "ip_address": ip_address } } fn session_is_expired(session: Session) -> Bool { time_after(time_now_iso(), session.expires_at) } // ── OAuthToken ─────────────────────────────────────────────────────────────── // // Tokens are SHA-256 hashed before storage — the raw token never persists. type OAuthToken { id: String provider: String access_token_hash: String refresh_token_hash: String expires_at: String scopes: String // JSON-encoded array } fn token_hash(raw: String) -> String { sha256_hex(raw) } fn oauth_token_new(provider: String, access_raw: String, refresh_raw: String, expires_at: String, scopes: String) -> OAuthToken { let access_h: String = token_hash(access_raw) let refresh_h: String = "" if !str_eq(refresh_raw, "") { let refresh_h = token_hash(refresh_raw) } { "id": uuid_v4(), "provider": provider, "access_token_hash": access_h, "refresh_token_hash": refresh_h, "expires_at": expires_at, "scopes": scopes } } fn oauth_token_is_expired(t: OAuthToken) -> Bool { time_after(time_now_iso(), t.expires_at) } // ── PKCE (RFC 7636) ────────────────────────────────────────────────────────── type PkceChallenge { verifier: String challenge: String method: String // always "S256" } fn pkce_generate() -> PkceChallenge { let verifier: String = base64url_no_pad(random_bytes(32)) let chal: String = base64url_no_pad(sha256_bytes(verifier)) { "verifier": verifier, "challenge": chal, "method": "S256" } } fn pkce_verify(challenge: String, verifier: String) -> Bool { let computed: String = base64url_no_pad(sha256_bytes(verifier)) str_eq(computed, challenge) } // ── OAuth providers (Google / GitHub / Apple) ──────────────────────────────── // // Stubbed: shape-only. `provider_authorization_url` produces the redirect URL; // `provider_exchange_code` POSTs to the token endpoint via http_post. type OAuthProviderCfg { name: String client_id: String client_secret: String auth_url: String token_url: String default_scopes: String } fn google_provider(client_id: String, client_secret: String) -> OAuthProviderCfg { { "name": "google", "client_id": client_id, "client_secret": client_secret, "auth_url": "https://accounts.google.com/o/oauth2/v2/auth", "token_url": "https://oauth2.googleapis.com/token", "default_scopes": "[\"openid\",\"email\",\"profile\"]" } } fn github_provider(client_id: String, client_secret: String) -> OAuthProviderCfg { { "name": "github", "client_id": client_id, "client_secret": client_secret, "auth_url": "https://github.com/login/oauth/authorize", "token_url": "https://github.com/login/oauth/access_token", "default_scopes": "[\"read:user\",\"user:email\"]" } } fn apple_provider(client_id: String, client_secret: String) -> OAuthProviderCfg { { "name": "apple", "client_id": client_id, "client_secret": client_secret, "auth_url": "https://appleid.apple.com/auth/authorize", "token_url": "https://appleid.apple.com/auth/token", "default_scopes": "[\"name\",\"email\"]" } } fn provider_authorization_url(p: OAuthProviderCfg, redirect_uri: String, code_challenge: String, state: String) -> String { p.auth_url + "?response_type=code" + "&client_id=" + url_encode(p.client_id) + "&redirect_uri=" + url_encode(redirect_uri) + "&scope=" + url_encode(json_array_to_space_list(p.default_scopes)) + "&state=" + url_encode(state) + "&code_challenge=" + url_encode(code_challenge) + "&code_challenge_method=S256" } // ── Engram client interface (graph CRUD) ───────────────────────────────────── // // Identity persists to the local Engram graph. These wrap engram_* runtime // calls (planned). Until that lands, the server-side stub uses a JSON file. fn engram_create_node(node_type: String, value_json: String) -> String { engram_node_create(node_type, value_json) } fn engram_create_edge(from_id: String, to_id: String, edge_type: String) -> Bool { engram_edge_create(from_id, to_id, edge_type) } fn engram_find_connected(node_id: String, edge_type: String) -> String { engram_edge_traverse(node_id, edge_type) } // ── OAuth flow coordinator ─────────────────────────────────────────────────── type AuthFlowParams { pkce_verifier: String pkce_challenge: String redirect_url: String state: String } fn begin_auth_flow(provider: OAuthProviderCfg, redirect_uri: String) -> AuthFlowParams { let pkce: PkceChallenge = pkce_generate() let state: String = base64url_no_pad(random_bytes(16)) let url: String = provider_authorization_url(provider, redirect_uri, pkce.challenge, state) { "pkce_verifier": pkce.verifier, "pkce_challenge": pkce.challenge, "redirect_url": url, "state": state } } fn exchange_code(provider: OAuthProviderCfg, code: String, pkce_verifier: String, redirect_uri: String, session_id: String) -> OAuthToken { let body: String = "grant_type=authorization_code" + "&code=" + url_encode(code) + "&redirect_uri=" + url_encode(redirect_uri) + "&client_id=" + url_encode(provider.client_id) + "&client_secret=" + url_encode(provider.client_secret) + "&code_verifier=" + url_encode(pkce_verifier) let resp: String = http_post(provider.token_url, body) let access: String = json_get(resp, "access_token") let refresh: String = json_get(resp, "refresh_token") let expires_in: Int = str_to_int(json_get(resp, "expires_in")) let exp_at: String = time_add_seconds(time_now_iso(), expires_in) let token: OAuthToken = oauth_token_new(provider.name, access, refresh, exp_at, "[]") let token_id: String = engram_create_node(NODE_OAUTH_TOKEN, json_encode(token)) engram_create_edge(session_id, token_id, EDGE_AUTHENTICATED_VIA) token } // ── Session manager ────────────────────────────────────────────────────────── fn session_create(user_id: String, ttl: Int, ip: String) -> Session { let s: Session = session_new(user_id, ttl, ip) let session_id: String = engram_create_node(NODE_SESSION, json_encode(s)) engram_create_edge(user_id, session_id, EDGE_HAS_SESSION) s } fn session_revoke(session_id: String) -> Bool { engram_node_delete(session_id) } // ── AuthGuard — applied via @authenticate decorator ───────────────────────── fn auth_guard_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) } // ── Identity context (passed through the request lifecycle) ───────────────── type IdentityContext { user_id: String session_id: String roles_json: String } fn identity_load(session_id: String) -> IdentityContext { let session_raw: String = engram_node_get(session_id) let user_id: String = json_get(session_raw, "user_id") let roles_raw: String = engram_find_connected(user_id, EDGE_HAS_ROLE) { "user_id": user_id, "session_id": session_id, "roles_json": roles_raw } } // ── Entry — smoke test ────────────────────────────────────────────────────── let user: User = user_new("will@neurontechnologies.ai", "Will Anderson") println("[el-identity] user " + user.email + " (" + user.id + ")")