import {ApiApi as DefaultApi} from "./generated"; export interface ClientAuthProvider { /** * Abstract method for authenticating a client. */ authenticate(): ClientAuthResponse; } export interface ClientAuthConfigurationProvider { /** * Abstract method for getting the configuration for the client. */ getConfig(): T; } export interface ClientAuthCredentialsProvider { /** * Abstract method for getting the credentials for the client. * @param user */ getCredentials(user?: string): T; } enum AuthInfoType { COOKIE = "cookie", HEADER = "header", URL = "url", METADATA = "metadata" } export interface ClientAuthResponse { getAuthInfoType(): AuthInfoType; getAuthInfo(): { key: string, value: string }; } export interface AbstractCredentials { getCredentials(): T; } export interface ClientAuthProtocolAdapter { injectCredentials(injectionContext: T): T; getApi(): any; } class SecretStr { constructor(private readonly secret: string) { } getSecret(): string { return this.secret; } } const base64Encode = (str: string): string => { return Buffer.from(str).toString('base64'); }; class BasicAuthCredentials implements AbstractCredentials { private readonly credentials: SecretStr; constructor(_creds: string) { this.credentials = new SecretStr(base64Encode(_creds)) } getCredentials(): SecretStr { //encode base64 return this.credentials; } } class BasicAuthClientAuthResponse implements ClientAuthResponse { constructor(private readonly credentials: BasicAuthCredentials) { } getAuthInfo(): { key: string; value: string } { return {key: "Authorization", value: "Basic " + this.credentials.getCredentials().getSecret()}; } getAuthInfoType(): AuthInfoType { return AuthInfoType.HEADER; } } export class BasicAuthCredentialsProvider implements ClientAuthCredentialsProvider { private readonly credentials: BasicAuthCredentials; /** * Creates a new BasicAuthCredentialsProvider. This provider loads credentials from provided text credentials or from the environment variable CHROMA_CLIENT_AUTH_CREDENTIALS. * @param _creds - The credentials * @throws {Error} If neither credentials provider or text credentials are supplied. */ constructor(_creds: string | undefined) { if (_creds === undefined && !process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) throw new Error("Credentials must be supplied via environment variable (CHROMA_CLIENT_AUTH_CREDENTIALS) or passed in as configuration."); this.credentials = new BasicAuthCredentials((_creds ?? process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) as string); } getCredentials(): BasicAuthCredentials { return this.credentials; } } class BasicAuthClientAuthProvider implements ClientAuthProvider { private readonly credentialsProvider: ClientAuthCredentialsProvider; /** * Creates a new BasicAuthClientAuthProvider. * @param options - The options for the authentication provider. * @param options.textCredentials - The credentials for the authentication provider. * @param options.credentialsProvider - The credentials provider for the authentication provider. * @throws {Error} If neither credentials provider or text credentials are supplied. */ constructor(options: { textCredentials: any; credentialsProvider: ClientAuthCredentialsProvider | undefined }) { if (!options.credentialsProvider && !options.textCredentials) { throw new Error("Either credentials provider or text credentials must be supplied."); } this.credentialsProvider = options.credentialsProvider || new BasicAuthCredentialsProvider(options.textCredentials); } authenticate(): ClientAuthResponse { return new BasicAuthClientAuthResponse(this.credentialsProvider.getCredentials()); } } class TokenAuthCredentials implements AbstractCredentials { private readonly credentials: SecretStr; constructor(_creds: string) { this.credentials = new SecretStr(_creds) } getCredentials(): SecretStr { return this.credentials; } } export class TokenCredentialsProvider implements ClientAuthCredentialsProvider { private readonly credentials: TokenAuthCredentials; constructor(_creds: string | undefined) { if (_creds === undefined && !process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) throw new Error("Credentials must be supplied via environment variable (CHROMA_CLIENT_AUTH_CREDENTIALS) or passed in as configuration."); this.credentials = new TokenAuthCredentials((_creds ?? process.env.CHROMA_CLIENT_AUTH_CREDENTIALS) as string); } getCredentials(): TokenAuthCredentials { return this.credentials; } } export class TokenClientAuthProvider implements ClientAuthProvider { private readonly credentialsProvider: ClientAuthCredentialsProvider; private readonly providerOptions: { headerType: TokenHeaderType }; constructor(options: { textCredentials: any; credentialsProvider: ClientAuthCredentialsProvider | undefined, providerOptions?: { headerType: TokenHeaderType } }) { if (!options.credentialsProvider && !options.textCredentials) { throw new Error("Either credentials provider or text credentials must be supplied."); } if (options.providerOptions === undefined || !options.providerOptions.hasOwnProperty("headerType")) { this.providerOptions = {headerType: "AUTHORIZATION"}; } else { this.providerOptions = {headerType: options.providerOptions.headerType}; } this.credentialsProvider = options.credentialsProvider || new TokenCredentialsProvider(options.textCredentials); } authenticate(): ClientAuthResponse { return new TokenClientAuthResponse(this.credentialsProvider.getCredentials(), this.providerOptions.headerType); } } type TokenHeaderType = 'AUTHORIZATION' | 'X_CHROMA_TOKEN'; const TokenHeader: Record { key: string; value: string; }> = { AUTHORIZATION: (value: string) => ({key: "Authorization", value: `Bearer ${value}`}), X_CHROMA_TOKEN: (value: string) => ({key: "X-Chroma-Token", value: value}) } class TokenClientAuthResponse implements ClientAuthResponse { constructor(private readonly credentials: TokenAuthCredentials, private readonly headerType: TokenHeaderType = 'AUTHORIZATION') { } getAuthInfo(): { key: string; value: string } { if (this.headerType === 'AUTHORIZATION') { return TokenHeader.AUTHORIZATION(this.credentials.getCredentials().getSecret()); } else if (this.headerType === 'X_CHROMA_TOKEN') { return TokenHeader.X_CHROMA_TOKEN(this.credentials.getCredentials().getSecret()); } else { throw new Error("Invalid header type: " + this.headerType + ". Valid types are: " + Object.keys(TokenHeader).join(", ")); } } getAuthInfoType(): AuthInfoType { return AuthInfoType.HEADER; } } export class IsomorphicFetchClientAuthProtocolAdapter implements ClientAuthProtocolAdapter { authProvider: ClientAuthProvider | undefined; wrapperApi: DefaultApi | undefined; /** * Creates a new adapter of IsomorphicFetchClientAuthProtocolAdapter. * @param api - The API to wrap. * @param authConfiguration - The configuration for the authentication provider. */ constructor(private api: DefaultApi, authConfiguration: AuthOptions) { switch (authConfiguration.provider) { case "basic": this.authProvider = new BasicAuthClientAuthProvider({ textCredentials: authConfiguration.credentials, credentialsProvider: authConfiguration.credentialsProvider }); break; case "token": this.authProvider = new TokenClientAuthProvider({ textCredentials: authConfiguration.credentials, credentialsProvider: authConfiguration.credentialsProvider, providerOptions: authConfiguration.providerOptions }); break; default: this.authProvider = undefined; break; } if (this.authProvider !== undefined) { this.wrapperApi = this.wrapMethods(this.api); } } getApi(): DefaultApi { return this.wrapperApi ?? this.api; } getAllMethods(obj: any): string[] { let methods: string[] = []; let currentObj = obj; do { const objMethods = Object.getOwnPropertyNames(currentObj) .filter(name => typeof currentObj[name] === 'function' && name !== 'constructor'); methods = methods.concat(objMethods); currentObj = Object.getPrototypeOf(currentObj); } while (currentObj); return methods; } wrapMethods(obj: any): any { let self = this; const methodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(obj)) .filter(name => typeof obj[name] === 'function' && name !== 'constructor'); return new Proxy(obj, { get(target, prop: string) { if (methodNames.includes(prop)) { return new Proxy(target[prop], { apply(fn, thisArg, args) { const modifiedArgs = args.map(arg => { if (arg && typeof arg === 'object' && 'method' in arg) { return self.injectCredentials(arg as RequestInit); } return arg; }); if (Object.keys(modifiedArgs[modifiedArgs.length - 1]).length === 0) { modifiedArgs[modifiedArgs.length - 1] = self.injectCredentials({} as RequestInit); } else { modifiedArgs[modifiedArgs.length - 1] = self.injectCredentials(modifiedArgs[modifiedArgs.length - 1] as RequestInit); } return fn.apply(thisArg, modifiedArgs); } }); } return target[prop]; } }); } injectCredentials(injectionContext: RequestInit): RequestInit { const authInfo = this.authProvider?.authenticate().getAuthInfo(); if (authInfo) { const {key, value} = authInfo; injectionContext = { ...injectionContext, headers: { [key]: value }, } } return injectionContext; } } export type AuthOptions = { provider: ClientAuthProvider | string | undefined, credentialsProvider?: ClientAuthCredentialsProvider | undefined, configProvider?: ClientAuthConfigurationProvider | undefined, credentials?: any | undefined, providerOptions?: any | undefined }