Compare commits

...

2 Commits

2 changed files with 187 additions and 18 deletions

View File

@ -1,10 +1,5 @@
import { HttpClient } from "../core/http";
import {
AuthConfig,
WhoAmI,
StorageAdapter,
MemoryStorage,
} from "./types";
import { AuthConfig, WhoAmI, StorageAdapter, MemoryStorage } from "./types";
export class AuthClient {
private httpClient: HttpClient;
@ -33,14 +28,14 @@ export class AuthClient {
setApiKey(apiKey: string) {
this.currentApiKey = apiKey;
this.currentJwt = undefined;
// Don't clear JWT - it will be cleared explicitly on logout
this.httpClient.setApiKey(apiKey);
this.storage.set("apiKey", apiKey);
}
setJwt(jwt: string) {
this.currentJwt = jwt;
this.currentApiKey = undefined;
// Don't clear API key - keep it as fallback for after logout
this.httpClient.setJwt(jwt);
this.storage.set("jwt", jwt);
}
@ -67,6 +62,51 @@ export class AuthClient {
return token;
}
/**
* Logout user and clear JWT, but preserve API key
* Use this for user logout in apps where API key is app-level credential
*/
async logoutUser(): Promise<void> {
// Attempt server-side logout if using JWT
if (this.currentJwt) {
try {
await this.httpClient.post("/v1/auth/logout", { all: true });
} catch (error) {
// Log warning but don't fail - local cleanup is more important
console.warn(
"Server-side logout failed, continuing with local cleanup:",
error
);
}
}
// Clear JWT only, preserve API key
this.currentJwt = undefined;
this.httpClient.setJwt(undefined);
await this.storage.set("jwt", ""); // Clear JWT from storage
// Ensure API key is loaded and set as active auth method
if (!this.currentApiKey) {
// Try to load from storage
const storedApiKey = await this.storage.get("apiKey");
if (storedApiKey) {
this.currentApiKey = storedApiKey;
}
}
// Restore API key as the active auth method
if (this.currentApiKey) {
this.httpClient.setApiKey(this.currentApiKey);
console.log("[Auth] API key restored after user logout");
} else {
console.warn("[Auth] No API key available after logout");
}
}
/**
* Full logout - clears both JWT and API key
* Use this to completely reset authentication state
*/
async logout(): Promise<void> {
// Only attempt server-side logout if using JWT
// API keys don't support server-side logout with all=true
@ -75,10 +115,13 @@ export class AuthClient {
await this.httpClient.post("/v1/auth/logout", { all: true });
} catch (error) {
// Log warning but don't fail - local cleanup is more important
console.warn('Server-side logout failed, continuing with local cleanup:', error);
console.warn(
"Server-side logout failed, continuing with local cleanup:",
error
);
}
}
// Always clear local state
this.currentApiKey = undefined;
this.currentJwt = undefined;
@ -94,4 +137,78 @@ export class AuthClient {
this.httpClient.setJwt(undefined);
await this.storage.clear();
}
/**
* Request a challenge nonce for wallet authentication
*/
async challenge(params: {
wallet: string;
purpose?: string;
namespace?: string;
}): Promise<{
nonce: string;
wallet: string;
namespace: string;
expires_at: string;
}> {
const response = await this.httpClient.post("/v1/auth/challenge", {
wallet: params.wallet,
purpose: params.purpose || "authentication",
namespace: params.namespace || "default",
});
return response;
}
/**
* Verify wallet signature and get JWT token
*/
async verify(params: {
wallet: string;
nonce: string;
signature: string;
namespace?: string;
}): Promise<{
access_token: string;
refresh_token: string;
subject: string;
namespace: string;
}> {
const response = await this.httpClient.post("/v1/auth/verify", {
wallet: params.wallet,
nonce: params.nonce,
signature: params.signature,
namespace: params.namespace || "default",
});
// Automatically set the JWT
this.setJwt(response.access_token);
return response;
}
/**
* Get API key for wallet (creates namespace ownership)
*/
async getApiKey(params: {
wallet: string;
nonce: string;
signature: string;
namespace?: string;
}): Promise<{
api_key: string;
namespace: string;
wallet: string;
}> {
const response = await this.httpClient.post("/v1/auth/api-key", {
wallet: params.wallet,
nonce: params.nonce,
signature: params.signature,
namespace: params.namespace || "default",
});
// Automatically set the API key
this.setApiKey(response.api_key);
return response;
}
}

View File

@ -27,20 +27,53 @@ export class HttpClient {
setApiKey(apiKey?: string) {
this.apiKey = apiKey;
this.jwt = undefined;
// Don't clear JWT - allow both to coexist
if (typeof console !== "undefined") {
console.log(
"[HttpClient] API key set:",
!!apiKey,
"JWT still present:",
!!this.jwt
);
}
}
setJwt(jwt?: string) {
this.jwt = jwt;
this.apiKey = undefined;
// Don't clear API key - allow both to coexist
if (typeof console !== "undefined") {
console.log(
"[HttpClient] JWT set:",
!!jwt,
"API key still present:",
!!this.apiKey
);
}
}
private getAuthHeaders(): Record<string, string> {
private getAuthHeaders(path: string): Record<string, string> {
const headers: Record<string, string> = {};
if (this.jwt) {
headers["Authorization"] = `Bearer ${this.jwt}`;
} else if (this.apiKey) {
headers["X-API-Key"] = this.apiKey;
// For database operations, ONLY use API key to avoid JWT user context
// interfering with namespace-level authorization
const isDbOperation = path.includes("/v1/rqlite/");
if (isDbOperation) {
// For database operations: use only API key (preferred for namespace operations)
if (this.apiKey) {
headers["X-API-Key"] = this.apiKey;
} else if (this.jwt) {
// Fallback to JWT if no API key
headers["Authorization"] = `Bearer ${this.jwt}`;
}
} else {
// For auth/other operations: send both JWT and API key
if (this.jwt) {
headers["Authorization"] = `Bearer ${this.jwt}`;
}
if (this.apiKey) {
headers["X-API-Key"] = this.apiKey;
}
}
return headers;
}
@ -68,10 +101,29 @@ export class HttpClient {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...this.getAuthHeaders(),
...this.getAuthHeaders(path),
...options.headers,
};
// Debug: Log headers being sent
if (
typeof console !== "undefined" &&
(path.includes("/db/") ||
path.includes("/query") ||
path.includes("/auth/"))
) {
console.log("[HttpClient] Request headers for", path, {
hasAuth: !!headers["Authorization"],
hasApiKey: !!headers["X-API-Key"],
authPrefix: headers["Authorization"]
? headers["Authorization"].substring(0, 20)
: "none",
apiKeyPrefix: headers["X-API-Key"]
? headers["X-API-Key"].substring(0, 20)
: "none",
});
}
const controller = new AbortController();
const requestTimeout = options.timeout ?? this.timeout; // Use override or default
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);