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 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 1241e3424..9eea2258f 100644 --- a/packages/synchronizer/src/__tests__/apiHandler.spec.ts +++ b/packages/synchronizer/src/__tests__/apiHandler.spec.ts @@ -45,23 +45,62 @@ describe('ApiHandler Tests', () => { }); describe('Api Url', () => { - it('uses default Api Url by default', async () => { - assert.equal('https://api.monokle.com', (new ApiHandler()).apiUrl); + 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 () => { - 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); + 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); }); - it('uses passed Api Url', async () => { - 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 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', + }); + 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', + }); + + 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', + }); + 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..178f118b3 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 {fetchOriginConfig} from '../handlers/configHandler.js'; const TEST_QUERY = ` query getCluster($id: ID) { @@ -117,4 +119,42 @@ 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 fetchOriginConfig('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'); + + res(); + } catch (err) { + rej(err); + } finally { + server.close(); + } + }); + }); + }); + }); }); 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..b493d0bbc 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 or createMonokleAuthenticatorFromConfig 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/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 cfa8f2b9e..e9f3ce401 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 or createMonokleSynchronizerFromConfig 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/createMonokleAuthenticator.ts b/packages/synchronizer/src/createMonokleAuthenticator.ts new file mode 100644 index 000000000..f43d335fe --- /dev/null +++ b/packages/synchronizer/src/createMonokleAuthenticator.ts @@ -0,0 +1,48 @@ +import {ApiHandler} from './handlers/apiHandler.js'; +import {DeviceFlowHandler} from './handlers/deviceFlowHandler.js'; +import {StorageHandlerAuth} from './handlers/storageHandlerAuth.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, + storageHandler: StorageHandlerAuth = new StorageHandlerAuth() +) { + try { + const originConfig = await fetchOriginConfig(origin); + + return createMonokleAuthenticatorFromConfig(authClientId, originConfig, storageHandler); + } catch (err: any) { + throw err; + } +} + +export function createMonokleAuthenticatorFromConfig( + authClientId: string, + config: OriginConfig, + storageHandler: StorageHandlerAuth = new StorageHandlerAuth() +) { + if (!authClientId) { + throw new Error(`No auth clientId provided.`); + } + + if (!config?.apiOrigin) { + throw new Error(`No api origin found in origin config from ${origin}.`); + } + + 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/createMonokleFetcher.ts b/packages/synchronizer/src/createMonokleFetcher.ts new file mode 100644 index 000000000..1ec800ee5 --- /dev/null +++ b/packages/synchronizer/src/createMonokleFetcher.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/createMonokleSynchronizer.ts b/packages/synchronizer/src/createMonokleSynchronizer.ts new file mode 100644 index 000000000..59d5ad067 --- /dev/null +++ b/packages/synchronizer/src/createMonokleSynchronizer.ts @@ -0,0 +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 {Synchronizer} from './utils/synchronizer.js'; + +export async function createMonokleSynchronizerFromOrigin( + origin: string = DEFAULT_ORIGIN, + storageHandler: StorageHandlerPolicy = new StorageHandlerPolicy(), + gitHandler: GitHandler = new GitHandler() +) { + try { + const originConfig = await fetchOriginConfig(origin); + + 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 4c3c0e3c6..42a9060f4 100644 --- a/packages/synchronizer/src/handlers/apiHandler.ts +++ b/packages/synchronizer/src/handlers/apiHandler.ts @@ -2,6 +2,7 @@ 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'; const getUserQuery = ` @@ -169,15 +170,34 @@ export type ApiRepoIdData = { data: { getProject: { repository: { - id: string - } - } - } + id: string; + }; + }; + }; }; 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; } } @@ -202,20 +222,29 @@ 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}); } 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/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/handlers/storageHandler.ts b/packages/synchronizer/src/handlers/storageHandler.ts index f828e5f47..7ebc9b520 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.js'; 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); } diff --git a/packages/synchronizer/src/index.ts b/packages/synchronizer/src/index.ts index 07315d591..36a7736a2 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'; @@ -16,3 +17,7 @@ export * from './constants.js'; export * from './createDefaultMonokleAuthenticator.js'; export * from './createDefaultMonokleFetcher.js'; export * from './createDefaultMonokleSynchronizer.js'; + +export * from './createMonokleAuthenticator.js'; +export * from './createMonokleFetcher.js'; +export * from './createMonokleSynchronizer.js'; 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; } }