assistant-cloud
Version:
Cloud integration for assistant-ui
271 lines (270 loc) • 9.52 kB
JavaScript
import { CloudResponseError, readCloudRecord, readCloudString } from "./cloudResponse.js";
import { normalizeBaseUrl } from "./baseUrl.js";
// src/AssistantCloudAuthStrategy.ts
const AUTH_TOKEN_REQUEST_TIMEOUT_MS = 3e4;
const withAuthTokenDeadline = async (operation, run) => {
const controller = new AbortController();
let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
controller.abort();
}, AUTH_TOKEN_REQUEST_TIMEOUT_MS);
try {
return await run(controller.signal);
} catch (error) {
if (timedOut) throw new Error(`Assistant Cloud ${operation} timed out after ${AUTH_TOKEN_REQUEST_TIMEOUT_MS}ms`, { cause: error });
throw error;
} finally {
clearTimeout(timeout);
}
};
const getJwtExpiry = (jwt) => {
try {
const bodyPart = jwt.split(".")[1];
if (!bodyPart) throw new Error("Invalid JWT format");
let base64 = bodyPart.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4 !== 0) base64 += "=";
const payload = atob(base64);
const exp = JSON.parse(payload).exp;
if (!exp || typeof exp !== "number") throw new Error("JWT does not contain a valid \"exp\" field");
return exp * 1e3;
} catch (error) {
throw new Error(`Unable to determine the token expiry: ${error}`);
}
};
const readNonEmptyCloudString = (value, field) => {
const result = readCloudString(value, field);
if (result.length === 0) throw new CloudResponseError(`Invalid Assistant Cloud response for "${field}": expected a non-empty string`);
return result;
};
const readRefreshTokenResponse = (value, field) => {
const refreshToken = readCloudRecord(value, field);
return {
token: readNonEmptyCloudString(refreshToken.token, `${field}.token`),
expires_at: readNonEmptyCloudString(refreshToken.expires_at, `${field}.expires_at`)
};
};
const readAuthTokenResponse = async (response, field) => {
let value;
try {
value = await response.json();
} catch {
throw new CloudResponseError(`Invalid Assistant Cloud response for "${field}": expected valid JSON`);
}
const data = readCloudRecord(value, field);
return {
data,
accessToken: readNonEmptyCloudString(data.access_token, `${field}.access_token`)
};
};
var AssistantCloudJWTAuthStrategy = class {
strategy = "jwt";
cachedToken = null;
tokenExpiry = null;
tokenRequest = null;
generation = 0;
#authTokenCallback;
constructor(authTokenCallback) {
this.#authTokenCallback = authTokenCallback;
}
async getAuthHeaders() {
const currentTime = Date.now();
if (this.cachedToken && this.tokenExpiry && this.tokenExpiry - currentTime > 3e4) return this.createAuthHeaders(this.cachedToken);
if (!this.tokenRequest) this.tokenRequest = this.fetchAuthHeaders(this.generation);
const tokenRequest = this.tokenRequest;
try {
return await tokenRequest;
} finally {
if (this.tokenRequest === tokenRequest) this.tokenRequest = null;
}
}
async fetchAuthHeaders(generation) {
const token = await this.#authTokenCallback();
if (generation !== this.generation) return false;
if (!token) {
this.cachedToken = null;
this.tokenExpiry = null;
return false;
}
const tokenExpiry = getJwtExpiry(token);
this.cachedToken = token;
this.tokenExpiry = tokenExpiry;
return this.createAuthHeaders(token);
}
createAuthHeaders(token) {
return { Authorization: `Bearer ${token}` };
}
readAuthHeaders(headers) {
const authHeader = headers.get("Authorization");
if (!authHeader) return;
const [scheme, token] = authHeader.split(" ");
if (scheme !== "Bearer" || !token) throw new Error("Invalid auth header received");
const tokenExpiry = getJwtExpiry(token);
this.cachedToken = token;
this.tokenExpiry = tokenExpiry;
}
invalidate() {
this.generation++;
this.cachedToken = null;
this.tokenExpiry = null;
this.tokenRequest = null;
}
};
var AssistantCloudAPIKeyAuthStrategy = class {
strategy = "api-key";
#apiKey;
#userId;
#workspaceId;
constructor(apiKey, userId, workspaceId) {
this.#apiKey = apiKey;
this.#userId = userId;
this.#workspaceId = workspaceId;
}
async getAuthHeaders() {
return {
Authorization: `Bearer ${this.#apiKey}`,
"Aui-User-Id": this.#userId,
"Aui-Workspace-Id": this.#workspaceId
};
}
readAuthHeaders() {}
invalidate() {}
};
const LEGACY_AUI_REFRESH_TOKEN_NAME = "aui:refresh_token";
const getRefreshTokenName = (baseUrl) => `${LEGACY_AUI_REFRESH_TOKEN_NAME}:${baseUrl}`;
const removeLegacyRefreshToken = (storage) => {
try {
storage.removeItem(LEGACY_AUI_REFRESH_TOKEN_NAME);
} catch {}
};
const getLocalStorage = () => {
if (!("localStorage" in globalThis)) return null;
try {
return globalThis.localStorage;
} catch {
return null;
}
};
const readRefreshToken = (baseUrl) => {
const storage = getLocalStorage();
if (!storage) return void 0;
try {
const name = getRefreshTokenName(baseUrl);
const value = storage.getItem(name);
if (value) {
removeLegacyRefreshToken(storage);
return JSON.parse(value);
}
const legacyValue = storage.getItem(LEGACY_AUI_REFRESH_TOKEN_NAME);
if (!legacyValue) return void 0;
let refreshToken;
try {
refreshToken = JSON.parse(legacyValue);
} catch {
removeLegacyRefreshToken(storage);
return;
}
storage.setItem(name, legacyValue);
removeLegacyRefreshToken(storage);
return refreshToken;
} catch {
return;
}
};
/** The refresh token of the anonymous identity this browser holds for `baseUrl`, or null when it has none. */
const readAnonymousRefreshToken = (baseUrl) => {
const refreshToken = readRefreshToken(normalizeBaseUrl(baseUrl));
if (!refreshToken) return null;
return new Date(refreshToken.expires_at).getTime() - Date.now() > 3e4 ? refreshToken.token : null;
};
const writeRefreshToken = (baseUrl, refreshToken) => {
const storage = getLocalStorage();
if (!storage) return;
try {
storage.setItem(getRefreshTokenName(baseUrl), JSON.stringify(refreshToken));
} catch {}
};
const removeRefreshToken = (baseUrl) => {
const storage = getLocalStorage();
if (!storage) return;
try {
storage.removeItem(getRefreshTokenName(baseUrl));
} catch {}
};
const anonymousAuthTokenRequests = /* @__PURE__ */ new WeakMap();
const getWebLockManager = () => {
if (!("navigator" in globalThis)) return null;
return globalThis.navigator?.locks ?? null;
};
const getAnonymousAuthLockName = (baseUrl) => `assistant-cloud:anonymous-auth:${baseUrl}`;
const getSharedAnonymousAuthToken = (baseUrl, requestToken) => {
const storage = getLocalStorage();
if (!storage) return requestToken();
let storageRequests = anonymousAuthTokenRequests.get(storage);
if (!storageRequests) {
storageRequests = /* @__PURE__ */ new Map();
anonymousAuthTokenRequests.set(storage, storageRequests);
}
const activeRequest = storageRequests.get(baseUrl);
if (activeRequest) return activeRequest;
const locks = getWebLockManager();
const sharedRequest = (locks ? locks.request(getAnonymousAuthLockName(baseUrl), requestToken) : requestToken()).finally(() => {
if (storageRequests.get(baseUrl) === sharedRequest) storageRequests.delete(baseUrl);
});
storageRequests.set(baseUrl, sharedRequest);
return sharedRequest;
};
var AssistantCloudAnonymousAuthStrategy = class {
strategy = "anon";
baseUrl;
jwtStrategy;
constructor(baseUrl) {
this.baseUrl = baseUrl;
const requestAuthToken = async () => {
const currentTime = Date.now();
const storedRefreshToken = readRefreshToken(this.baseUrl);
if (storedRefreshToken) {
if (new Date(storedRefreshToken.expires_at).getTime() - currentTime > 3e4) {
const refreshedAccessToken = await withAuthTokenDeadline("refresh token request", async (signal) => {
const response = await fetch(`${this.baseUrl}/v1/auth/tokens/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: storedRefreshToken.token }),
signal
});
if (response.ok) {
const { data, accessToken } = await readAuthTokenResponse(response, "refresh auth token response");
if (data.refresh_token != null) writeRefreshToken(this.baseUrl, readRefreshTokenResponse(data.refresh_token, "refresh auth token response.refresh_token"));
return accessToken;
}
if (response.status === 429 || response.status >= 500) throw new Error(`Assistant Cloud token refresh failed with status ${response.status}`);
return null;
});
if (refreshedAccessToken !== null) return refreshedAccessToken;
} else removeRefreshToken(this.baseUrl);
}
return withAuthTokenDeadline("anonymous token request", async (signal) => {
const response = await fetch(`${this.baseUrl}/v1/auth/tokens/anonymous`, {
method: "POST",
signal
});
if (!response.ok) return null;
const { data, accessToken } = await readAuthTokenResponse(response, "anonymous auth token response");
writeRefreshToken(this.baseUrl, readRefreshTokenResponse(data.refresh_token, "anonymous auth token response.refresh_token"));
return accessToken;
});
};
this.jwtStrategy = new AssistantCloudJWTAuthStrategy(() => getSharedAnonymousAuthToken(this.baseUrl, requestAuthToken));
}
async getAuthHeaders() {
return this.jwtStrategy.getAuthHeaders();
}
readAuthHeaders(headers) {
this.jwtStrategy.readAuthHeaders(headers);
}
invalidate() {
this.jwtStrategy.invalidate();
}
};
export { AssistantCloudAPIKeyAuthStrategy, AssistantCloudAnonymousAuthStrategy, AssistantCloudJWTAuthStrategy, readAnonymousRefreshToken };
//# sourceMappingURL=AssistantCloudAuthStrategy.js.map