From 1c342eec3d284cfa0659c163ea9f1f0059228f3c Mon Sep 17 00:00:00 2001 From: f1ames Date: Wed, 15 Nov 2023 13:26:41 +0100 Subject: [PATCH 01/13] feat: introduce method to fetch origin config --- .../src/__tests__/apiHandler.spec.ts | 1 + .../synchronizer/src/handlers/apiHandler.ts | 39 ++++++++++++++----- packages/synchronizer/src/utils/fetcher.ts | 36 +++++++++++++++++ 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/packages/synchronizer/src/__tests__/apiHandler.spec.ts b/packages/synchronizer/src/__tests__/apiHandler.spec.ts index 1241e3424..b68d3b9f7 100644 --- a/packages/synchronizer/src/__tests__/apiHandler.spec.ts +++ b/packages/synchronizer/src/__tests__/apiHandler.spec.ts @@ -55,6 +55,7 @@ describe('ApiHandler Tests', () => { assert.equal('https://api.monokle.com', (new ApiHandler(null as any)).apiUrl); assert.equal('https://api.monokle.com', (new ApiHandler(undefined as any)).apiUrl); assert.equal('https://api.monokle.com', (new ApiHandler(0 as any)).apiUrl); + assert.equal('https://api.monokle.com', (new ApiHandler([] as any)).apiUrl); }); it('uses passed Api Url', async () => { diff --git a/packages/synchronizer/src/handlers/apiHandler.ts b/packages/synchronizer/src/handlers/apiHandler.ts index 4c3c0e3c6..8101b2db7 100644 --- a/packages/synchronizer/src/handlers/apiHandler.ts +++ b/packages/synchronizer/src/handlers/apiHandler.ts @@ -3,6 +3,7 @@ import fetch from 'node-fetch'; import {SuppressionStatus} from '@monokle/types'; import {DEFAULT_API_URL} from '../constants.js'; import type {TokenInfo} from './storageHandlerAuth.js'; +import { OriginConfig } from '../utils/fetcher.js'; const getUserQuery = ` query getUser { @@ -176,8 +177,24 @@ export type ApiRepoIdData = { }; export class ApiHandler { - constructor(private _apiUrl: string = DEFAULT_API_URL) { - if ((_apiUrl || '').length === 0) { + private _apiUrl: string; + private _originConfig?: OriginConfig; + + constructor(); + constructor(_apiUrl: string); + constructor(_originConfig: OriginConfig); + constructor(_apiUrlOrOriginConfig: string | OriginConfig = DEFAULT_API_URL) { + + if (typeof _apiUrlOrOriginConfig === 'string') { + this._apiUrl = _apiUrlOrOriginConfig; + } else if (_apiUrlOrOriginConfig !== null && typeof _apiUrlOrOriginConfig === 'object' && !Array.isArray(_apiUrlOrOriginConfig)) { + this._originConfig = _apiUrlOrOriginConfig; + this._apiUrl = _apiUrlOrOriginConfig.apiOrigin; + } else { + this._apiUrl = DEFAULT_API_URL; + } + + if ((this._apiUrl || '').length === 0) { this._apiUrl = DEFAULT_API_URL; } } @@ -207,15 +224,19 @@ export class ApiHandler { } generateDeepLink(path: string) { - if (this.apiUrl.includes('staging.monokle.com')) { - return normalizeUrl(`https://app.staging.monokle.com/${path}`); - } else if (this.apiUrl.includes('.monokle.com')) { - return normalizeUrl(`https://app.monokle.com/${path}`); + let appUrl = this._originConfig?.origin; + + if (!appUrl) { + if (this.apiUrl.includes('staging.monokle.com')) { + appUrl = 'https://app.staging.monokle.com/'; + } else if (this.apiUrl.includes('.monokle.com')) { + appUrl = 'https://app.monokle.com/'; + } else { + appUrl = this.apiUrl; + } } - // For any custom base urls we just append the path. - // @TODO this might need adjustment in the future for self-hosted solutions. - return normalizeUrl(`${this.apiUrl}/${path}`); + return normalizeUrl(`${appUrl}/${path}`); } async queryApi(query: string, tokenInfo: TokenInfo, variables = {}): Promise { diff --git a/packages/synchronizer/src/utils/fetcher.ts b/packages/synchronizer/src/utils/fetcher.ts index e674a5a6a..3829b2dd4 100644 --- a/packages/synchronizer/src/utils/fetcher.ts +++ b/packages/synchronizer/src/utils/fetcher.ts @@ -1,3 +1,5 @@ +import normalizeUrl from 'normalize-url'; +import fetch from 'node-fetch'; import {EventEmitter} from 'events'; import {ApiHandler} from '../handlers/apiHandler.js'; import {TokenInfo} from '../handlers/storageHandlerAuth.js'; @@ -13,6 +15,14 @@ export type QueryResult = { error?: string; }; +export type OriginConfig = { + origin: string; + apiOrigin: string; + authOrigin: string; + authClientId: string; + [key: string]: string; +}; + export class Fetcher extends EventEmitter { private _token: TokenInfo | undefined; @@ -20,6 +30,32 @@ export class Fetcher extends EventEmitter { super(); } + static async getOriginConfig(origin: string): Promise { + try { + const configUrl = normalizeUrl(`${origin}/config.js`); + const response = await fetch(configUrl); + const responseText = await response.text(); + + const values = responseText.match(/([A-Z_]+)\s*:\s*"(.*?)"/gm)?.reduce((acc: Record, match) => { + if (match[1] && match[2]) { + acc[match[1]] = match[2]; + } + return acc; + }, {}); + + if (values) { + values.origin = origin; + values.apiOrigin = values.API_ORIGIN; + values.authOrigin = values.OIDC_DISCOVERY_URL; + values.authClientId = values.CLIENT_ID; + } + + return values as OriginConfig; + } catch (error: any) { + return undefined; + } + } + useBearerToken(token: string) { this._token = { accessToken: token, From e2cf7af81a0d957e024414dc6c05e43b58c69a8b Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 16 Nov 2023 11:23:23 +0100 Subject: [PATCH 02/13] test: add origin config tests --- package-lock.json | 162 +++++++++++++++++- packages/synchronizer/package.json | 2 + .../src/__tests__/apiHandler.spec.ts | 41 ++++- .../src/__tests__/fetcher.spec.ts | 41 +++++ packages/synchronizer/src/utils/fetcher.ts | 7 +- 5 files changed, 241 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index db0510caa..cff603a6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7588,6 +7588,16 @@ "integrity": "sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==", "dev": true }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "4.3.9", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.9.tgz", @@ -7869,6 +7879,30 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==" }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -7932,6 +7966,12 @@ "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", "dev": true }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/is-ci": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.3.tgz", @@ -8025,6 +8065,12 @@ "@types/unist": "^2" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -8118,6 +8164,12 @@ "dev": true, "optional": true }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "node_modules/@types/react": { "version": "18.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.14.tgz", @@ -8160,6 +8212,27 @@ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.5.tgz", "integrity": "sha512-NAxro9/RqWXTqdSjccDZAjA4nXK+6zRun+HvibYJfGy8TQhpOC7Vv6v2rlHYKrT0Q8jGGoNRd/xVdHRIQRNlFQ==" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@types/sinon": { "version": "10.0.20", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.20.tgz", @@ -29244,7 +29317,7 @@ }, "packages/components": { "name": "@monokle/components", - "version": "2.3.4", + "version": "2.3.5", "license": "MIT", "dependencies": { "react-fast-compare": "^3.2.1", @@ -29253,7 +29326,7 @@ "devDependencies": { "@ant-design/icons": "4.7.0", "@babel/core": "7.17.8", - "@monokle/validation": "0.31.5", + "@monokle/validation": "0.31.6", "@rjsf/antd": "5.0.0-beta.11", "@storybook/addon-actions": "6.5.16", "@storybook/addon-essentials": "6.5.16", @@ -29505,7 +29578,7 @@ }, "packages/synchronizer": { "name": "@monokle/synchronizer", - "version": "0.10.2", + "version": "0.11.0", "license": "MIT", "dependencies": { "@monokle/types": "*", @@ -29521,11 +29594,13 @@ }, "devDependencies": { "@types/chai": "^4.3.5", + "@types/express": "^4.17.21", "@types/git-url-parse": "^9.0.1", "@types/mocha": "^10.0.1", "@types/sinon": "^10.0.16", "c8": "^8.0.1", "chai": "^4.3.7", + "express": "^4.18.2", "mocha": "^10.2.0", "sinon": "^15.2.0" } @@ -30021,11 +30096,11 @@ }, "packages/types": { "name": "@monokle/types", - "version": "0.3.1" + "version": "0.3.2" }, "packages/validation": { "name": "@monokle/validation", - "version": "0.31.5", + "version": "0.31.6", "license": "MIT", "dependencies": { "@monokle/types": "*", @@ -33195,7 +33270,7 @@ "requires": { "@ant-design/icons": "4.7.0", "@babel/core": "7.17.8", - "@monokle/validation": "0.31.5", + "@monokle/validation": "0.31.6", "@rjsf/antd": "5.0.0-beta.11", "@storybook/addon-actions": "6.5.16", "@storybook/addon-essentials": "6.5.16", @@ -33297,12 +33372,14 @@ "requires": { "@monokle/types": "*", "@types/chai": "^4.3.5", + "@types/express": "^4.17.21", "@types/git-url-parse": "^9.0.1", "@types/mocha": "^10.0.1", "@types/sinon": "^10.0.16", "c8": "^8.0.1", "chai": "^4.3.7", "env-paths": "^2.2.1", + "express": "^4.18.2", "git-url-parse": "^13.1.0", "mkdirp": "^3.0.1", "mocha": "^10.2.0", @@ -36363,6 +36440,16 @@ "integrity": "sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==", "dev": true }, + "@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/chai": { "version": "4.3.9", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.9.tgz", @@ -36644,6 +36731,30 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==" }, + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -36707,6 +36818,12 @@ "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", "dev": true }, + "@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "@types/is-ci": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.3.tgz", @@ -36800,6 +36917,12 @@ "@types/unist": "^2" } }, + "@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -36893,6 +37016,12 @@ "dev": true, "optional": true }, + "@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "@types/react": { "version": "18.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.14.tgz", @@ -36935,6 +37064,27 @@ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.5.tgz", "integrity": "sha512-NAxro9/RqWXTqdSjccDZAjA4nXK+6zRun+HvibYJfGy8TQhpOC7Vv6v2rlHYKrT0Q8jGGoNRd/xVdHRIQRNlFQ==" }, + "@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "requires": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "@types/sinon": { "version": "10.0.20", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.20.tgz", diff --git a/packages/synchronizer/package.json b/packages/synchronizer/package.json index 434bf23bb..a9c67ef92 100644 --- a/packages/synchronizer/package.json +++ b/packages/synchronizer/package.json @@ -50,11 +50,13 @@ }, "devDependencies": { "@types/chai": "^4.3.5", + "@types/express": "^4.17.21", "@types/git-url-parse": "^9.0.1", "@types/mocha": "^10.0.1", "@types/sinon": "^10.0.16", "c8": "^8.0.1", "chai": "^4.3.7", + "express": "^4.18.2", "mocha": "^10.2.0", "sinon": "^15.2.0" } diff --git a/packages/synchronizer/src/__tests__/apiHandler.spec.ts b/packages/synchronizer/src/__tests__/apiHandler.spec.ts index b68d3b9f7..52023ea51 100644 --- a/packages/synchronizer/src/__tests__/apiHandler.spec.ts +++ b/packages/synchronizer/src/__tests__/apiHandler.spec.ts @@ -45,11 +45,11 @@ describe('ApiHandler Tests', () => { }); describe('Api Url', () => { - it('uses default Api Url by default', async () => { + it('uses default Api Url by default', () => { assert.equal('https://api.monokle.com', (new ApiHandler()).apiUrl); }); - it('uses default Api Url when falsy value passed', async () => { + it('uses default Api Url when falsy value passed', () => { assert.equal('https://api.monokle.com', (new ApiHandler('')).apiUrl); assert.equal('https://api.monokle.com', (new ApiHandler(false as any)).apiUrl); assert.equal('https://api.monokle.com', (new ApiHandler(null as any)).apiUrl); @@ -58,11 +58,46 @@ describe('ApiHandler Tests', () => { assert.equal('https://api.monokle.com', (new ApiHandler([] as any)).apiUrl); }); - it('uses passed Api Url', async () => { + it('uses passed Api Url', () => { assert.equal('https://dev.api.monokle.com', (new ApiHandler('https://dev.api.monokle.com')).apiUrl); assert.equal('https://api.monokle.io', (new ApiHandler('https://api.monokle.io')).apiUrl); assert.equal('http://localhost:5000', (new ApiHandler('http://localhost:5000')).apiUrl); assert.equal('http://localhost', (new ApiHandler('http://localhost:80')).apiUrl); }); + + it('uses passed origin data #1', () => { + const apiHandler = new ApiHandler({ + origin: 'https://test.monokle.com', + apiOrigin: 'https://api.test.monokle.com', + authOrigin: 'https://auth.test.monokle.com', + authClientId: 'SAMPLE_CLIENT_ID', + }); + assert.equal('https://api.test.monokle.com', apiHandler.apiUrl); + }); + + it('uses passed origin data #2', () => { + const apiHandler = new ApiHandler({ + origin: 'https://custom.domain.io', + apiOrigin: 'https://custom.domain.io/api', + authOrigin: 'https://custom.domain.io/auth', + authClientId: 'SAMPLE_CLIENT_ID', + }); + + assert.equal('https://custom.domain.io/api', apiHandler.apiUrl); + }); + + it('generates correct deep links', () => { + assert.equal('https://app.monokle.com/projects', (new ApiHandler('')).generateDeepLink('projects')); + assert.equal('https://app.staging.monokle.com/projects', (new ApiHandler('https://api.staging.monokle.com')).generateDeepLink('projects')); + assert.equal('http://localhost:5000/projects', (new ApiHandler('http://localhost:5000')).generateDeepLink('projects')); + + const apiHandler = new ApiHandler({ + origin: 'https://custom.domain.io', + apiOrigin: 'https://custom.domain.io/api', + authOrigin: 'https://custom.domain.io/auth', + authClientId: 'SAMPLE_CLIENT_ID', + }); + assert.equal('https://custom.domain.io/projects', apiHandler.generateDeepLink('projects')); + }); }); }); diff --git a/packages/synchronizer/src/__tests__/fetcher.spec.ts b/packages/synchronizer/src/__tests__/fetcher.spec.ts index 2949142e3..31aacb678 100644 --- a/packages/synchronizer/src/__tests__/fetcher.spec.ts +++ b/packages/synchronizer/src/__tests__/fetcher.spec.ts @@ -1,6 +1,8 @@ import sinon from 'sinon'; import {assert} from 'chai'; +import express from 'express'; import {createDefaultMonokleFetcher} from '../createDefaultMonokleFetcher.js'; +import { Fetcher } from '../utils/fetcher.js'; const TEST_QUERY = ` query getCluster($id: ID) { @@ -117,4 +119,43 @@ describe('Fetcher Tests', () => { assert.match(queryResult.error ?? '', /Connection error/); }); }); + + describe('getOriginConfig', () => { + it('fetches and parses config.js from origin', async () => { + return new Promise((res, rej) => { + const app = express(); + + app.get('/config.js', (_req, res) => { + res.send(` + globalThis.import_meta_env = { + API_ORIGIN: "https://api.monokle.local", + CONTENT_ORIGIN: "https://api.monokle.local", + COLLAB_STREAM_URL: "wss://api.monokle.local/collab", + COLLAB_HTTP_URL: "https://api.monokle.local/collab", + + OIDC_DISCOVERY_URL: "https://id.monokle.local/realms/monokle", + CLIENT_ID: "clientId", + }; + `); + }); + + const server = app.listen(13000, async () => { + try { + const originData = await Fetcher.getOriginConfig('localhost:13000'); + + assert.equal(originData?.origin, 'http://localhost:13000'); + assert.equal(originData?.apiOrigin, 'https://api.monokle.local'); + assert.equal(originData?.authOrigin, 'https://id.monokle.local/realms/monokle'); + assert.equal(originData?.authClientId, 'clientId'); + + res(); + } catch (err) { + rej(err); + } finally { + server.close(); + } + }); + }); + }); + }); }); diff --git a/packages/synchronizer/src/utils/fetcher.ts b/packages/synchronizer/src/utils/fetcher.ts index 3829b2dd4..a446d577c 100644 --- a/packages/synchronizer/src/utils/fetcher.ts +++ b/packages/synchronizer/src/utils/fetcher.ts @@ -36,7 +36,7 @@ export class Fetcher extends EventEmitter { const response = await fetch(configUrl); const responseText = await response.text(); - const values = responseText.match(/([A-Z_]+)\s*:\s*"(.*?)"/gm)?.reduce((acc: Record, match) => { + const values = Array.from(responseText.matchAll(/([A-Z_]+)\s*:\s*"(.*?)"/gm)).reduce((acc: Record, match) => { if (match[1] && match[2]) { acc[match[1]] = match[2]; } @@ -44,7 +44,7 @@ export class Fetcher extends EventEmitter { }, {}); if (values) { - values.origin = origin; + values.origin = normalizeUrl(origin); values.apiOrigin = values.API_ORIGIN; values.authOrigin = values.OIDC_DISCOVERY_URL; values.authClientId = values.CLIENT_ID; @@ -52,7 +52,8 @@ export class Fetcher extends EventEmitter { return values as OriginConfig; } catch (error: any) { - return undefined; + // Rethrow error so integrations can catch it and propagate/react. + throw error; } } From 69fb68aed36508dbabfd588bbd2b1d1ce0afe2f0 Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 16 Nov 2023 11:25:17 +0100 Subject: [PATCH 03/13] chore: add changeset --- .changeset/wise-owls-pull.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-owls-pull.md diff --git a/.changeset/wise-owls-pull.md b/.changeset/wise-owls-pull.md new file mode 100644 index 000000000..923becde7 --- /dev/null +++ b/.changeset/wise-owls-pull.md @@ -0,0 +1,5 @@ +--- +"@monokle/synchronizer": minor +--- + +Introduced method to fetch origin config From 107f9b1458e05ecf516aabe098c672a1040e4633 Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 16 Nov 2023 11:49:22 +0100 Subject: [PATCH 04/13] refactor: add method to get default storage paths for more flexibility --- packages/synchronizer/src/handlers/storageHandler.ts | 6 ++++++ packages/synchronizer/src/handlers/storageHandlerAuth.ts | 7 +++---- packages/synchronizer/src/handlers/storageHandlerPolicy.ts | 6 ++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/synchronizer/src/handlers/storageHandler.ts b/packages/synchronizer/src/handlers/storageHandler.ts index f828e5f47..58d38ec16 100644 --- a/packages/synchronizer/src/handlers/storageHandler.ts +++ b/packages/synchronizer/src/handlers/storageHandler.ts @@ -1,8 +1,10 @@ +import envPaths from 'env-paths'; import {Document, parse} from 'yaml'; import {mkdirp} from 'mkdirp'; import {existsSync, readFileSync} from 'fs'; import {readFile, writeFile} from 'fs/promises'; import {dirname, join, normalize} from 'path'; +import {DEFAULT_STORAGE_CONFIG_FOLDER} from '../constants'; export abstract class StorageHandler { constructor(private _storageFolderPath: string) {} @@ -71,3 +73,7 @@ export abstract class StorageHandler { } } } + +export function getDefaultStorageConfigPaths(suffix = '') { + return envPaths(DEFAULT_STORAGE_CONFIG_FOLDER, {suffix}); +} diff --git a/packages/synchronizer/src/handlers/storageHandlerAuth.ts b/packages/synchronizer/src/handlers/storageHandlerAuth.ts index b78a20e8f..8757fe29f 100644 --- a/packages/synchronizer/src/handlers/storageHandlerAuth.ts +++ b/packages/synchronizer/src/handlers/storageHandlerAuth.ts @@ -1,6 +1,5 @@ -import envPaths from 'env-paths'; -import {StorageHandler} from './storageHandler.js'; -import {DEFAULT_STORAGE_CONFIG_FILE_AUTH, DEFAULT_STORAGE_CONFIG_FOLDER} from '../constants.js'; +import {StorageHandler, getDefaultStorageConfigPaths} from './storageHandler.js'; +import {DEFAULT_STORAGE_CONFIG_FILE_AUTH} from '../constants.js'; import type {TokenSet} from './deviceFlowHandler.js'; export type TokenType = 'Bearer' | 'ApiKey'; @@ -28,7 +27,7 @@ export class StorageHandlerAuth extends StorageHandler { private _defaultFileName: string; constructor( - storageFolderPath: string = envPaths(DEFAULT_STORAGE_CONFIG_FOLDER, {suffix: ''}).config, + storageFolderPath: string = getDefaultStorageConfigPaths().config, defaultFileName: string = DEFAULT_STORAGE_CONFIG_FILE_AUTH ) { super(storageFolderPath); diff --git a/packages/synchronizer/src/handlers/storageHandlerPolicy.ts b/packages/synchronizer/src/handlers/storageHandlerPolicy.ts index a8cddb6c3..109225461 100644 --- a/packages/synchronizer/src/handlers/storageHandlerPolicy.ts +++ b/packages/synchronizer/src/handlers/storageHandlerPolicy.ts @@ -1,13 +1,11 @@ -import envPaths from 'env-paths'; import {Document} from 'yaml'; -import {StorageHandler} from './storageHandler.js'; -import {DEFAULT_STORAGE_CONFIG_FOLDER} from '../constants.js'; +import {StorageHandler, getDefaultStorageConfigPaths} from './storageHandler.js'; import type {ValidationConfig} from '@monokle/types'; export type StoragePolicyFormat = ValidationConfig; export class StorageHandlerPolicy extends StorageHandler { - constructor(storageFolderPath: string = envPaths(DEFAULT_STORAGE_CONFIG_FOLDER, {suffix: ''}).cache) { + constructor(storageFolderPath: string = getDefaultStorageConfigPaths().cache) { super(storageFolderPath); } From 7c6a41b7503a44f001b3a4363491f4994229dd85 Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 16 Nov 2023 12:00:09 +0100 Subject: [PATCH 05/13] chore(synchronizer): reformat code --- .../src/__tests__/apiHandler.spec.ts | 34 ++++++++++------- .../src/__tests__/fetcher.spec.ts | 2 +- .../synchronizer/src/handlers/apiHandler.ts | 24 ++++++++---- packages/synchronizer/src/utils/fetcher.ts | 15 +++++--- .../synchronizer/src/utils/synchronizer.ts | 38 +++++++++++++------ 5 files changed, 73 insertions(+), 40 deletions(-) diff --git a/packages/synchronizer/src/__tests__/apiHandler.spec.ts b/packages/synchronizer/src/__tests__/apiHandler.spec.ts index 52023ea51..d0f2cb9dc 100644 --- a/packages/synchronizer/src/__tests__/apiHandler.spec.ts +++ b/packages/synchronizer/src/__tests__/apiHandler.spec.ts @@ -46,23 +46,23 @@ describe('ApiHandler Tests', () => { describe('Api Url', () => { it('uses default Api Url by default', () => { - assert.equal('https://api.monokle.com', (new ApiHandler()).apiUrl); + assert.equal('https://api.monokle.com', new ApiHandler().apiUrl); }); it('uses default Api Url when falsy value passed', () => { - assert.equal('https://api.monokle.com', (new ApiHandler('')).apiUrl); - assert.equal('https://api.monokle.com', (new ApiHandler(false as any)).apiUrl); - assert.equal('https://api.monokle.com', (new ApiHandler(null as any)).apiUrl); - assert.equal('https://api.monokle.com', (new ApiHandler(undefined as any)).apiUrl); - assert.equal('https://api.monokle.com', (new ApiHandler(0 as any)).apiUrl); - assert.equal('https://api.monokle.com', (new ApiHandler([] as any)).apiUrl); + assert.equal('https://api.monokle.com', new ApiHandler('').apiUrl); + assert.equal('https://api.monokle.com', new ApiHandler(false as any).apiUrl); + assert.equal('https://api.monokle.com', new ApiHandler(null as any).apiUrl); + assert.equal('https://api.monokle.com', new ApiHandler(undefined as any).apiUrl); + assert.equal('https://api.monokle.com', new ApiHandler(0 as any).apiUrl); + assert.equal('https://api.monokle.com', new ApiHandler([] as any).apiUrl); }); it('uses passed Api Url', () => { - assert.equal('https://dev.api.monokle.com', (new ApiHandler('https://dev.api.monokle.com')).apiUrl); - assert.equal('https://api.monokle.io', (new ApiHandler('https://api.monokle.io')).apiUrl); - assert.equal('http://localhost:5000', (new ApiHandler('http://localhost:5000')).apiUrl); - assert.equal('http://localhost', (new ApiHandler('http://localhost:80')).apiUrl); + assert.equal('https://dev.api.monokle.com', new ApiHandler('https://dev.api.monokle.com').apiUrl); + assert.equal('https://api.monokle.io', new ApiHandler('https://api.monokle.io').apiUrl); + assert.equal('http://localhost:5000', new ApiHandler('http://localhost:5000').apiUrl); + assert.equal('http://localhost', new ApiHandler('http://localhost:80').apiUrl); }); it('uses passed origin data #1', () => { @@ -87,9 +87,15 @@ describe('ApiHandler Tests', () => { }); it('generates correct deep links', () => { - assert.equal('https://app.monokle.com/projects', (new ApiHandler('')).generateDeepLink('projects')); - assert.equal('https://app.staging.monokle.com/projects', (new ApiHandler('https://api.staging.monokle.com')).generateDeepLink('projects')); - assert.equal('http://localhost:5000/projects', (new ApiHandler('http://localhost:5000')).generateDeepLink('projects')); + assert.equal('https://app.monokle.com/projects', new ApiHandler('').generateDeepLink('projects')); + assert.equal( + 'https://app.staging.monokle.com/projects', + new ApiHandler('https://api.staging.monokle.com').generateDeepLink('projects') + ); + assert.equal( + 'http://localhost:5000/projects', + new ApiHandler('http://localhost:5000').generateDeepLink('projects') + ); const apiHandler = new ApiHandler({ origin: 'https://custom.domain.io', diff --git a/packages/synchronizer/src/__tests__/fetcher.spec.ts b/packages/synchronizer/src/__tests__/fetcher.spec.ts index 31aacb678..5b22a6bb4 100644 --- a/packages/synchronizer/src/__tests__/fetcher.spec.ts +++ b/packages/synchronizer/src/__tests__/fetcher.spec.ts @@ -2,7 +2,7 @@ import sinon from 'sinon'; import {assert} from 'chai'; import express from 'express'; import {createDefaultMonokleFetcher} from '../createDefaultMonokleFetcher.js'; -import { Fetcher } from '../utils/fetcher.js'; +import {Fetcher} from '../utils/fetcher.js'; const TEST_QUERY = ` query getCluster($id: ID) { diff --git a/packages/synchronizer/src/handlers/apiHandler.ts b/packages/synchronizer/src/handlers/apiHandler.ts index 8101b2db7..3c57170a3 100644 --- a/packages/synchronizer/src/handlers/apiHandler.ts +++ b/packages/synchronizer/src/handlers/apiHandler.ts @@ -3,7 +3,7 @@ import fetch from 'node-fetch'; import {SuppressionStatus} from '@monokle/types'; import {DEFAULT_API_URL} from '../constants.js'; import type {TokenInfo} from './storageHandlerAuth.js'; -import { OriginConfig } from '../utils/fetcher.js'; +import {OriginConfig} from '../utils/fetcher.js'; const getUserQuery = ` query getUser { @@ -170,10 +170,10 @@ export type ApiRepoIdData = { data: { getProject: { repository: { - id: string - } - } - } + id: string; + }; + }; + }; }; export class ApiHandler { @@ -184,10 +184,13 @@ export class ApiHandler { constructor(_apiUrl: string); constructor(_originConfig: OriginConfig); constructor(_apiUrlOrOriginConfig: string | OriginConfig = DEFAULT_API_URL) { - if (typeof _apiUrlOrOriginConfig === 'string') { this._apiUrl = _apiUrlOrOriginConfig; - } else if (_apiUrlOrOriginConfig !== null && typeof _apiUrlOrOriginConfig === 'object' && !Array.isArray(_apiUrlOrOriginConfig)) { + } else if ( + _apiUrlOrOriginConfig !== null && + typeof _apiUrlOrOriginConfig === 'object' && + !Array.isArray(_apiUrlOrOriginConfig) + ) { this._originConfig = _apiUrlOrOriginConfig; this._apiUrl = _apiUrlOrOriginConfig.apiOrigin; } else { @@ -219,7 +222,12 @@ export class ApiHandler { return this.queryApi(getSuppressionsQuery, tokenInfo, {repositoryId}); } - async getRepoId(projectSlug: string, repoOwner: string, repoName: string, tokenInfo: TokenInfo): Promise { + async getRepoId( + projectSlug: string, + repoOwner: string, + repoName: string, + tokenInfo: TokenInfo + ): Promise { return this.queryApi(getRepoIdQuery, tokenInfo, {projectSlug, repoOwner, repoName}); } diff --git a/packages/synchronizer/src/utils/fetcher.ts b/packages/synchronizer/src/utils/fetcher.ts index a446d577c..953539397 100644 --- a/packages/synchronizer/src/utils/fetcher.ts +++ b/packages/synchronizer/src/utils/fetcher.ts @@ -36,12 +36,15 @@ export class Fetcher extends EventEmitter { const response = await fetch(configUrl); const responseText = await response.text(); - const values = Array.from(responseText.matchAll(/([A-Z_]+)\s*:\s*"(.*?)"/gm)).reduce((acc: Record, match) => { - if (match[1] && match[2]) { - acc[match[1]] = match[2]; - } - return acc; - }, {}); + const values = Array.from(responseText.matchAll(/([A-Z_]+)\s*:\s*"(.*?)"/gm)).reduce( + (acc: Record, match) => { + if (match[1] && match[2]) { + acc[match[1]] = match[2]; + } + return acc; + }, + {} + ); if (values) { values.origin = normalizeUrl(origin); diff --git a/packages/synchronizer/src/utils/synchronizer.ts b/packages/synchronizer/src/utils/synchronizer.ts index 85f6afafd..cac97fbca 100644 --- a/packages/synchronizer/src/utils/synchronizer.ts +++ b/packages/synchronizer/src/utils/synchronizer.ts @@ -79,7 +79,7 @@ export class Synchronizer extends EventEmitter { const projectSlugFromInput = this.getProjectSlug(rootPathOrRepoDataOrProjectData); const freshProjectInfo = projectSlugFromInput - ? await this.getProject({ slug: projectSlugFromInput }, tokenInfo) + ? await this.getProject({slug: projectSlugFromInput}, tokenInfo) : await this.getMatchingProject(inputData as RepoRemoteInputData, tokenInfo); return !freshProjectInfo @@ -92,7 +92,11 @@ export class Synchronizer extends EventEmitter { } async getPolicy(rootPath: string, forceRefetch?: boolean, tokenInfo?: TokenInfo): Promise; - async getPolicy(rootPathWithProject: RepoPathInputData, forceRefetch?: boolean, tokenInfo?: TokenInfo): Promise; + async getPolicy( + rootPathWithProject: RepoPathInputData, + forceRefetch?: boolean, + tokenInfo?: TokenInfo + ): Promise; async getPolicy(repoData: RepoRemoteInputData, forceRefetch?: boolean, tokenInfo?: TokenInfo): Promise; async getPolicy(projectData: ProjectInputData, forceRefetch?: boolean, tokenInfo?: TokenInfo): Promise; async getPolicy( @@ -150,7 +154,7 @@ export class Synchronizer extends EventEmitter { const projectSlugFromInput = this.getProjectSlug(rootPathOrRepoDataOrProjectData); if (projectSlugFromInput) { - this._pullPromise = this.fetchPolicyForProject({ slug: projectSlugFromInput }, tokenInfo); + this._pullPromise = this.fetchPolicyForProject({slug: projectSlugFromInput}, tokenInfo); return this._pullPromise; } @@ -242,10 +246,17 @@ export class Synchronizer extends EventEmitter { private async getRepoId(repoData: RepoRemoteInputData, tokenInfo: TokenInfo) { if (repoData.ownerProjectSlug) { - const repoIdData = await this._apiHandler.getRepoId(repoData.ownerProjectSlug, repoData.owner, repoData.name, tokenInfo); + const repoIdData = await this._apiHandler.getRepoId( + repoData.ownerProjectSlug, + repoData.owner, + repoData.name, + tokenInfo + ); if (!repoIdData?.data?.getProject?.repository?.id) { - throw new Error(`The '${repoData.owner}/${repoData.name}' repository does not belong to a '${repoData.ownerProjectSlug}' project.`); + throw new Error( + `The '${repoData.owner}/${repoData.name}' repository does not belong to a '${repoData.ownerProjectSlug}' project.` + ); } return repoIdData.data.getProject.repository.id; @@ -325,7 +336,10 @@ export class Synchronizer extends EventEmitter { } } - private async getMatchingProject(repoData: RepoRemoteInputData, tokenInfo: TokenInfo): Promise { + private async getMatchingProject( + repoData: RepoRemoteInputData, + tokenInfo: TokenInfo + ): Promise { const userData = await this._apiHandler.getUser(tokenInfo); if (!userData?.data?.me) { throw new Error('Cannot fetch user data, make sure you are authenticated and have internet access.'); @@ -421,7 +435,9 @@ export class Synchronizer extends EventEmitter { return `${prefix}-${repoData.provider}-${repoData.owner}-${repoData.name}`; } - private async getRepoOrProjectData(inputData: string | RepoPathInputData | RepoRemoteInputData | ProjectInputData): Promise { + private async getRepoOrProjectData( + inputData: string | RepoPathInputData | RepoRemoteInputData | ProjectInputData + ): Promise { if (this.isProjectData(inputData)) { return inputData as ProjectInputData; } @@ -434,7 +450,7 @@ export class Synchronizer extends EventEmitter { }; } - return typeof inputData === 'string' ? await this.getRootGitData(inputData) : inputData as RepoRemoteData; + return typeof inputData === 'string' ? await this.getRootGitData(inputData) : (inputData as RepoRemoteData); } private isProjectData(projectData: any) { @@ -446,8 +462,8 @@ export class Synchronizer extends EventEmitter { } private getProjectSlug(input: any) { - return this.isProjectData(input) || input.ownerProjectSlug?.length > 0 ? - input.slug ?? input.ownerProjectSlug : - undefined; + return this.isProjectData(input) || input.ownerProjectSlug?.length > 0 + ? input.slug ?? input.ownerProjectSlug + : undefined; } } From 506ba0c237e19b4209d6839f53834be1cb8c2abe Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 16 Nov 2023 12:01:21 +0100 Subject: [PATCH 06/13] fix: fix imports --- packages/synchronizer/src/handlers/storageHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/synchronizer/src/handlers/storageHandler.ts b/packages/synchronizer/src/handlers/storageHandler.ts index 58d38ec16..7ebc9b520 100644 --- a/packages/synchronizer/src/handlers/storageHandler.ts +++ b/packages/synchronizer/src/handlers/storageHandler.ts @@ -4,7 +4,7 @@ import {mkdirp} from 'mkdirp'; import {existsSync, readFileSync} from 'fs'; import {readFile, writeFile} from 'fs/promises'; import {dirname, join, normalize} from 'path'; -import {DEFAULT_STORAGE_CONFIG_FOLDER} from '../constants'; +import {DEFAULT_STORAGE_CONFIG_FOLDER} from '../constants.js'; export abstract class StorageHandler { constructor(private _storageFolderPath: string) {} From e230a174feeba50efb7db497b77e5d35e78b1ae9 Mon Sep 17 00:00:00 2001 From: f1ames Date: Thu, 16 Nov 2023 14:49:57 +0100 Subject: [PATCH 07/13] refactor: add factory methods with origin --- .../createMonokleAuthenticatorFromOrigin.ts | 36 +++++++++++++++++++ .../createMonokleSynchronizerFromOrigin.ts | 19 ++++++++++ packages/synchronizer/src/index.ts | 3 ++ 3 files changed, 58 insertions(+) create mode 100644 packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts create mode 100644 packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts diff --git a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts new file mode 100644 index 000000000..f885c5656 --- /dev/null +++ b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts @@ -0,0 +1,36 @@ +import {ApiHandler} from './handlers/apiHandler.js'; +import {DeviceFlowHandler} from './handlers/deviceFlowHandler.js'; +import {StorageHandlerAuth} from './handlers/storageHandlerAuth.js'; +import {Fetcher} from './utils/fetcher.js'; +import {Authenticator} from './utils/authenticator.js'; +import {DEFAULT_DEVICE_FLOW_ALG, DEFAULT_DEVICE_FLOW_CLIENT_SECRET} from './constants.js'; + +export async function createMonokleAuthenticatorFromOrigin(origin: string) { + try { + const originConfig = await Fetcher.getOriginConfig(origin); + + if (!originConfig?.apiOrigin) { + throw new Error(`No api origin found in origin config from ${origin}.`); + } + + if (!originConfig?.authOrigin) { + throw new Error(`No auth origin found in origin config from ${origin}.`); + } + + if (!originConfig?.clientId) { + throw new Error(`No auth clientId found in origin config from ${origin}.`); + } + + return new Authenticator( + new StorageHandlerAuth(), + new ApiHandler(originConfig.apiOrigin), + new DeviceFlowHandler(originConfig.authOrigin, { + client_id: originConfig.clientId, + client_secret: DEFAULT_DEVICE_FLOW_CLIENT_SECRET, + id_token_signed_response_alg: DEFAULT_DEVICE_FLOW_ALG, + }) + ); + } catch (err: any) { + throw err; + } +} diff --git a/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts b/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts new file mode 100644 index 000000000..263cdbcb6 --- /dev/null +++ b/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts @@ -0,0 +1,19 @@ +import {ApiHandler} from './handlers/apiHandler.js'; +import {GitHandler} from './handlers/gitHandler.js'; +import {StorageHandlerPolicy} from './handlers/storageHandlerPolicy.js'; +import {Fetcher} from './utils/fetcher.js'; +import {Synchronizer} from './utils/synchronizer.js'; + +export async function createMonokleSynchronizerFromOrigin(origin: string) { + try { + const originConfig = await Fetcher.getOriginConfig(origin); + + if (!originConfig?.apiOrigin) { + throw new Error(`No api origin found in origin config from ${origin}.`); + } + + return new Synchronizer(new StorageHandlerPolicy(), new ApiHandler(originConfig.apiOrigin), new GitHandler()); + } catch (err: any) { + throw err; + } +} diff --git a/packages/synchronizer/src/index.ts b/packages/synchronizer/src/index.ts index 07315d591..b7998f5ba 100644 --- a/packages/synchronizer/src/index.ts +++ b/packages/synchronizer/src/index.ts @@ -16,3 +16,6 @@ export * from './constants.js'; export * from './createDefaultMonokleAuthenticator.js'; export * from './createDefaultMonokleFetcher.js'; export * from './createDefaultMonokleSynchronizer.js'; + +export * from './createMonokleAuthenticatorFromOrigin.js'; +export * from './createMonokleSynchronizerFromOrigin.js'; From 240066cb19d79b8edda255ae7a3a1cc8882c0622 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 17 Nov 2023 09:57:15 +0100 Subject: [PATCH 08/13] refactor: improve config fetching; deprecate default creators --- packages/synchronizer/src/constants.ts | 1 + .../src/createDefaultMonokleAuthenticator.ts | 10 +++++++++ .../src/createDefaultMonokleSynchronizer.ts | 10 +++++++++ .../createMonokleAuthenticatorFromOrigin.ts | 4 ++-- .../createMonokleSynchronizerFromOrigin.ts | 3 ++- packages/synchronizer/src/utils/fetcher.ts | 21 +++++++++++++++++++ 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/synchronizer/src/constants.ts b/packages/synchronizer/src/constants.ts index f00ae5ec8..4532e5eae 100644 --- a/packages/synchronizer/src/constants.ts +++ b/packages/synchronizer/src/constants.ts @@ -1,6 +1,7 @@ export const DEFAULT_STORAGE_CONFIG_FOLDER = 'monokle'; export const DEFAULT_STORAGE_CONFIG_FILE_AUTH = 'auth.yaml'; +export const DEFAULT_ORIGIN = 'https://app.monokle.com'; export const DEFAULT_API_URL = 'https://api.monokle.com'; export const DEFAULT_DEVICE_FLOW_IDP_URL = 'https://id.monokle.com/realms/monokle'; diff --git a/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts b/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts index 5b6271772..668a7a93e 100644 --- a/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts +++ b/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts @@ -3,6 +3,16 @@ import {DeviceFlowHandler} from './handlers/deviceFlowHandler.js'; import {StorageHandlerAuth} from './handlers/storageHandlerAuth.js'; import {Authenticator} from './utils/authenticator.js'; +/** + * Creates default Monokle Authenticator instance. + * + * @deprecated Use createMonokleAuthenticatorFromOrigin instead which does not rely on hardcoded config. + * + * @param storageHandler + * @param apiHandler + * @param deviceFlowHandler + * @returns Authenticator instance + */ export function createDefaultMonokleAuthenticator( storageHandler: StorageHandlerAuth = new StorageHandlerAuth(), apiHandler: ApiHandler = new ApiHandler(), diff --git a/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts b/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts index cfa8f2b9e..59b96d3ae 100644 --- a/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts +++ b/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts @@ -3,6 +3,16 @@ import {GitHandler} from './handlers/gitHandler.js'; import {StorageHandlerPolicy} from './handlers/storageHandlerPolicy.js'; import {Synchronizer} from './utils/synchronizer.js'; +/** + * Creates default Monokle Synchronizer instance. + * + * @deprecated Use createMonokleSynchronizerFromOrigin instead which does not rely on hardcoded config. + * + * @param storageHandler + * @param apiHandler + * @param gitHandler + * @returns Synchronizer instance + */ export function createDefaultMonokleSynchronizer( storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(), apiHandler: ApiHandler = new ApiHandler(), diff --git a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts index f885c5656..a886ecda6 100644 --- a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts +++ b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts @@ -3,9 +3,9 @@ import {DeviceFlowHandler} from './handlers/deviceFlowHandler.js'; import {StorageHandlerAuth} from './handlers/storageHandlerAuth.js'; import {Fetcher} from './utils/fetcher.js'; import {Authenticator} from './utils/authenticator.js'; -import {DEFAULT_DEVICE_FLOW_ALG, DEFAULT_DEVICE_FLOW_CLIENT_SECRET} from './constants.js'; +import {DEFAULT_DEVICE_FLOW_ALG, DEFAULT_DEVICE_FLOW_CLIENT_SECRET, DEFAULT_ORIGIN} from './constants.js'; -export async function createMonokleAuthenticatorFromOrigin(origin: string) { +export async function createMonokleAuthenticatorFromOrigin(origin: string = DEFAULT_ORIGIN) { try { const originConfig = await Fetcher.getOriginConfig(origin); diff --git a/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts b/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts index 263cdbcb6..7190e365d 100644 --- a/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts +++ b/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts @@ -1,10 +1,11 @@ +import {DEFAULT_ORIGIN} from './constants.js'; import {ApiHandler} from './handlers/apiHandler.js'; import {GitHandler} from './handlers/gitHandler.js'; import {StorageHandlerPolicy} from './handlers/storageHandlerPolicy.js'; import {Fetcher} from './utils/fetcher.js'; import {Synchronizer} from './utils/synchronizer.js'; -export async function createMonokleSynchronizerFromOrigin(origin: string) { +export async function createMonokleSynchronizerFromOrigin(origin: string = DEFAULT_ORIGIN) { try { const originConfig = await Fetcher.getOriginConfig(origin); diff --git a/packages/synchronizer/src/utils/fetcher.ts b/packages/synchronizer/src/utils/fetcher.ts index 953539397..83aa7762b 100644 --- a/packages/synchronizer/src/utils/fetcher.ts +++ b/packages/synchronizer/src/utils/fetcher.ts @@ -23,7 +23,14 @@ export type OriginConfig = { [key: string]: string; }; +export type CachedOriginConfig = { + config: OriginConfig; + downloadedAt: number; +}; + export class Fetcher extends EventEmitter { + static _originConfig: CachedOriginConfig | undefined; + private _token: TokenInfo | undefined; constructor(private _apiHandler: ApiHandler) { @@ -31,6 +38,15 @@ export class Fetcher extends EventEmitter { } static async getOriginConfig(origin: string): Promise { + if (Fetcher._originConfig) { + const now = Date.now(); + + // Use already fetched config if it's less than 5 minutes old. + if (now - Fetcher._originConfig.downloadedAt < 1000 * 60 * 5) { + return Fetcher._originConfig.config; + } + } + try { const configUrl = normalizeUrl(`${origin}/config.js`); const response = await fetch(configUrl); @@ -53,6 +69,11 @@ export class Fetcher extends EventEmitter { values.authClientId = values.CLIENT_ID; } + Fetcher._originConfig = { + config: values as OriginConfig, + downloadedAt: Date.now(), + }; + return values as OriginConfig; } catch (error: any) { // Rethrow error so integrations can catch it and propagate/react. From c3e0559f93911fbd02fe98d3e5829e488bbced35 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 17 Nov 2023 10:38:07 +0100 Subject: [PATCH 09/13] fix: fix config prop name --- .../synchronizer/src/createMonokleAuthenticatorFromOrigin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts index a886ecda6..5daa9b2cd 100644 --- a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts +++ b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts @@ -17,7 +17,7 @@ export async function createMonokleAuthenticatorFromOrigin(origin: string = DEFA throw new Error(`No auth origin found in origin config from ${origin}.`); } - if (!originConfig?.clientId) { + if (!originConfig?.authClientId) { throw new Error(`No auth clientId found in origin config from ${origin}.`); } @@ -25,7 +25,7 @@ export async function createMonokleAuthenticatorFromOrigin(origin: string = DEFA new StorageHandlerAuth(), new ApiHandler(originConfig.apiOrigin), new DeviceFlowHandler(originConfig.authOrigin, { - client_id: originConfig.clientId, + client_id: originConfig.authClientId, client_secret: DEFAULT_DEVICE_FLOW_CLIENT_SECRET, id_token_signed_response_alg: DEFAULT_DEVICE_FLOW_ALG, }) From 4e2fb013aface8542dab996e2c28fa5125becd71 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 17 Nov 2023 11:23:42 +0100 Subject: [PATCH 10/13] fix: require client id to be provided --- .../synchronizer/src/__tests__/apiHandler.spec.ts | 3 --- packages/synchronizer/src/__tests__/fetcher.spec.ts | 1 - .../src/createMonokleAuthenticatorFromOrigin.ts | 12 ++++++------ packages/synchronizer/src/utils/fetcher.ts | 2 -- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/synchronizer/src/__tests__/apiHandler.spec.ts b/packages/synchronizer/src/__tests__/apiHandler.spec.ts index d0f2cb9dc..9eea2258f 100644 --- a/packages/synchronizer/src/__tests__/apiHandler.spec.ts +++ b/packages/synchronizer/src/__tests__/apiHandler.spec.ts @@ -70,7 +70,6 @@ describe('ApiHandler Tests', () => { origin: 'https://test.monokle.com', apiOrigin: 'https://api.test.monokle.com', authOrigin: 'https://auth.test.monokle.com', - authClientId: 'SAMPLE_CLIENT_ID', }); assert.equal('https://api.test.monokle.com', apiHandler.apiUrl); }); @@ -80,7 +79,6 @@ describe('ApiHandler Tests', () => { origin: 'https://custom.domain.io', apiOrigin: 'https://custom.domain.io/api', authOrigin: 'https://custom.domain.io/auth', - authClientId: 'SAMPLE_CLIENT_ID', }); assert.equal('https://custom.domain.io/api', apiHandler.apiUrl); @@ -101,7 +99,6 @@ describe('ApiHandler Tests', () => { origin: 'https://custom.domain.io', apiOrigin: 'https://custom.domain.io/api', authOrigin: 'https://custom.domain.io/auth', - authClientId: 'SAMPLE_CLIENT_ID', }); assert.equal('https://custom.domain.io/projects', apiHandler.generateDeepLink('projects')); }); diff --git a/packages/synchronizer/src/__tests__/fetcher.spec.ts b/packages/synchronizer/src/__tests__/fetcher.spec.ts index 5b22a6bb4..78a1fa70c 100644 --- a/packages/synchronizer/src/__tests__/fetcher.spec.ts +++ b/packages/synchronizer/src/__tests__/fetcher.spec.ts @@ -146,7 +146,6 @@ describe('Fetcher Tests', () => { assert.equal(originData?.origin, 'http://localhost:13000'); assert.equal(originData?.apiOrigin, 'https://api.monokle.local'); assert.equal(originData?.authOrigin, 'https://id.monokle.local/realms/monokle'); - assert.equal(originData?.authClientId, 'clientId'); res(); } catch (err) { diff --git a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts index 5daa9b2cd..f98b5269b 100644 --- a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts +++ b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts @@ -5,10 +5,14 @@ import {Fetcher} from './utils/fetcher.js'; import {Authenticator} from './utils/authenticator.js'; import {DEFAULT_DEVICE_FLOW_ALG, DEFAULT_DEVICE_FLOW_CLIENT_SECRET, DEFAULT_ORIGIN} from './constants.js'; -export async function createMonokleAuthenticatorFromOrigin(origin: string = DEFAULT_ORIGIN) { +export async function createMonokleAuthenticatorFromOrigin(authClientId: string, origin: string = DEFAULT_ORIGIN) { try { const originConfig = await Fetcher.getOriginConfig(origin); + if (!authClientId) { + throw new Error(`No auth clientId provided.`); + } + if (!originConfig?.apiOrigin) { throw new Error(`No api origin found in origin config from ${origin}.`); } @@ -17,15 +21,11 @@ export async function createMonokleAuthenticatorFromOrigin(origin: string = DEFA throw new Error(`No auth origin found in origin config from ${origin}.`); } - if (!originConfig?.authClientId) { - throw new Error(`No auth clientId found in origin config from ${origin}.`); - } - return new Authenticator( new StorageHandlerAuth(), new ApiHandler(originConfig.apiOrigin), new DeviceFlowHandler(originConfig.authOrigin, { - client_id: originConfig.authClientId, + 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/utils/fetcher.ts b/packages/synchronizer/src/utils/fetcher.ts index 83aa7762b..a59ab127d 100644 --- a/packages/synchronizer/src/utils/fetcher.ts +++ b/packages/synchronizer/src/utils/fetcher.ts @@ -19,7 +19,6 @@ export type OriginConfig = { origin: string; apiOrigin: string; authOrigin: string; - authClientId: string; [key: string]: string; }; @@ -66,7 +65,6 @@ export class Fetcher extends EventEmitter { values.origin = normalizeUrl(origin); values.apiOrigin = values.API_ORIGIN; values.authOrigin = values.OIDC_DISCOVERY_URL; - values.authClientId = values.CLIENT_ID; } Fetcher._originConfig = { From e9eb04583261221c4886fd806643d3c5fbaa2254 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 17 Nov 2023 11:55:50 +0100 Subject: [PATCH 11/13] fix: fix origin config cache --- packages/synchronizer/src/utils/fetcher.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/synchronizer/src/utils/fetcher.ts b/packages/synchronizer/src/utils/fetcher.ts index a59ab127d..958739aa6 100644 --- a/packages/synchronizer/src/utils/fetcher.ts +++ b/packages/synchronizer/src/utils/fetcher.ts @@ -25,6 +25,7 @@ export type OriginConfig = { export type CachedOriginConfig = { config: OriginConfig; downloadedAt: number; + origin: string; }; export class Fetcher extends EventEmitter { @@ -38,10 +39,8 @@ export class Fetcher extends EventEmitter { static async getOriginConfig(origin: string): Promise { if (Fetcher._originConfig) { - const now = Date.now(); - - // Use already fetched config if it's less than 5 minutes old. - if (now - Fetcher._originConfig.downloadedAt < 1000 * 60 * 5) { + // Use recently fetched config if from same origin and it's less than 5 minutes old. + if (origin === Fetcher._originConfig.origin && (Date.now()) - Fetcher._originConfig.downloadedAt < 1000 * 60 * 5) { return Fetcher._originConfig.config; } } @@ -70,6 +69,7 @@ export class Fetcher extends EventEmitter { Fetcher._originConfig = { config: values as OriginConfig, downloadedAt: Date.now(), + origin, }; return values as OriginConfig; From c559c45b9088acb3cb7fb941e99ab7ddeb5ca72f Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 17 Nov 2023 15:40:23 +0100 Subject: [PATCH 12/13] refactor: extract origin config fetching logic --- .../src/__tests__/fetcher.spec.ts | 4 +- .../src/createDefaultMonokleAuthenticator.ts | 2 +- .../src/createDefaultMonokleFetcher.ts | 8 +++ .../src/createDefaultMonokleSynchronizer.ts | 2 +- .../createMonokleAuthenticatorFromOrigin.ts | 58 ++++++++++-------- .../src/createMonokleFetcherFromOrigin.ts | 22 +++++++ .../createMonokleSynchronizerFromOrigin.ts | 28 ++++++--- .../synchronizer/src/handlers/apiHandler.ts | 2 +- .../src/handlers/configHandler.ts | 59 +++++++++++++++++++ packages/synchronizer/src/index.ts | 2 + packages/synchronizer/src/utils/fetcher.ts | 59 ------------------- 11 files changed, 151 insertions(+), 95 deletions(-) create mode 100644 packages/synchronizer/src/createMonokleFetcherFromOrigin.ts create mode 100644 packages/synchronizer/src/handlers/configHandler.ts diff --git a/packages/synchronizer/src/__tests__/fetcher.spec.ts b/packages/synchronizer/src/__tests__/fetcher.spec.ts index 78a1fa70c..178f118b3 100644 --- a/packages/synchronizer/src/__tests__/fetcher.spec.ts +++ b/packages/synchronizer/src/__tests__/fetcher.spec.ts @@ -2,7 +2,7 @@ import sinon from 'sinon'; import {assert} from 'chai'; import express from 'express'; import {createDefaultMonokleFetcher} from '../createDefaultMonokleFetcher.js'; -import {Fetcher} from '../utils/fetcher.js'; +import {fetchOriginConfig} from '../handlers/configHandler.js'; const TEST_QUERY = ` query getCluster($id: ID) { @@ -141,7 +141,7 @@ describe('Fetcher Tests', () => { const server = app.listen(13000, async () => { try { - const originData = await Fetcher.getOriginConfig('localhost:13000'); + const originData = await fetchOriginConfig('localhost:13000'); assert.equal(originData?.origin, 'http://localhost:13000'); assert.equal(originData?.apiOrigin, 'https://api.monokle.local'); diff --git a/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts b/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts index 668a7a93e..b493d0bbc 100644 --- a/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts +++ b/packages/synchronizer/src/createDefaultMonokleAuthenticator.ts @@ -6,7 +6,7 @@ import {Authenticator} from './utils/authenticator.js'; /** * Creates default Monokle Authenticator instance. * - * @deprecated Use createMonokleAuthenticatorFromOrigin instead which does not rely on hardcoded config. + * @deprecated Use createMonokleAuthenticatorFromOrigin or createMonokleAuthenticatorFromConfig instead which does not rely on hardcoded config. * * @param storageHandler * @param apiHandler diff --git a/packages/synchronizer/src/createDefaultMonokleFetcher.ts b/packages/synchronizer/src/createDefaultMonokleFetcher.ts index 97c33ff5d..eb02f6a6f 100644 --- a/packages/synchronizer/src/createDefaultMonokleFetcher.ts +++ b/packages/synchronizer/src/createDefaultMonokleFetcher.ts @@ -1,6 +1,14 @@ import {ApiHandler} from './handlers/apiHandler.js'; import {Fetcher} from './utils/fetcher.js'; +/** + * Creates default Monokle Fetcher instance. + * + * @deprecated Use createMonokleFetcherFromOrigin or createMonokleFetcherFromConfig instead which does not rely on hardcoded config. + * + * @param apiHandler + * @returns Fetcher instance + */ export function createDefaultMonokleFetcher(apiHandler: ApiHandler = new ApiHandler()) { return new Fetcher(apiHandler); } diff --git a/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts b/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts index 59b96d3ae..e9f3ce401 100644 --- a/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts +++ b/packages/synchronizer/src/createDefaultMonokleSynchronizer.ts @@ -6,7 +6,7 @@ import {Synchronizer} from './utils/synchronizer.js'; /** * Creates default Monokle Synchronizer instance. * - * @deprecated Use createMonokleSynchronizerFromOrigin instead which does not rely on hardcoded config. + * @deprecated Use createMonokleSynchronizerFromOrigin or createMonokleSynchronizerFromConfig instead which does not rely on hardcoded config. * * @param storageHandler * @param apiHandler diff --git a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts index f98b5269b..f43d335fe 100644 --- a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts +++ b/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts @@ -1,36 +1,48 @@ import {ApiHandler} from './handlers/apiHandler.js'; import {DeviceFlowHandler} from './handlers/deviceFlowHandler.js'; import {StorageHandlerAuth} from './handlers/storageHandlerAuth.js'; -import {Fetcher} from './utils/fetcher.js'; import {Authenticator} from './utils/authenticator.js'; import {DEFAULT_DEVICE_FLOW_ALG, DEFAULT_DEVICE_FLOW_CLIENT_SECRET, DEFAULT_ORIGIN} from './constants.js'; +import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js'; -export async function createMonokleAuthenticatorFromOrigin(authClientId: string, origin: string = DEFAULT_ORIGIN) { +export async function createMonokleAuthenticatorFromOrigin( + authClientId: string, + origin: string = DEFAULT_ORIGIN, + storageHandler: StorageHandlerAuth = new StorageHandlerAuth() +) { try { - const originConfig = await Fetcher.getOriginConfig(origin); + const originConfig = await fetchOriginConfig(origin); - if (!authClientId) { - throw new Error(`No auth clientId provided.`); - } + return createMonokleAuthenticatorFromConfig(authClientId, originConfig, storageHandler); + } catch (err: any) { + throw err; + } +} - if (!originConfig?.apiOrigin) { - throw new Error(`No api origin found in origin config from ${origin}.`); - } +export function createMonokleAuthenticatorFromConfig( + authClientId: string, + config: OriginConfig, + storageHandler: StorageHandlerAuth = new StorageHandlerAuth() +) { + if (!authClientId) { + throw new Error(`No auth clientId provided.`); + } - if (!originConfig?.authOrigin) { - throw new Error(`No auth origin found in origin config from ${origin}.`); - } + if (!config?.apiOrigin) { + throw new Error(`No api origin found in origin config from ${origin}.`); + } - return new Authenticator( - new StorageHandlerAuth(), - new ApiHandler(originConfig.apiOrigin), - new DeviceFlowHandler(originConfig.authOrigin, { - client_id: authClientId, - client_secret: DEFAULT_DEVICE_FLOW_CLIENT_SECRET, - id_token_signed_response_alg: DEFAULT_DEVICE_FLOW_ALG, - }) - ); - } catch (err: any) { - throw err; + if (!config?.authOrigin) { + throw new Error(`No auth origin found in origin config from ${origin}.`); } + + return new Authenticator( + storageHandler, + new ApiHandler(config), + new DeviceFlowHandler(config.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/createMonokleFetcherFromOrigin.ts b/packages/synchronizer/src/createMonokleFetcherFromOrigin.ts new file mode 100644 index 000000000..1ec800ee5 --- /dev/null +++ b/packages/synchronizer/src/createMonokleFetcherFromOrigin.ts @@ -0,0 +1,22 @@ +import {DEFAULT_ORIGIN} from './constants.js'; +import {ApiHandler} 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) { + try { + const originConfig = await fetchOriginConfig(origin); + + return createMonokleFetcherFromConfig(originConfig); + } catch (err: any) { + throw err; + } +} + +export function createMonokleFetcherFromConfig(config: OriginConfig) { + if (!config?.apiOrigin) { + throw new Error(`No api origin found in origin config from ${origin}.`); + } + + return new Fetcher(new ApiHandler(config)); +} diff --git a/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts b/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts index 7190e365d..59d5ad067 100644 --- a/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts +++ b/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts @@ -1,20 +1,32 @@ import {DEFAULT_ORIGIN} from './constants.js'; import {ApiHandler} from './handlers/apiHandler.js'; +import {OriginConfig, fetchOriginConfig} from './handlers/configHandler.js'; import {GitHandler} from './handlers/gitHandler.js'; import {StorageHandlerPolicy} from './handlers/storageHandlerPolicy.js'; -import {Fetcher} from './utils/fetcher.js'; import {Synchronizer} from './utils/synchronizer.js'; -export async function createMonokleSynchronizerFromOrigin(origin: string = DEFAULT_ORIGIN) { +export async function createMonokleSynchronizerFromOrigin( + origin: string = DEFAULT_ORIGIN, + storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(), + gitHandler: GitHandler = new GitHandler() +) { try { - const originConfig = await Fetcher.getOriginConfig(origin); + const originConfig = await fetchOriginConfig(origin); - if (!originConfig?.apiOrigin) { - throw new Error(`No api origin found in origin config from ${origin}.`); - } - - return new Synchronizer(new StorageHandlerPolicy(), new ApiHandler(originConfig.apiOrigin), new GitHandler()); + return createMonokleSynchronizerFromConfig(originConfig, storageHandler, gitHandler); } catch (err: any) { throw err; } } + +export function createMonokleSynchronizerFromConfig( + config: OriginConfig, + storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(), + gitHandler: GitHandler = new GitHandler() +) { + if (!config?.apiOrigin) { + throw new Error(`No api origin found in origin config from ${origin}.`); + } + + return new Synchronizer(storageHandler, new ApiHandler(config), gitHandler); +} diff --git a/packages/synchronizer/src/handlers/apiHandler.ts b/packages/synchronizer/src/handlers/apiHandler.ts index 3c57170a3..42a9060f4 100644 --- a/packages/synchronizer/src/handlers/apiHandler.ts +++ b/packages/synchronizer/src/handlers/apiHandler.ts @@ -2,8 +2,8 @@ import normalizeUrl from 'normalize-url'; import fetch from 'node-fetch'; import {SuppressionStatus} from '@monokle/types'; import {DEFAULT_API_URL} from '../constants.js'; +import {OriginConfig} from '../handlers/configHandler.js'; import type {TokenInfo} from './storageHandlerAuth.js'; -import {OriginConfig} from '../utils/fetcher.js'; const getUserQuery = ` query getUser { diff --git a/packages/synchronizer/src/handlers/configHandler.ts b/packages/synchronizer/src/handlers/configHandler.ts new file mode 100644 index 000000000..8f3bfd79d --- /dev/null +++ b/packages/synchronizer/src/handlers/configHandler.ts @@ -0,0 +1,59 @@ +import normalizeUrl from 'normalize-url'; +import fetch from 'node-fetch'; + +export type OriginConfig = { + origin: string; + apiOrigin: string; + authOrigin: string; + [key: string]: string; +}; + +export type CachedOriginConfig = { + config: OriginConfig; + downloadedAt: number; + origin: string; +}; + +let originConfigCache: CachedOriginConfig | undefined = undefined; + +export async function fetchOriginConfig(origin: string) { + if (originConfigCache) { + // Use recently fetched config if from same origin and it's less than 5 minutes old. + if (origin === originConfigCache.origin && Date.now() - originConfigCache.downloadedAt < 1000 * 60 * 5) { + return originConfigCache.config; + } + } + + try { + const configUrl = normalizeUrl(`${origin}/config.js`); + const response = await fetch(configUrl); + const responseText = await response.text(); + + const values = Array.from(responseText.matchAll(/([A-Z_]+)\s*:\s*"(.*?)"/gm)).reduce( + (acc: Record, match) => { + if (match[1] && match[2]) { + acc[match[1]] = match[2]; + } + return acc; + }, + {} + ); + + if (values) { + values.origin = normalizeUrl(origin); + values.apiOrigin = values.API_ORIGIN; + values.authOrigin = values.OIDC_DISCOVERY_URL; + } + + originConfigCache = { + config: values as OriginConfig, + downloadedAt: Date.now(), + origin, + }; + + return values as OriginConfig; + } catch (error: any) { + // Rethrow error so integrations can catch it and propagate/react. + throw error; + } +} diff --git a/packages/synchronizer/src/index.ts b/packages/synchronizer/src/index.ts index b7998f5ba..95e1ca35d 100644 --- a/packages/synchronizer/src/index.ts +++ b/packages/synchronizer/src/index.ts @@ -1,4 +1,5 @@ export * from './handlers/apiHandler.js'; +export * from './handlers/configHandler.js'; export * from './handlers/deviceFlowHandler.js'; export * from './handlers/gitHandler.js'; export * from './handlers/storageHandler.js'; @@ -18,4 +19,5 @@ export * from './createDefaultMonokleFetcher.js'; export * from './createDefaultMonokleSynchronizer.js'; export * from './createMonokleAuthenticatorFromOrigin.js'; +export * from './createMonokleFetcherFromOrigin.js'; export * from './createMonokleSynchronizerFromOrigin.js'; diff --git a/packages/synchronizer/src/utils/fetcher.ts b/packages/synchronizer/src/utils/fetcher.ts index 958739aa6..e674a5a6a 100644 --- a/packages/synchronizer/src/utils/fetcher.ts +++ b/packages/synchronizer/src/utils/fetcher.ts @@ -1,5 +1,3 @@ -import normalizeUrl from 'normalize-url'; -import fetch from 'node-fetch'; import {EventEmitter} from 'events'; import {ApiHandler} from '../handlers/apiHandler.js'; import {TokenInfo} from '../handlers/storageHandlerAuth.js'; @@ -15,70 +13,13 @@ export type QueryResult = { error?: string; }; -export type OriginConfig = { - origin: string; - apiOrigin: string; - authOrigin: string; - [key: string]: string; -}; - -export type CachedOriginConfig = { - config: OriginConfig; - downloadedAt: number; - origin: string; -}; - export class Fetcher extends EventEmitter { - static _originConfig: CachedOriginConfig | undefined; - private _token: TokenInfo | undefined; constructor(private _apiHandler: ApiHandler) { super(); } - static async getOriginConfig(origin: string): Promise { - if (Fetcher._originConfig) { - // Use recently fetched config if from same origin and it's less than 5 minutes old. - if (origin === Fetcher._originConfig.origin && (Date.now()) - Fetcher._originConfig.downloadedAt < 1000 * 60 * 5) { - return Fetcher._originConfig.config; - } - } - - try { - const configUrl = normalizeUrl(`${origin}/config.js`); - const response = await fetch(configUrl); - const responseText = await response.text(); - - const values = Array.from(responseText.matchAll(/([A-Z_]+)\s*:\s*"(.*?)"/gm)).reduce( - (acc: Record, match) => { - if (match[1] && match[2]) { - acc[match[1]] = match[2]; - } - return acc; - }, - {} - ); - - if (values) { - values.origin = normalizeUrl(origin); - values.apiOrigin = values.API_ORIGIN; - values.authOrigin = values.OIDC_DISCOVERY_URL; - } - - Fetcher._originConfig = { - config: values as OriginConfig, - downloadedAt: Date.now(), - origin, - }; - - return values as OriginConfig; - } catch (error: any) { - // Rethrow error so integrations can catch it and propagate/react. - throw error; - } - } - useBearerToken(token: string) { this._token = { accessToken: token, From 75cc7d53591f4498033bd8b5b78bb22ccfe53af3 Mon Sep 17 00:00:00 2001 From: f1ames Date: Fri, 17 Nov 2023 15:52:32 +0100 Subject: [PATCH 13/13] chore: rename creator files --- ...enticatorFromOrigin.ts => createMonokleAuthenticator.ts} | 0 ...eMonokleFetcherFromOrigin.ts => createMonokleFetcher.ts} | 0 ...nchronizerFromOrigin.ts => createMonokleSynchronizer.ts} | 0 packages/synchronizer/src/index.ts | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/synchronizer/src/{createMonokleAuthenticatorFromOrigin.ts => createMonokleAuthenticator.ts} (100%) rename packages/synchronizer/src/{createMonokleFetcherFromOrigin.ts => createMonokleFetcher.ts} (100%) rename packages/synchronizer/src/{createMonokleSynchronizerFromOrigin.ts => createMonokleSynchronizer.ts} (100%) diff --git a/packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts b/packages/synchronizer/src/createMonokleAuthenticator.ts similarity index 100% rename from packages/synchronizer/src/createMonokleAuthenticatorFromOrigin.ts rename to packages/synchronizer/src/createMonokleAuthenticator.ts diff --git a/packages/synchronizer/src/createMonokleFetcherFromOrigin.ts b/packages/synchronizer/src/createMonokleFetcher.ts similarity index 100% rename from packages/synchronizer/src/createMonokleFetcherFromOrigin.ts rename to packages/synchronizer/src/createMonokleFetcher.ts diff --git a/packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts b/packages/synchronizer/src/createMonokleSynchronizer.ts similarity index 100% rename from packages/synchronizer/src/createMonokleSynchronizerFromOrigin.ts rename to packages/synchronizer/src/createMonokleSynchronizer.ts diff --git a/packages/synchronizer/src/index.ts b/packages/synchronizer/src/index.ts index 95e1ca35d..36a7736a2 100644 --- a/packages/synchronizer/src/index.ts +++ b/packages/synchronizer/src/index.ts @@ -18,6 +18,6 @@ export * from './createDefaultMonokleAuthenticator.js'; export * from './createDefaultMonokleFetcher.js'; export * from './createDefaultMonokleSynchronizer.js'; -export * from './createMonokleAuthenticatorFromOrigin.js'; -export * from './createMonokleFetcherFromOrigin.js'; -export * from './createMonokleSynchronizerFromOrigin.js'; +export * from './createMonokleAuthenticator.js'; +export * from './createMonokleFetcher.js'; +export * from './createMonokleSynchronizer.js';