File size: 11,415 Bytes
287a0bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import {ApiApi as DefaultApi} from "./generated";

export interface ClientAuthProvider {
    /**
     * Abstract method for authenticating a client.
     */
    authenticate(): ClientAuthResponse;
}

export interface ClientAuthConfigurationProvider<T> {
    /**
     * Abstract method for getting the configuration for the client.
     */
    getConfig(): T;
}

export interface ClientAuthCredentialsProvider<T> {
    /**
     * 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<T> {
    getCredentials(): T;
}

export interface ClientAuthProtocolAdapter<T> {
    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<SecretStr> {
    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<BasicAuthCredentials> {
    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<any>;

    /**
     * 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<any> | 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<SecretStr> {
    private readonly credentials: SecretStr;

    constructor(_creds: string) {
        this.credentials = new SecretStr(_creds)
    }

    getCredentials(): SecretStr {
        return this.credentials;
    }
}

export class TokenCredentialsProvider implements ClientAuthCredentialsProvider<TokenAuthCredentials> {
    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<any>;
    private readonly providerOptions: { headerType: TokenHeaderType };

    constructor(options: {
        textCredentials: any;
        credentialsProvider: ClientAuthCredentialsProvider<any> | 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<TokenHeaderType, (value: string) => { 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<RequestInit> {
    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<any> | undefined,
    configProvider?: ClientAuthConfigurationProvider<any> | undefined,
    credentials?: any | undefined,
    providerOptions?: any | undefined
}