Skip to content

Commit c0faf53

Browse files
authored
Embedding header providers (#59)
* couple tiny fixes * added embedding headers providers (no integration + docs + testing) * added embedding headers providers (no docs + testing) * patched a few implemenation bugs * added embedding headers providers (no testing) * added embedding headers providers unit tests * headers integration testing
1 parent 16f729c commit c0faf53

21 files changed

+572
-110
lines changed

scripts/list-embedding-providers.sh

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
1+
# Errors on using unbound variables
2+
set -u
3+
14
# Properly sources the .env file to bring env variables into scope
25
eval "$(tr -d '\r' < .env)"
36

4-
# Sanity check
5-
if [ -z "${APPLICATION_URI}" ]; then
6-
echo "Error: APPLICATION_URI is not set."
7-
exit 1
8-
fi
9-
10-
# Sanity check
11-
if [ -z "${APPLICATION_TOKEN}" ]; then
12-
echo "Error: APPLICATION_TOKEN is not set."
13-
exit 1
14-
fi
15-
167
# jq strips away the irrelevant fields we don't care about
178
curl -sL "${APPLICATION_URI}/api/json/v1" \
189
--header "Token: ${APPLICATION_TOKEN}" \

src/api/clients/data-api-http-client.ts

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515

1616
import {
1717
DEFAULT_DATA_API_AUTH_HEADER,
18-
DEFAULT_EMBEDDING_API_KEY_HEADER,
1918
DEFAULT_NAMESPACE,
20-
DEFAULT_TIMEOUT,
19+
DEFAULT_TIMEOUT, HeaderProvider,
2120
hrTimeMs,
2221
HttpClient,
2322
HTTPClientOptions,
@@ -37,7 +36,8 @@ import {
3736
AdminCommandSucceededEvent,
3837
AdminSpawnOptions,
3938
} from '@/src/devops';
40-
import { TokenProvider } from '@/src/common';
39+
import { nullish, TokenProvider } from '@/src/common';
40+
import { EmbeddingHeadersProvider } from '@/src/data-api/embedding-providers';
4141

4242
/**
4343
* @internal
@@ -55,11 +55,6 @@ interface ExecuteCommandOptions {
5555
collection?: string,
5656
}
5757

58-
interface DataAPIHttpClientOptions extends HTTPClientOptions {
59-
namespace: string | undefined,
60-
emissionStrategy: EmissionStrategy,
61-
}
62-
6358
type EmissionStrategy = (emitter: TypedEmitter<DataAPIClientEvents>) => {
6459
emitCommandStarted(info: DataAPIRequestInfo): void,
6560
emitCommandFailed(info: DataAPIRequestInfo, error: Error, started: number): void,
@@ -98,6 +93,13 @@ const adaptInfo4Devops = (info: DataAPIRequestInfo) => (<const>{
9893
path: info.url,
9994
});
10095

96+
interface DataAPIHttpClientOpts extends HTTPClientOptions {
97+
namespace: string | undefined,
98+
emissionStrategy: EmissionStrategy,
99+
embeddingHeaders: EmbeddingHeadersProvider,
100+
tokenProvider: TokenProvider,
101+
}
102+
101103
/**
102104
* @internal
103105
*/
@@ -106,44 +108,52 @@ export class DataAPIHttpClient extends HttpClient {
106108
public namespace?: string;
107109
public maxTimeMS: number;
108110
public emissionStrategy: ReturnType<EmissionStrategy>
109-
readonly #props: DataAPIHttpClientOptions;
111+
readonly #props: DataAPIHttpClientOpts;
110112

111-
constructor(props: DataAPIHttpClientOptions, embeddingApiKey?: string) {
112-
super(props, mkHeaders(embeddingApiKey));
113+
constructor(props: DataAPIHttpClientOpts) {
114+
super(props, [mkAuthHeaderProvider(props.tokenProvider), props.embeddingHeaders.getHeaders.bind(props.embeddingHeaders)]);
113115
this.namespace = 'namespace' in props ? props.namespace : DEFAULT_NAMESPACE;
114116
this.#props = props;
115117
this.maxTimeMS = this.fetchCtx.maxTimeMS ?? DEFAULT_TIMEOUT;
116118
this.emissionStrategy = props.emissionStrategy(props.emitter);
117119
}
118120

119121
public forCollection(namespace: string, collection: string, opts: CollectionSpawnOptions | undefined): DataAPIHttpClient {
120-
const clone = new DataAPIHttpClient(this.#props, opts?.embeddingApiKey);
122+
const clone = new DataAPIHttpClient({
123+
...this.#props,
124+
embeddingHeaders: EmbeddingHeadersProvider.parseHeaders(opts?.embeddingApiKey),
125+
namespace: namespace,
126+
});
127+
121128
clone.collection = collection;
122-
clone.namespace = namespace;
123129
clone.maxTimeMS = opts?.defaultMaxTimeMS ?? this.maxTimeMS;
130+
124131
return clone;
125132
}
126133

127134
public forDbAdmin(opts: AdminSpawnOptions | undefined): DataAPIHttpClient {
128135
const clone = new DataAPIHttpClient({
129136
...this.#props,
137+
tokenProvider: opts?.adminToken ? TokenProvider.parseToken(opts?.adminToken) : this.#props.tokenProvider,
130138
monitorCommands: opts?.monitorCommands || this.#props.monitorCommands,
131-
applicationToken: TokenProvider.parseToken(opts?.adminToken || this.#props.applicationToken),
132139
baseUrl: opts?.endpointUrl || this.#props.baseUrl,
133140
baseApiPath: opts?.endpointUrl ? '' : this.#props.baseApiPath,
134141
});
142+
135143
clone.emissionStrategy = EmissionStrategy.Admin(this.emitter);
136144
clone.collection = undefined;
137145
clone.namespace = undefined;
146+
138147
return clone;
139148
}
140149

141-
public timeoutManager(timeoutMs: number | undefined) {
142-
return this._mkTimeoutManager(timeoutMs);
150+
public timeoutManager(timeout: number | undefined) {
151+
timeout ??= this.maxTimeMS;
152+
return new TimeoutManager(timeout, () => new DataAPITimeoutError(timeout));
143153
}
144154

145155
public async executeCommand(command: Record<string, any>, options: TimeoutOptions & ExecuteCommandOptions | undefined) {
146-
const timeoutManager = options?.timeoutManager ?? this._mkTimeoutManager(options?.maxTimeMS);
156+
const timeoutManager = options?.timeoutManager ?? this.timeoutManager(options?.maxTimeMS);
147157

148158
return await this._requestDataAPI({
149159
url: this.baseUrl,
@@ -218,11 +228,6 @@ export class DataAPIHttpClient extends HttpClient {
218228
throw e;
219229
}
220230
}
221-
222-
private _mkTimeoutManager(timeout: number | undefined) {
223-
timeout ??= this.maxTimeMS;
224-
return new TimeoutManager(timeout, () => new DataAPITimeoutError(timeout));
225-
}
226231
}
227232

228233
const mkFauxErroredResponse = (message: string): RawDataAPIResponse => {
@@ -269,15 +274,14 @@ export function reviver(_: string, value: any): any {
269274
return value;
270275
}
271276

272-
function mkHeaders(embeddingApiKey: string | undefined) {
273-
if (embeddingApiKey) {
274-
return (token: string | undefined) => ({
275-
[DEFAULT_EMBEDDING_API_KEY_HEADER]: embeddingApiKey,
276-
[DEFAULT_DATA_API_AUTH_HEADER]: token,
277-
});
278-
} else {
279-
return (token: string | undefined) => ({
280-
[DEFAULT_DATA_API_AUTH_HEADER]: token,
281-
});
282-
}
277+
const mkAuthHeaderProvider = (tp: TokenProvider): HeaderProvider => () => {
278+
const token = tp.getToken();
279+
280+
return (token instanceof Promise)
281+
? token.then(mkAuthHeader)
282+
: mkAuthHeader(token);
283283
}
284+
285+
const mkAuthHeader = (token: string | nullish): Record<string, string> => (token)
286+
? { [DEFAULT_DATA_API_AUTH_HEADER]: token }
287+
: {};

src/api/clients/devops-api-http-client.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {
2424
AdminCommandStartedEvent,
2525
AdminCommandSucceededEvent,
2626
} from '@/src/devops';
27-
import { HTTPClientOptions, HttpMethodStrings } from '@/src/api/clients/types';
27+
import { HeaderProvider, HTTPClientOptions, HttpMethodStrings } from '@/src/api/clients/types';
28+
import { nullish, TokenProvider } from '@/src/common';
2829

2930
/**
3031
* @internal
@@ -50,19 +51,23 @@ interface DevopsAPIResponse {
5051
status: number,
5152
}
5253

54+
interface DevOpsAPIHttpClientOpts extends HTTPClientOptions {
55+
tokenProvider: TokenProvider,
56+
}
57+
5358
/**
5459
* @internal
5560
*/
5661
export class DevOpsAPIHttpClient extends HttpClient {
57-
constructor(opts: HTTPClientOptions) {
58-
super(opts, mkHeaders);
62+
constructor(opts: DevOpsAPIHttpClientOpts) {
63+
super(opts, [mkAuthHeaderProvider(opts.tokenProvider)]);
5964
}
6065

6166
public async request(req: DevOpsAPIRequestInfo, options: TimeoutOptions | undefined, started: number = 0): Promise<DevopsAPIResponse> {
6267
const isLongRunning = started !== 0;
6368

6469
try {
65-
const timeoutManager = options?.timeoutManager ?? this._mkTimeoutManager(options?.maxTimeMS);
70+
const timeoutManager = options?.timeoutManager ?? this._timeoutManager(options?.maxTimeMS);
6671
const url = this.baseUrl + req.path;
6772

6873
if (this.monitorCommands && !isLongRunning) {
@@ -109,7 +114,7 @@ export class DevOpsAPIHttpClient extends HttpClient {
109114
}
110115

111116
public async requestLongRunning(req: DevOpsAPIRequestInfo, info: LongRunningRequestInfo): Promise<DevopsAPIResponse> {
112-
const timeoutManager = this._mkTimeoutManager(info.options?.maxTimeMS);
117+
const timeoutManager = this._timeoutManager(info.options?.maxTimeMS);
113118
const isLongRunning = info.options?.blocking !== false;
114119

115120
if (this.monitorCommands) {
@@ -132,6 +137,11 @@ export class DevOpsAPIHttpClient extends HttpClient {
132137
return resp;
133138
}
134139

140+
private _timeoutManager(timeout: number | undefined) {
141+
timeout ??= this.fetchCtx.maxTimeMS ?? (12 * 60 * 1000);
142+
return new TimeoutManager(timeout, (info) => new DevOpsAPITimeoutError(info.url, timeout));
143+
}
144+
135145
private async _awaitStatus(id: string, req: DevOpsAPIRequestInfo, info: LongRunningRequestInfo, timeoutManager: TimeoutManager, started: number): Promise<void> {
136146
if (info.options?.blocking === false) {
137147
return;
@@ -180,15 +190,16 @@ export class DevOpsAPIHttpClient extends HttpClient {
180190
});
181191
}
182192
}
183-
184-
private _mkTimeoutManager(timeout: number | undefined) {
185-
timeout ??= this.fetchCtx.maxTimeMS ?? (12 * 60 * 1000);
186-
return new TimeoutManager(timeout, (info) => new DevOpsAPITimeoutError(info.url, timeout));
187-
}
188193
}
189194

190-
function mkHeaders(token: string | undefined) {
191-
return (token)
192-
? { [DEFAULT_DEVOPS_API_AUTH_HEADER]: `Bearer ${token}` }
193-
: {}
195+
const mkAuthHeaderProvider = (tp: TokenProvider): HeaderProvider => () => {
196+
const token = tp.getToken();
197+
198+
return (token instanceof Promise)
199+
? token.then(mkAuthHeader)
200+
: mkAuthHeader(token);
194201
}
202+
203+
const mkAuthHeader = (token: string | nullish): Record<string, string> => (token)
204+
? { [DEFAULT_DEVOPS_API_AUTH_HEADER]: `Bearer ${token}` }
205+
: {};

src/api/clients/http-client.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import { CLIENT_USER_AGENT, RAGSTACK_REQUESTED_WITH } from '@/src/api/constants'
1616
import { Caller, DataAPIClientEvents } from '@/src/client';
1717
import TypedEmitter from 'typed-emitter';
1818
import { FetchCtx, FetcherResponseInfo } from '@/src/api/fetch/types';
19-
import { HTTPClientOptions, HTTPRequestInfo, MkReqHeaders } from '@/src/api/clients/types';
20-
import { TokenProvider } from '@/src/common';
19+
import { HeaderProvider, HTTPClientOptions, HTTPRequestInfo } from '@/src/api/clients/types';
2120

2221
/**
2322
* @internal
@@ -27,11 +26,9 @@ export abstract class HttpClient {
2726
readonly emitter: TypedEmitter<DataAPIClientEvents>;
2827
readonly monitorCommands: boolean;
2928
readonly fetchCtx: FetchCtx;
30-
readonly applicationToken: TokenProvider;
3129
readonly baseHeaders: Record<string, any>;
3230

33-
protected constructor(options: HTTPClientOptions, readonly mkReqHeaders: MkReqHeaders) {
34-
this.applicationToken = options.applicationToken;
31+
protected constructor(options: HTTPClientOptions, readonly headerProviders: HeaderProvider[]) {
3532
this.baseUrl = options.baseUrl;
3633
this.emitter = options.emitter;
3734
this.monitorCommands = options.monitorCommands;
@@ -63,9 +60,17 @@ export abstract class HttpClient {
6360
? `${info.url}?${new URLSearchParams(params).toString()}`
6461
: info.url;
6562

66-
const token = await this.applicationToken.getToken();
67-
const reqHeaders = this.mkReqHeaders(token ?? undefined);
68-
Object.assign(reqHeaders, this.baseHeaders);
63+
const reqHeaders = { ...this.baseHeaders };
64+
65+
for (const provider of this.headerProviders) {
66+
const maybePromise = provider();
67+
68+
const newHeaders = ('then' in maybePromise)
69+
? await maybePromise
70+
: maybePromise;
71+
72+
Object.assign(reqHeaders, newHeaders);
73+
}
6974

7075
return await this.fetchCtx.ctx.fetch({
7176
url: url,

src/api/clients/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@ import type TypedEmitter from 'typed-emitter';
1616
import type { DataAPICommandEvents } from '@/src/data-api';
1717
import type { FetchCtx, HttpMethods } from '@/src/api';
1818
import type { TimeoutManager } from '@/src/api/timeout-managers';
19-
import { TokenProvider } from '@/src/common';
2019

2120
/**
2221
* @internal
2322
*/
2423
export interface HTTPClientOptions {
2524
baseUrl: string,
2625
baseApiPath?: string | null,
27-
applicationToken: TokenProvider,
2826
emitter: TypedEmitter<DataAPICommandEvents>,
2927
monitorCommands: boolean,
3028
fetchCtx: FetchCtx,
@@ -34,7 +32,7 @@ export interface HTTPClientOptions {
3432
/**
3533
* @internal
3634
*/
37-
export type MkReqHeaders = (token: string | undefined) => Record<string, any>;
35+
export type HeaderProvider = (() => Promise<Record<string, string>> | Record<string, string>);
3836

3937
/**
4038
* @internal

src/common/token-providers/static-token-provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class StaticTokenProvider extends TokenProvider {
5353
*
5454
* @returns the string the token provider was instantiated with.
5555
*/
56-
override getToken(): Promise<string | nullish> {
57-
return Promise.resolve(this.#token);
56+
override getToken(): string | nullish {
57+
return this.#token;
5858
}
5959
}

src/common/token-providers/token-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export abstract class TokenProvider {
4343
* The function which provides the token. It may do any I/O as it wishes to obtain/refresh the token, as it's called
4444
* every time the token is required for use, whether it be for the Data API, or the DevOps API.
4545
*/
46-
abstract getToken(): Promise<string | nullish>;
46+
abstract getToken(): string | nullish | Promise<string | nullish>;
4747

4848
/**
4949
* Turns a string token into a {@link StaticTokenProvider} if necessary. Throws an error if

src/common/token-providers/userpass-token-providers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export class UsernamePasswordTokenProvider extends TokenProvider {
4848
*
4949
* @returns the token in the format `cassandra:[username_b64]:[password_b64]`
5050
*/
51-
override getToken(): Promise<string> {
52-
return Promise.resolve(this.#token);
51+
override getToken(): string {
52+
return this.#token;
5353
}
5454

5555
private _encodeB64(input: string) {

0 commit comments

Comments
 (0)