///|
/// Better Auth - Authentication library for TypeScript
///
/// Documentation: https://www.better-auth.com/
///
/// Database Setup:
/// Before using better-auth, you need to generate and run migrations:
/// ```bash
/// npx @better-auth/cli@latest generate
/// npx @better-auth/cli@latest migrate
/// ```
/// See: https://www.better-auth.com/docs/adapters/sqlite
///|
/// Auth instance
#external
pub type Auth
///|
pub fn Auth::to_any(self : Auth) -> @core.Any = "%identity"
///|
/// User type returned from better-auth API
#external
pub type User
///|
pub fn User::to_any(self : User) -> @core.Any = "%identity"
///|
/// Get user id
pub fn User::id(self : User) -> String {
self.to_any()["id"].cast()
}
///|
/// Get user email
pub fn User::email(self : User) -> String {
self.to_any()["email"].cast()
}
///|
/// Get user name
pub fn User::name(self : User) -> String? {
@core.identity_option(self.to_any()["name"])
}
///|
/// Get user image
pub fn User::image(self : User) -> String? {
@core.identity_option(self.to_any()["image"])
}
///|
/// Get user emailVerified
pub fn User::emailVerified(self : User) -> Bool {
self.to_any()["emailVerified"].cast()
}
///|
/// Get user createdAt
pub fn User::createdAt(self : User) -> String {
self.to_any()["createdAt"].cast()
}
///|
/// Get user updatedAt
pub fn User::updatedAt(self : User) -> String {
self.to_any()["updatedAt"].cast()
}
///|
/// Session type stored in database
#external
pub type Session
///|
pub fn Session::to_any(self : Session) -> @core.Any = "%identity"
///|
/// Get session id
pub fn Session::id(self : Session) -> String {
self.to_any()["id"].cast()
}
///|
/// Get session userId
pub fn Session::userId(self : Session) -> String {
self.to_any()["userId"].cast()
}
///|
/// Get session token
pub fn Session::token(self : Session) -> String {
self.to_any()["token"].cast()
}
///|
/// Get session expiresAt
pub fn Session::expiresAt(self : Session) -> String {
self.to_any()["expiresAt"].cast()
}
///|
/// Get session ipAddress
pub fn Session::ipAddress(self : Session) -> String? {
@core.identity_option(self.to_any()["ipAddress"])
}
///|
/// Get session userAgent
pub fn Session::userAgent(self : Session) -> String? {
@core.identity_option(self.to_any()["userAgent"])
}
///|
/// Auth API - server-side methods
#external
pub type AuthApi
///|
pub fn AuthApi::to_any(self : AuthApi) -> @core.Any = "%identity"
///|
/// Get the API from auth instance
pub fn Auth::api(self : Auth) -> AuthApi {
self.to_any()["api"] |> @core.identity
}
///|
/// Auth handler for HTTP frameworks
pub fn Auth::handler(self : Auth) -> @core.Any {
self.to_any()["handler"] |> @core.identity
}
///|
/// Provider trait - abstraction for auth providers
pub trait Provider {
/// Apply provider configuration to the auth config object
fn apply(Self, @core.Any) -> Unit
}
///|
/// Email and password provider
pub(all) struct EmailPassword {
enabled : Bool
}
///|
pub impl Provider for EmailPassword with fn apply(self, config) {
let obj = @core.new_object()
obj["enabled"] = @core.any(self.enabled)
config._set("emailAndPassword", obj)
}
///|
/// GitHub OAuth provider
pub(all) struct GitHub {
client_id : String
client_secret : String
}
///|
pub impl Provider for GitHub with fn apply(self, config) {
let sp = get_or_create_social_providers(config)
let obj = @core.new_object()
obj["clientId"] = @core.any(self.client_id)
obj["clientSecret"] = @core.any(self.client_secret)
sp._set("github", obj)
}
///|
/// Google OAuth provider
pub(all) struct Google {
client_id : String
client_secret : String
}
///|
pub impl Provider for Google with fn apply(self, config) {
let sp = get_or_create_social_providers(config)
let obj = @core.new_object()
obj["clientId"] = @core.any(self.client_id)
obj["clientSecret"] = @core.any(self.client_secret)
sp._set("google", obj)
}
///|
fn get_or_create_social_providers(config : @core.Any) -> @core.Any {
let existing = config["socialProviders"]
if @core.is_nullish(existing) {
let sp = @core.new_object()
config._set("socialProviders", sp)
sp
} else {
existing
}
}
///|
/// Create a new Auth instance with providers
pub fn betterAuth(
database~ : @core.Any,
providers~ : Array[&Provider],
base_url? : String,
secret? : String,
) -> Auth {
let config = @core.new_object()
config["database"] = @core.any(database)
for provider in providers {
provider.apply(config)
}
if base_url is Some(url) {
config["baseURL"] = @core.any(url)
}
if secret is Some(s) {
config["secret"] = @core.any(s)
}
ffi_better_auth(config |> @core.identity) |> @core.identity
}
///|
extern "js" fn ffi_better_auth(config : @core.Any) -> @core.Any =
#|(config) => {
#| const { betterAuth } = require('better-auth');
#| return betterAuth(config);
#|}
///|
/// Auth response from signUpEmail/signInEmail API
/// Returns { token, user } on success, or { error } on failure
#external
pub type AuthResponse
///|
pub fn AuthResponse::to_any(self : AuthResponse) -> @core.Any = "%identity"
///|
/// Get user from response
pub fn AuthResponse::user(self : AuthResponse) -> User {
self.to_any()["user"] |> @core.identity
}
///|
/// Get session token from response (for cookie setting)
/// Note: signInEmail returns token directly, not nested in session
pub fn AuthResponse::token(self : AuthResponse) -> String {
self.to_any()["token"].cast()
}
///|
/// Check if response has error
pub fn AuthResponse::hasError(self : AuthResponse) -> Bool {
!@core.is_nullish(self.to_any()["error"])
}
///|
/// Get error from response if present
pub fn AuthResponse::error(self : AuthResponse) -> @core.Any {
self.to_any()["error"] |> @core.identity
}
///|
/// Get error message from response if present
pub fn AuthResponse::errorMessage(self : AuthResponse) -> String? {
let err = self.to_any()["error"]
if @core.is_nullish(err) {
None
} else {
Some(err["message"].cast())
}
}
///|
/// Sign up with email
pub async fn AuthApi::signUpEmail(
self : AuthApi,
name~ : String,
email~ : String,
password~ : String,
image? : String,
callback_url? : String,
) -> AuthResponse {
let body = @core.new_object()
body["name"] = @core.any(name)
body["email"] = @core.any(email)
body["password"] = @core.any(password)
if image is Some(img) {
body["image"] = @core.any(img)
}
if callback_url is Some(url) {
body["callbackURL"] = @core.any(url)
}
let req_obj = @core.new_object()
req_obj["body"] = body
let promise = ffi_call_auth_api_with_catch(
self.to_any()["signUpEmail"] |> @core.identity,
req_obj |> @core.identity,
)
promise.wait()
}
///|
/// Sign in with email
pub async fn AuthApi::signInEmail(
self : AuthApi,
email~ : String,
password~ : String,
remember_me? : Bool,
callback_url? : String,
headers? : @core.Any,
) -> AuthResponse {
let body = @core.new_object()
body["email"] = @core.any(email)
body["password"] = @core.any(password)
if remember_me is Some(rm) {
body["rememberMe"] = @core.any(rm)
}
if callback_url is Some(url) {
body["callbackURL"] = @core.any(url)
}
let req_obj = @core.new_object()
req_obj["body"] = body
if headers is Some(h) {
req_obj["headers"] = @core.any(h)
}
let promise = ffi_call_auth_api_with_catch(
self.to_any()["signInEmail"] |> @core.identity,
req_obj |> @core.identity,
)
promise.wait()
}
///|
/// Sign in with social provider (returns redirect URL)
pub async fn AuthApi::signInSocial(
self : AuthApi,
provider~ : String,
callback_url? : String,
) -> @core.Any {
let body = @core.new_object()
body["provider"] = @core.any(provider)
if callback_url is Some(url) {
body["callbackURL"] = @core.any(url)
}
let req_obj = @core.new_object()
req_obj["body"] = body
let promise : @js.Promise[@core.Any] = self.to_any()["signInSocial"]._invoke([
req_obj,
])
|> @core.identity
promise.wait()
}
///|
/// Session response from getSession API
/// Returns { session, user } on success, or null if not authenticated
#external
pub type SessionResponse
///|
pub fn SessionResponse::to_any(self : SessionResponse) -> @core.Any = "%identity"
///|
/// Get session from response
pub fn SessionResponse::session(self : SessionResponse) -> Session {
self.to_any()["session"] |> @core.identity
}
///|
/// Get user from response
pub fn SessionResponse::user(self : SessionResponse) -> User {
self.to_any()["user"] |> @core.identity
}
///|
/// Get session from request headers
/// Returns SessionResponse with { session, user } if authenticated, or null
pub async fn AuthApi::getSession(
self : AuthApi,
headers : @core.Any,
) -> SessionResponse? {
let req_obj = @core.new_object()
req_obj["headers"] = @core.any(headers)
let promise : @js.Promise[@core.Any] = self.to_any()["getSession"]._invoke([
req_obj,
])
|> @core.identity
let result : @core.Any = promise.wait() |> @core.any
if @core.is_nullish(result) || @core.is_nullish(result["user"]) {
None
} else {
Some(result |> @core.identity)
}
}
///|
/// Sign out
pub async fn AuthApi::signOut(self : AuthApi, headers : @core.Any) -> Unit {
let req_obj = @core.new_object()
req_obj["headers"] = @core.any(headers)
let promise : @js.Promise[Unit] = self.to_any()["signOut"]._invoke([req_obj])
|> @core.identity
promise.wait()
}
///|
/// Helper to call auth API methods and catch errors, converting them to { error: { message } }
extern "js" fn ffi_call_auth_api_with_catch(
fn_ : @core.Any,
args : @core.Any,
) -> @js.Promise[AuthResponse] =
#|(fn_, args) => {
#| return fn_(args).catch(err => {
#| // Convert error to { error: { message } } format
#| const message = err.body?.message || err.message || 'Unknown error';
#| return { error: { message, code: err.body?.code || 'ERROR' } };
#| });
#|}