From fa7e9e5d88df435f537c5987d975ab4e2f97dad5 Mon Sep 17 00:00:00 2001 From: f1ames Date: Mon, 18 Dec 2023 13:17:02 +0100 Subject: [PATCH 1/4] feat(synchronizer): allow to pass client config --- .../src/createMonokleAuthenticator.ts | 16 +++++++++------- .../synchronizer/src/createMonokleFetcher.ts | 12 ++++++------ .../src/createMonokleSynchronizer.ts | 12 +++++++----- .../synchronizer/src/handlers/apiHandler.ts | 18 +++++++++++++++--- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/synchronizer/src/createMonokleAuthenticator.ts b/packages/synchronizer/src/createMonokleAuthenticator.ts index f43d335fe..3ed016d7f 100644 --- a/packages/synchronizer/src/createMonokleAuthenticator.ts +++ b/packages/synchronizer/src/createMonokleAuthenticator.ts @@ -1,4 +1,4 @@ -import {ApiHandler} from './handlers/apiHandler.js'; +import {ApiHandler, ClientConfig} from './handlers/apiHandler.js'; import {DeviceFlowHandler} from './handlers/deviceFlowHandler.js'; import {StorageHandlerAuth} from './handlers/storageHandlerAuth.js'; import {Authenticator} from './utils/authenticator.js'; @@ -7,13 +7,14 @@ import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js'; export async function createMonokleAuthenticatorFromOrigin( authClientId: string, + clientConfig: ClientConfig, origin: string = DEFAULT_ORIGIN, storageHandler: StorageHandlerAuth = new StorageHandlerAuth() ) { try { const originConfig = await fetchOriginConfig(origin); - return createMonokleAuthenticatorFromConfig(authClientId, originConfig, storageHandler); + return createMonokleAuthenticatorFromConfig(authClientId, clientConfig, originConfig, storageHandler); } catch (err: any) { throw err; } @@ -21,25 +22,26 @@ export async function createMonokleAuthenticatorFromOrigin( export function createMonokleAuthenticatorFromConfig( authClientId: string, - config: OriginConfig, + clientConfig: ClientConfig, + originConfig: OriginConfig, storageHandler: StorageHandlerAuth = new StorageHandlerAuth() ) { if (!authClientId) { throw new Error(`No auth clientId provided.`); } - if (!config?.apiOrigin) { + if (!originConfig?.apiOrigin) { throw new Error(`No api origin found in origin config from ${origin}.`); } - if (!config?.authOrigin) { + if (!originConfig?.authOrigin) { throw new Error(`No auth origin found in origin config from ${origin}.`); } return new Authenticator( storageHandler, - new ApiHandler(config), - new DeviceFlowHandler(config.authOrigin, { + new ApiHandler(originConfig, clientConfig), + new DeviceFlowHandler(originConfig.authOrigin, { client_id: authClientId, client_secret: DEFAULT_DEVICE_FLOW_CLIENT_SECRET, id_token_signed_response_alg: DEFAULT_DEVICE_FLOW_ALG, diff --git a/packages/synchronizer/src/createMonokleFetcher.ts b/packages/synchronizer/src/createMonokleFetcher.ts index 1ec800ee5..d7993b3b1 100644 --- a/packages/synchronizer/src/createMonokleFetcher.ts +++ b/packages/synchronizer/src/createMonokleFetcher.ts @@ -1,22 +1,22 @@ import {DEFAULT_ORIGIN} from './constants.js'; -import {ApiHandler} from './handlers/apiHandler.js'; +import {ApiHandler, ClientConfig} from './handlers/apiHandler.js'; import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js'; import {Fetcher} from './utils/fetcher.js'; -export async function createMonokleFetcherFromOrigin(origin: string = DEFAULT_ORIGIN) { +export async function createMonokleFetcherFromOrigin(clientConfig: ClientConfig, origin: string = DEFAULT_ORIGIN) { try { const originConfig = await fetchOriginConfig(origin); - return createMonokleFetcherFromConfig(originConfig); + return createMonokleFetcherFromConfig(clientConfig, originConfig); } catch (err: any) { throw err; } } -export function createMonokleFetcherFromConfig(config: OriginConfig) { - if (!config?.apiOrigin) { +export function createMonokleFetcherFromConfig(clientConfig: ClientConfig, originConfig: OriginConfig) { + if (!originConfig?.apiOrigin) { throw new Error(`No api origin found in origin config from ${origin}.`); } - return new Fetcher(new ApiHandler(config)); + return new Fetcher(new ApiHandler(originConfig, clientConfig)); } diff --git a/packages/synchronizer/src/createMonokleSynchronizer.ts b/packages/synchronizer/src/createMonokleSynchronizer.ts index 59d5ad067..48ccf56f9 100644 --- a/packages/synchronizer/src/createMonokleSynchronizer.ts +++ b/packages/synchronizer/src/createMonokleSynchronizer.ts @@ -1,11 +1,12 @@ import {DEFAULT_ORIGIN} from './constants.js'; -import {ApiHandler} from './handlers/apiHandler.js'; +import {ApiHandler, ClientConfig} from './handlers/apiHandler.js'; import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js'; import {GitHandler} from './handlers/gitHandler.js'; import {StorageHandlerPolicy} from './handlers/storageHandlerPolicy.js'; import {Synchronizer} from './utils/synchronizer.js'; export async function createMonokleSynchronizerFromOrigin( + clientConfig: ClientConfig, origin: string = DEFAULT_ORIGIN, storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(), gitHandler: GitHandler = new GitHandler() @@ -13,20 +14,21 @@ export async function createMonokleSynchronizerFromOrigin( try { const originConfig = await fetchOriginConfig(origin); - return createMonokleSynchronizerFromConfig(originConfig, storageHandler, gitHandler); + return createMonokleSynchronizerFromConfig(clientConfig, originConfig, storageHandler, gitHandler); } catch (err: any) { throw err; } } export function createMonokleSynchronizerFromConfig( - config: OriginConfig, + clientConfig: ClientConfig, + originConfig: OriginConfig, storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(), gitHandler: GitHandler = new GitHandler() ) { - if (!config?.apiOrigin) { + if (!originConfig?.apiOrigin) { throw new Error(`No api origin found in origin config from ${origin}.`); } - return new Synchronizer(storageHandler, new ApiHandler(config), gitHandler); + return new Synchronizer(storageHandler, new ApiHandler(originConfig, clientConfig), gitHandler); } diff --git a/packages/synchronizer/src/handlers/apiHandler.ts b/packages/synchronizer/src/handlers/apiHandler.ts index f0a1e2178..1f8132a4c 100644 --- a/packages/synchronizer/src/handlers/apiHandler.ts +++ b/packages/synchronizer/src/handlers/apiHandler.ts @@ -176,14 +176,20 @@ export type ApiRepoIdData = { }; }; +export type ClientConfig = { + name: string; + version: string; +}; + export class ApiHandler { private _apiUrl: string; + private _clientConfig: ClientConfig; private _originConfig?: OriginConfig; constructor(); - constructor(_apiUrl: string); - constructor(_originConfig: OriginConfig); - constructor(_apiUrlOrOriginConfig: string | OriginConfig = DEFAULT_API_URL) { + constructor(_apiUrl: string, clientConfig?: ClientConfig); + constructor(_originConfig: OriginConfig, clientConfig?: ClientConfig); + constructor(_apiUrlOrOriginConfig: string | OriginConfig = DEFAULT_API_URL, clientConfig?: ClientConfig) { if (typeof _apiUrlOrOriginConfig === 'string') { this._apiUrl = _apiUrlOrOriginConfig; } else if ( @@ -200,6 +206,11 @@ export class ApiHandler { if ((this._apiUrl || '').length === 0) { this._apiUrl = DEFAULT_API_URL; } + + this._clientConfig = { + name: clientConfig?.name || 'unknown', + version: clientConfig?.version || 'unknown', + } } get apiUrl() { @@ -282,6 +293,7 @@ export class ApiHandler { headers: { 'Content-Type': 'application/json', Authorization: this.formatAuthorizationHeader(tokenInfo), + 'User-Agent': `${this._clientConfig.name}; ${this._clientConfig.version}`, }, body: JSON.stringify({ query, From bb6f504dcbccb4871f5214c756b611bb4384bd51 Mon Sep 17 00:00:00 2001 From: f1ames Date: Mon, 18 Dec 2023 13:17:34 +0100 Subject: [PATCH 2/4] chore: reformat code --- packages/synchronizer/src/handlers/apiHandler.ts | 2 +- packages/synchronizer/src/handlers/configHandler.ts | 6 ++++-- packages/synchronizer/src/utils/authenticator.ts | 7 +++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/synchronizer/src/handlers/apiHandler.ts b/packages/synchronizer/src/handlers/apiHandler.ts index 1f8132a4c..beb599f16 100644 --- a/packages/synchronizer/src/handlers/apiHandler.ts +++ b/packages/synchronizer/src/handlers/apiHandler.ts @@ -210,7 +210,7 @@ export class ApiHandler { this._clientConfig = { name: clientConfig?.name || 'unknown', version: clientConfig?.version || 'unknown', - } + }; } get apiUrl() { diff --git a/packages/synchronizer/src/handlers/configHandler.ts b/packages/synchronizer/src/handlers/configHandler.ts index ce39ee32f..5401c82d7 100644 --- a/packages/synchronizer/src/handlers/configHandler.ts +++ b/packages/synchronizer/src/handlers/configHandler.ts @@ -27,10 +27,12 @@ export async function fetchOriginConfig(origin: string, timeout = 30 * 1000) { try { const configUrl = normalize(`${origin}/config.js`); - const response = await fetch(configUrl, { timeout }); + const response = await fetch(configUrl, {timeout}); if (!response.ok) { - throw new Error(`Failed to fetch config from ${configUrl} with status ${response.status}: ${response.statusText}`); + throw new Error( + `Failed to fetch config from ${configUrl} with status ${response.status}: ${response.statusText}` + ); } const responseText = await response.text(); diff --git a/packages/synchronizer/src/utils/authenticator.ts b/packages/synchronizer/src/utils/authenticator.ts index ec57615ca..821db3afc 100644 --- a/packages/synchronizer/src/utils/authenticator.ts +++ b/packages/synchronizer/src/utils/authenticator.ts @@ -77,7 +77,7 @@ export class Authenticator extends EventEmitter { } } - async refreshToken(force = false, options: RefreshTokenOptions = { logoutOnInvalidGrant: true }) { + async refreshToken(force = false, options: RefreshTokenOptions = {logoutOnInvalidGrant: true}) { const authData = this._user.data?.auth; const tokenData = authData?.token; @@ -104,7 +104,10 @@ export class Authenticator extends EventEmitter { } catch (err: any) { // This is a workaround for origin conflict where user is logged in already with different origin // and authenticator is querying different one. - if (options?.logoutOnFail || options?.logoutOnInvalidGrant && err.message.toLowerCase().includes('invalid_grant')) { + if ( + options?.logoutOnFail || + (options?.logoutOnInvalidGrant && err.message.toLowerCase().includes('invalid_grant')) + ) { await this._storageHandler.emptyStoreData(); this._user = new User(null); // Do not emit logout event since we treat this as user not being logged in with desired origin. From d62e100ec59495d0efa3ff6e28b156a48bb7b54e Mon Sep 17 00:00:00 2001 From: f1ames Date: Mon, 18 Dec 2023 14:06:45 +0100 Subject: [PATCH 3/4] fix: follow standard user-agent header format --- .../synchronizer/src/handlers/apiHandler.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/synchronizer/src/handlers/apiHandler.ts b/packages/synchronizer/src/handlers/apiHandler.ts index beb599f16..97c733084 100644 --- a/packages/synchronizer/src/handlers/apiHandler.ts +++ b/packages/synchronizer/src/handlers/apiHandler.ts @@ -179,6 +179,8 @@ export type ApiRepoIdData = { export type ClientConfig = { name: string; version: string; + os?: string; + additionalData?: Record; }; export class ApiHandler { @@ -210,6 +212,8 @@ export class ApiHandler { this._clientConfig = { name: clientConfig?.name || 'unknown', version: clientConfig?.version || 'unknown', + os: clientConfig?.os || '', + additionalData: clientConfig?.additionalData || {}, }; } @@ -293,7 +297,7 @@ export class ApiHandler { headers: { 'Content-Type': 'application/json', Authorization: this.formatAuthorizationHeader(tokenInfo), - 'User-Agent': `${this._clientConfig.name}; ${this._clientConfig.version}`, + 'User-Agent': this.formatUserAgentHeader(this._clientConfig), }, body: JSON.stringify({ query, @@ -310,4 +314,18 @@ export class ApiHandler { : 'Bearer'; return `${tokenType} ${tokenInfo.accessToken}`; } + + private formatUserAgentHeader(clientConfig: ClientConfig) { + const product = `${clientConfig.name}/${clientConfig.version}`; + + const comment = []; + if (clientConfig.os) { + comment.push(clientConfig.os); + } + if (clientConfig.additionalData) { + comment.push(Object.entries(clientConfig.additionalData).map(([key, value]) => `${key}=${value}`)); + } + + return `${product}${comment.length > 0 ? ` (${comment.join('; ')})` : ''}`; + } } From 7126eb44e021c68290d8f9366704177db842a958 Mon Sep 17 00:00:00 2001 From: f1ames Date: Tue, 19 Dec 2023 09:38:55 +0100 Subject: [PATCH 4/4] chore: add changeset --- .changeset/cold-cameras-camp.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-cameras-camp.md diff --git a/.changeset/cold-cameras-camp.md b/.changeset/cold-cameras-camp.md new file mode 100644 index 000000000..5e68b5df4 --- /dev/null +++ b/.changeset/cold-cameras-camp.md @@ -0,0 +1,5 @@ +--- +"@monokle/synchronizer": minor +--- + +Add mechanism to pass client identity config with every API request