diff --git a/.github/workflows/check-documentation-urls.yml b/.github/workflows/check-documentation-urls.yml index 6e352d2016..8dca30d389 100644 --- a/.github/workflows/check-documentation-urls.yml +++ b/.github/workflows/check-documentation-urls.yml @@ -27,7 +27,7 @@ jobs: run: pnpm install - name: Build nodes-base - run: pnpm --filter n8n-workflow --filter=n8n-core --filter=n8n-nodes-base build + run: pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base build - name: Test URLS run: node scripts/validate-docs-links.js diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index f7a0f8f21a..53ea34eb91 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -38,7 +38,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - files: packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml + files: packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml - name: Lint env: diff --git a/.github/workflows/ci-postgres-mysql.yml b/.github/workflows/ci-postgres-mysql.yml index adf381395b..bbec701552 100644 --- a/.github/workflows/ci-postgres-mysql.yml +++ b/.github/workflows/ci-postgres-mysql.yml @@ -34,7 +34,7 @@ jobs: compose-file: ./.github/docker-compose.yml - name: Build Core, Workflow, and CLI - run: pnpm --filter n8n-workflow --filter=n8n-core --filter=n8n build + run: pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n build - name: Test MySQL working-directory: packages/cli diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index e6d4cdabd7..01e913b010 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -70,7 +70,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - files: packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml + files: packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml lint: name: Lint changes diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml index f6fc73083c..6f714450b6 100644 --- a/.github/workflows/test-workflows.yml +++ b/.github/workflows/test-workflows.yml @@ -49,7 +49,7 @@ jobs: working-directory: n8n run: | pnpm install - pnpm build + pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base --filter n8n build shell: bash - name: Import credentials diff --git a/packages/@n8n/client-oauth2/.eslintrc.js b/packages/@n8n/client-oauth2/.eslintrc.js new file mode 100644 index 0000000000..fe7469cf3c --- /dev/null +++ b/packages/@n8n/client-oauth2/.eslintrc.js @@ -0,0 +1,14 @@ +const { sharedOptions } = require('@n8n_io/eslint-config/shared'); + +/** + * @type {import('@types/eslint').ESLint.ConfigData} + */ +module.exports = { + extends: ['@n8n_io/eslint-config/base'], + + ...sharedOptions(__dirname), + + rules: { + '@typescript-eslint/consistent-type-imports': 'error', + }, +}; diff --git a/packages/@n8n/client-oauth2/jest.config.js b/packages/@n8n/client-oauth2/jest.config.js new file mode 100644 index 0000000000..d6c48554a7 --- /dev/null +++ b/packages/@n8n/client-oauth2/jest.config.js @@ -0,0 +1,2 @@ +/** @type {import('jest').Config} */ +module.exports = require('../../../jest.config'); diff --git a/packages/@n8n/client-oauth2/package.json b/packages/@n8n/client-oauth2/package.json new file mode 100644 index 0000000000..6acda89b76 --- /dev/null +++ b/packages/@n8n/client-oauth2/package.json @@ -0,0 +1,25 @@ +{ + "name": "@n8n/client-oauth2", + "version": "0.1.0", + "scripts": { + "clean": "rimraf dist .turbo", + "dev": "pnpm watch", + "typecheck": "tsc", + "build": "tsc -p tsconfig.build.json", + "format": "prettier --write . --ignore-path ../../../.prettierignore", + "lint": "eslint --quiet .", + "lintfix": "eslint . --fix", + "watch": "tsc -p tsconfig.build.json --watch", + "test": "jest", + "test:dev": "jest --watch" + }, + "main": "dist/index.js", + "module": "src/index.ts", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "dependencies": { + "axios": "^0.21.1" + } +} diff --git a/packages/@n8n/client-oauth2/src/ClientOAuth2.ts b/packages/@n8n/client-oauth2/src/ClientOAuth2.ts new file mode 100644 index 0000000000..288b91e4ca --- /dev/null +++ b/packages/@n8n/client-oauth2/src/ClientOAuth2.ts @@ -0,0 +1,112 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as qs from 'querystring'; +import axios from 'axios'; +import { getAuthError } from './utils'; +import type { ClientOAuth2TokenData } from './ClientOAuth2Token'; +import { ClientOAuth2Token } from './ClientOAuth2Token'; +import { CodeFlow } from './CodeFlow'; +import { CredentialsFlow } from './CredentialsFlow'; +import type { Headers, Query } from './types'; + +export interface ClientOAuth2RequestObject { + url: string; + method: 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT'; + body?: Record; + query?: Query; + headers?: Headers; +} + +export interface ClientOAuth2Options { + clientId: string; + clientSecret: string; + accessTokenUri: string; + authorizationUri?: string; + redirectUri?: string; + scopes?: string[]; + authorizationGrants?: string[]; + state?: string; + body?: Record; + query?: Query; + headers?: Headers; +} + +class ResponseError extends Error { + constructor(readonly status: number, readonly body: object, readonly code = 'ESTATUS') { + super(`HTTP status ${status}`); + } +} + +/** + * Construct an object that can handle the multiple OAuth 2.0 flows. + */ +export class ClientOAuth2 { + code: CodeFlow; + + credentials: CredentialsFlow; + + constructor(readonly options: ClientOAuth2Options) { + this.code = new CodeFlow(this); + this.credentials = new CredentialsFlow(this); + } + + /** + * Create a new token from existing data. + */ + createToken(data: ClientOAuth2TokenData, type?: string): ClientOAuth2Token { + return new ClientOAuth2Token(this, { + ...data, + ...(typeof type === 'string' ? { token_type: type } : type), + }); + } + + /** + * Attempt to parse response body as JSON, fall back to parsing as a query string. + */ + private parseResponseBody(body: string): T { + try { + return JSON.parse(body); + } catch (e) { + return qs.parse(body) as T; + } + } + + /** + * Using the built-in request method, we'll automatically attempt to parse + * the response. + */ + async request(options: ClientOAuth2RequestObject): Promise { + let url = options.url; + const query = qs.stringify(options.query); + + if (query) { + url += (url.indexOf('?') === -1 ? '?' : '&') + query; + } + + const response = await axios.request({ + url, + method: options.method, + data: qs.stringify(options.body), + headers: options.headers, + transformResponse: (res) => res, + // Axios rejects the promise by default for all status codes 4xx. + // We override this to reject promises only on 5xxs + validateStatus: (status) => status < 500, + }); + + const body = this.parseResponseBody(response.data); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const authErr = getAuthError(body); + if (authErr) throw authErr; + + if (response.status < 200 || response.status >= 399) + throw new ResponseError(response.status, response.data); + + return body; + } +} diff --git a/packages/@n8n/client-oauth2/src/ClientOAuth2Token.ts b/packages/@n8n/client-oauth2/src/ClientOAuth2Token.ts new file mode 100644 index 0000000000..5c749a9009 --- /dev/null +++ b/packages/@n8n/client-oauth2/src/ClientOAuth2Token.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { ClientOAuth2, ClientOAuth2Options, ClientOAuth2RequestObject } from './ClientOAuth2'; +import { auth, getRequestOptions } from './utils'; +import { DEFAULT_HEADERS } from './constants'; + +export interface ClientOAuth2TokenData extends Record { + token_type?: string | undefined; + access_token: string; + refresh_token: string; + expires_in?: string; + scope?: string | undefined; +} +/** + * General purpose client token generator. + */ +export class ClientOAuth2Token { + readonly tokenType?: string; + + readonly accessToken: string; + + readonly refreshToken: string; + + private expires: Date; + + constructor(readonly client: ClientOAuth2, readonly data: ClientOAuth2TokenData) { + this.tokenType = data.token_type?.toLowerCase() ?? 'bearer'; + this.accessToken = data.access_token; + this.refreshToken = data.refresh_token; + + this.expires = new Date(); + this.expires.setSeconds(this.expires.getSeconds() + Number(data.expires_in)); + } + + /** + * Sign a standardized request object with user authentication information. + */ + sign(requestObject: ClientOAuth2RequestObject): ClientOAuth2RequestObject { + if (!this.accessToken) { + throw new Error('Unable to sign without access token'); + } + + requestObject.headers = requestObject.headers ?? {}; + + if (this.tokenType === 'bearer') { + requestObject.headers.Authorization = 'Bearer ' + this.accessToken; + } else { + const parts = requestObject.url.split('#'); + const token = 'access_token=' + this.accessToken; + const url = parts[0].replace(/[?&]access_token=[^&#]/, ''); + const fragment = parts[1] ? '#' + parts[1] : ''; + + // Prepend the correct query string parameter to the url. + requestObject.url = url + (url.indexOf('?') > -1 ? '&' : '?') + token + fragment; + + // Attempt to avoid storing the url in proxies, since the access token + // is exposed in the query parameters. + requestObject.headers.Pragma = 'no-store'; + requestObject.headers['Cache-Control'] = 'no-store'; + } + + return requestObject; + } + + /** + * Refresh a user access token with the supplied token. + */ + async refresh(opts?: ClientOAuth2Options): Promise { + const options = { ...this.client.options, ...opts }; + + if (!this.refreshToken) throw new Error('No refresh token'); + + const requestOptions = getRequestOptions( + { + url: options.accessTokenUri, + method: 'POST', + headers: { + ...DEFAULT_HEADERS, + Authorization: auth(options.clientId, options.clientSecret), + }, + body: { + refresh_token: this.refreshToken, + grant_type: 'refresh_token', + }, + }, + options, + ); + + const responseData = await this.client.request(requestOptions); + return this.client.createToken({ ...this.data, ...responseData }); + } + + /** + * Check whether the token has expired. + */ + expired(): boolean { + return Date.now() > this.expires.getTime(); + } +} diff --git a/packages/@n8n/client-oauth2/src/CodeFlow.ts b/packages/@n8n/client-oauth2/src/CodeFlow.ts new file mode 100644 index 0000000000..f25feb574c --- /dev/null +++ b/packages/@n8n/client-oauth2/src/CodeFlow.ts @@ -0,0 +1,121 @@ +import * as qs from 'querystring'; +import type { ClientOAuth2, ClientOAuth2Options } from './ClientOAuth2'; +import type { ClientOAuth2Token, ClientOAuth2TokenData } from './ClientOAuth2Token'; +import { DEFAULT_HEADERS, DEFAULT_URL_BASE } from './constants'; +import { auth, expects, getAuthError, getRequestOptions, sanitizeScope } from './utils'; + +interface CodeFlowBody { + code: string | string[]; + grant_type: 'authorization_code'; + redirect_uri?: string; + client_id?: string; +} + +/** + * Support authorization code OAuth 2.0 grant. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.1 + */ +export class CodeFlow { + constructor(private client: ClientOAuth2) {} + + /** + * Generate the uri for doing the first redirect. + */ + getUri(opts?: ClientOAuth2Options): string { + const options = { ...this.client.options, ...opts }; + + // Check the required parameters are set. + expects(options, 'clientId', 'authorizationUri'); + + const query: Record = { + client_id: options.clientId, + redirect_uri: options.redirectUri, + response_type: 'code', + state: options.state, + }; + if (options.scopes !== undefined) { + query.scope = sanitizeScope(options.scopes); + } + + if (options.authorizationUri) { + const sep = options.authorizationUri.includes('?') ? '&' : '?'; + return options.authorizationUri + sep + qs.stringify({ ...query, ...options.query }); + } + throw new TypeError('Missing authorization uri, unable to get redirect uri'); + } + + /** + * Get the code token from the redirected uri and make another request for + * the user access token. + */ + async getToken( + uri: string | URL, + opts?: Partial, + ): Promise { + const options = { ...this.client.options, ...opts }; + + expects(options, 'clientId', 'accessTokenUri'); + + const url = uri instanceof URL ? uri : new URL(uri, DEFAULT_URL_BASE); + if ( + typeof options.redirectUri === 'string' && + typeof url.pathname === 'string' && + url.pathname !== new URL(options.redirectUri, DEFAULT_URL_BASE).pathname + ) { + throw new TypeError('Redirected path should match configured path, but got: ' + url.pathname); + } + + if (!url.search?.substring(1)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new TypeError(`Unable to process uri: ${uri.toString()}`); + } + + const data = + typeof url.search === 'string' ? qs.parse(url.search.substring(1)) : url.search || {}; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const error = getAuthError(data); + if (error) throw error; + + if (options.state && data.state !== options.state) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new TypeError(`Invalid state: ${data.state}`); + } + + // Check whether the response code is set. + if (!data.code) { + throw new TypeError('Missing code, unable to request token'); + } + + const headers = { ...DEFAULT_HEADERS }; + const body: CodeFlowBody = { + code: data.code, + grant_type: 'authorization_code', + redirect_uri: options.redirectUri, + }; + + // `client_id`: REQUIRED, if the client is not authenticating with the + // authorization server as described in Section 3.2.1. + // Reference: https://tools.ietf.org/html/rfc6749#section-3.2.1 + if (options.clientSecret) { + headers.Authorization = auth(options.clientId, options.clientSecret); + } else { + body.client_id = options.clientId; + } + + const requestOptions = getRequestOptions( + { + url: options.accessTokenUri, + method: 'POST', + headers, + body, + }, + options, + ); + + const responseData = await this.client.request(requestOptions); + return this.client.createToken(responseData); + } +} diff --git a/packages/@n8n/client-oauth2/src/CredentialsFlow.ts b/packages/@n8n/client-oauth2/src/CredentialsFlow.ts new file mode 100644 index 0000000000..3bc7c2ac3a --- /dev/null +++ b/packages/@n8n/client-oauth2/src/CredentialsFlow.ts @@ -0,0 +1,52 @@ +import type { ClientOAuth2, ClientOAuth2Options } from './ClientOAuth2'; +import type { ClientOAuth2Token, ClientOAuth2TokenData } from './ClientOAuth2Token'; +import { DEFAULT_HEADERS } from './constants'; +import { auth, expects, getRequestOptions, sanitizeScope } from './utils'; + +interface CredentialsFlowBody { + grant_type: 'client_credentials'; + scope?: string; +} + +/** + * Support client credentials OAuth 2.0 grant. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.4 + */ +export class CredentialsFlow { + constructor(private client: ClientOAuth2) {} + + /** + * Request an access token using the client credentials. + */ + async getToken(opts?: Partial): Promise { + const options = { ...this.client.options, ...opts }; + + expects(options, 'clientId', 'clientSecret', 'accessTokenUri'); + + const body: CredentialsFlowBody = { + grant_type: 'client_credentials', + }; + + if (options.scopes !== undefined) { + body.scope = sanitizeScope(options.scopes); + } + + const requestOptions = getRequestOptions( + { + url: options.accessTokenUri, + method: 'POST', + headers: { + ...DEFAULT_HEADERS, + // eslint-disable-next-line @typescript-eslint/naming-convention + Authorization: auth(options.clientId, options.clientSecret), + }, + body, + }, + options, + ); + + const responseData = await this.client.request(requestOptions); + return this.client.createToken(responseData); + } +} diff --git a/packages/@n8n/client-oauth2/src/constants.ts b/packages/@n8n/client-oauth2/src/constants.ts new file mode 100644 index 0000000000..e4895aa470 --- /dev/null +++ b/packages/@n8n/client-oauth2/src/constants.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { Headers } from './types'; + +export const DEFAULT_URL_BASE = 'https://example.org/'; + +/** + * Default headers for executing OAuth 2.0 flows. + */ +export const DEFAULT_HEADERS: Headers = { + Accept: 'application/json, application/x-www-form-urlencoded', + 'Content-Type': 'application/x-www-form-urlencoded', +}; + +/** + * Format error response types to regular strings for displaying to clients. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ +export const ERROR_RESPONSES: Record = { + invalid_request: [ + 'The request is missing a required parameter, includes an', + 'invalid parameter value, includes a parameter more than', + 'once, or is otherwise malformed.', + ].join(' '), + invalid_client: [ + 'Client authentication failed (e.g., unknown client, no', + 'client authentication included, or unsupported', + 'authentication method).', + ].join(' '), + invalid_grant: [ + 'The provided authorization grant (e.g., authorization', + 'code, resource owner credentials) or refresh token is', + 'invalid, expired, revoked, does not match the redirection', + 'URI used in the authorization request, or was issued to', + 'another client.', + ].join(' '), + unauthorized_client: [ + 'The client is not authorized to request an authorization', + 'code using this method.', + ].join(' '), + unsupported_grant_type: [ + 'The authorization grant type is not supported by the', + 'authorization server.', + ].join(' '), + access_denied: ['The resource owner or authorization server denied the request.'].join(' '), + unsupported_response_type: [ + 'The authorization server does not support obtaining', + 'an authorization code using this method.', + ].join(' '), + invalid_scope: ['The requested scope is invalid, unknown, or malformed.'].join(' '), + server_error: [ + 'The authorization server encountered an unexpected', + 'condition that prevented it from fulfilling the request.', + '(This error code is needed because a 500 Internal Server', + 'Error HTTP status code cannot be returned to the client', + 'via an HTTP redirect.)', + ].join(' '), + temporarily_unavailable: [ + 'The authorization server is currently unable to handle', + 'the request due to a temporary overloading or maintenance', + 'of the server.', + ].join(' '), +}; diff --git a/packages/@n8n/client-oauth2/src/index.ts b/packages/@n8n/client-oauth2/src/index.ts new file mode 100644 index 0000000000..376c10f1ee --- /dev/null +++ b/packages/@n8n/client-oauth2/src/index.ts @@ -0,0 +1,2 @@ +export { ClientOAuth2, ClientOAuth2Options, ClientOAuth2RequestObject } from './ClientOAuth2'; +export { ClientOAuth2Token, ClientOAuth2TokenData } from './ClientOAuth2Token'; diff --git a/packages/@n8n/client-oauth2/src/types.ts b/packages/@n8n/client-oauth2/src/types.ts new file mode 100644 index 0000000000..906efcc43e --- /dev/null +++ b/packages/@n8n/client-oauth2/src/types.ts @@ -0,0 +1,2 @@ +export type Headers = Record; +export type Query = Record; diff --git a/packages/@n8n/client-oauth2/src/utils.ts b/packages/@n8n/client-oauth2/src/utils.ts new file mode 100644 index 0000000000..e3433e5cf1 --- /dev/null +++ b/packages/@n8n/client-oauth2/src/utils.ts @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ClientOAuth2RequestObject } from './ClientOAuth2'; +import { ERROR_RESPONSES } from './constants'; + +/** + * Check if properties exist on an object and throw when they aren't. + */ +export function expects(obj: any, ...args: any[]) { + for (let i = 1; i < args.length; i++) { + const prop = args[i]; + if (obj[prop] === null) { + throw new TypeError('Expected "' + prop + '" to exist'); + } + } +} + +export class AuthError extends Error { + constructor(message: string, readonly body: any, readonly code = 'EAUTH') { + super(message); + } +} + +/** + * Pull an authentication error from the response data. + */ +export function getAuthError(body: { + error: string; + error_description?: string; +}): Error | undefined { + const message: string | undefined = + ERROR_RESPONSES[body.error] ?? body.error_description ?? body.error; + + if (message) { + return new AuthError(message, body); + } + + return undefined; +} + +/** + * Ensure a value is a string. + */ +function toString(str: string | null | undefined) { + return str === null ? '' : String(str); +} + +/** + * Sanitize the scopes option to be a string. + */ +export function sanitizeScope(scopes: string[] | string): string { + return Array.isArray(scopes) ? scopes.join(' ') : toString(scopes); +} + +/** + * Create basic auth header. + */ +export function auth(username: string, password: string): string { + return 'Basic ' + Buffer.from(toString(username) + ':' + toString(password)).toString('base64'); +} + +/** + * Merge request options from an options object. + */ +export function getRequestOptions( + { url, method, body, query, headers }: ClientOAuth2RequestObject, + options: any, +): ClientOAuth2RequestObject { + const rOptions = { + url, + method, + body: { ...body, ...options.body }, + query: { ...query, ...options.query }, + headers: { ...headers, ...options.headers }, + }; + // if request authorization was overridden delete it from header + if (rOptions.headers.Authorization === '') { + delete rOptions.headers.Authorization; + } + return rOptions; +} diff --git a/packages/@n8n/client-oauth2/test/CodeFlow.test.ts b/packages/@n8n/client-oauth2/test/CodeFlow.test.ts new file mode 100644 index 0000000000..7b62e5ba14 --- /dev/null +++ b/packages/@n8n/client-oauth2/test/CodeFlow.test.ts @@ -0,0 +1,189 @@ +import nock from 'nock'; +import { ClientOAuth2, ClientOAuth2Token } from '../src'; +import * as config from './config'; +import { AuthError } from '@/utils'; + +describe('CodeFlow', () => { + beforeAll(async () => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + }); + + const uri = `/auth/callback?code=${config.code}&state=${config.state}`; + + const githubAuth = new ClientOAuth2({ + clientId: config.clientId, + clientSecret: config.clientSecret, + accessTokenUri: config.accessTokenUri, + authorizationUri: config.authorizationUri, + authorizationGrants: ['code'], + redirectUri: config.redirectUri, + scopes: ['notifications'], + }); + + describe('#getUri', () => { + it('should return a valid uri', () => { + expect(githubAuth.code.getUri()).toEqual( + `${config.authorizationUri}?client_id=abc&` + + `redirect_uri=${encodeURIComponent(config.redirectUri)}&` + + 'response_type=code&state=&scope=notifications', + ); + }); + + describe('when scopes are undefined', () => { + it('should not include scope in the uri', () => { + const authWithoutScopes = new ClientOAuth2({ + clientId: config.clientId, + clientSecret: config.clientSecret, + accessTokenUri: config.accessTokenUri, + authorizationUri: config.authorizationUri, + authorizationGrants: ['code'], + redirectUri: config.redirectUri, + }); + expect(authWithoutScopes.code.getUri()).toEqual( + `${config.authorizationUri}?client_id=abc&` + + `redirect_uri=${encodeURIComponent(config.redirectUri)}&` + + 'response_type=code&state=', + ); + }); + }); + + it('should include empty scopes array as an empty string', () => { + const authWithEmptyScopes = new ClientOAuth2({ + clientId: config.clientId, + clientSecret: config.clientSecret, + accessTokenUri: config.accessTokenUri, + authorizationUri: config.authorizationUri, + authorizationGrants: ['code'], + redirectUri: config.redirectUri, + scopes: [], + }); + expect(authWithEmptyScopes.code.getUri()).toEqual( + `${config.authorizationUri}?client_id=abc&` + + `redirect_uri=${encodeURIComponent(config.redirectUri)}&` + + 'response_type=code&state=&scope=', + ); + }); + + it('should include empty scopes string as an empty string', () => { + const authWithEmptyScopes = new ClientOAuth2({ + clientId: config.clientId, + clientSecret: config.clientSecret, + accessTokenUri: config.accessTokenUri, + authorizationUri: config.authorizationUri, + authorizationGrants: ['code'], + redirectUri: config.redirectUri, + scopes: [], + }); + expect(authWithEmptyScopes.code.getUri()).toEqual( + `${config.authorizationUri}?client_id=abc&` + + `redirect_uri=${encodeURIComponent(config.redirectUri)}&` + + 'response_type=code&state=&scope=', + ); + }); + + describe('when authorizationUri contains query parameters', () => { + it('should preserve query string parameters', () => { + const authWithParams = new ClientOAuth2({ + clientId: config.clientId, + clientSecret: config.clientSecret, + accessTokenUri: config.accessTokenUri, + authorizationUri: `${config.authorizationUri}?bar=qux`, + authorizationGrants: ['code'], + redirectUri: config.redirectUri, + scopes: ['notifications'], + }); + expect(authWithParams.code.getUri()).toEqual( + `${config.authorizationUri}?bar=qux&client_id=abc&` + + `redirect_uri=${encodeURIComponent(config.redirectUri)}&` + + 'response_type=code&state=&scope=notifications', + ); + }); + }); + }); + + describe('#getToken', () => { + const mockTokenCall = () => + nock(config.baseUrl) + .post( + '/login/oauth/access_token', + ({ code, grant_type, redirect_uri }) => + code === config.code && + grant_type === 'authorization_code' && + redirect_uri === config.redirectUri, + ) + .once() + .reply(200, { + access_token: config.accessToken, + refresh_token: config.refreshToken, + }); + + it('should request the token', async () => { + mockTokenCall(); + const user = await githubAuth.code.getToken(uri); + + expect(user).toBeInstanceOf(ClientOAuth2Token); + expect(user.accessToken).toEqual(config.accessToken); + expect(user.tokenType).toEqual('bearer'); + }); + + it('should reject with auth errors', async () => { + let errored = false; + + try { + await githubAuth.code.getToken(`${config.redirectUri}?error=invalid_request`); + } catch (err) { + errored = true; + expect(err).toBeInstanceOf(AuthError); + if (err instanceof AuthError) { + expect(err.code).toEqual('EAUTH'); + expect(err.body.error).toEqual('invalid_request'); + } + } + expect(errored).toEqual(true); + }); + + describe('#sign', () => { + it('should be able to sign a standard request object', async () => { + mockTokenCall(); + const token = await githubAuth.code.getToken(uri); + const requestOptions = token.sign({ + method: 'GET', + url: 'http://api.github.com/user', + }); + expect(requestOptions.headers?.Authorization).toEqual(`Bearer ${config.accessToken}`); + }); + }); + + describe('#refresh', () => { + const mockRefreshCall = () => + nock(config.baseUrl) + .post( + '/login/oauth/access_token', + ({ refresh_token, grant_type }) => + refresh_token === config.refreshToken && grant_type === 'refresh_token', + ) + .once() + .reply(200, { + access_token: config.refreshedAccessToken, + refresh_token: config.refreshedRefreshToken, + }); + + it('should make a request to get a new access token', async () => { + mockTokenCall(); + const token = await githubAuth.code.getToken(uri, { state: config.state }); + expect(token.refreshToken).toEqual(config.refreshToken); + + mockRefreshCall(); + const token1 = await token.refresh(); + expect(token1).toBeInstanceOf(ClientOAuth2Token); + expect(token1.accessToken).toEqual(config.refreshedAccessToken); + expect(token1.refreshToken).toEqual(config.refreshedRefreshToken); + expect(token1.tokenType).toEqual('bearer'); + }); + }); + }); +}); diff --git a/packages/@n8n/client-oauth2/test/CredentialsFlow.test.ts b/packages/@n8n/client-oauth2/test/CredentialsFlow.test.ts new file mode 100644 index 0000000000..7aee647b25 --- /dev/null +++ b/packages/@n8n/client-oauth2/test/CredentialsFlow.test.ts @@ -0,0 +1,116 @@ +import nock from 'nock'; +import { ClientOAuth2, ClientOAuth2Token } from '../src'; +import * as config from './config'; + +describe('CredentialsFlow', () => { + beforeAll(async () => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + }); + + describe('#getToken', () => { + const createAuthClient = (scopes?: string[]) => + new ClientOAuth2({ + clientId: config.clientId, + clientSecret: config.clientSecret, + accessTokenUri: config.accessTokenUri, + authorizationGrants: ['credentials'], + scopes, + }); + + const mockTokenCall = (requestedScope?: string) => + nock(config.baseUrl) + .post( + '/login/oauth/access_token', + ({ scope, grant_type }) => + scope === requestedScope && grant_type === 'client_credentials', + ) + .once() + .reply(200, { + access_token: config.accessToken, + refresh_token: config.refreshToken, + scope: requestedScope, + }); + + it('should request the token', async () => { + const authClient = createAuthClient(['notifications']); + mockTokenCall('notifications'); + + const user = await authClient.credentials.getToken(); + + expect(user).toBeInstanceOf(ClientOAuth2Token); + expect(user.accessToken).toEqual(config.accessToken); + expect(user.tokenType).toEqual('bearer'); + expect(user.data.scope).toEqual('notifications'); + }); + + it('when scopes are undefined, it should not send scopes to an auth server', async () => { + const authClient = createAuthClient(); + mockTokenCall(); + + const user = await authClient.credentials.getToken(); + expect(user).toBeInstanceOf(ClientOAuth2Token); + expect(user.accessToken).toEqual(config.accessToken); + expect(user.tokenType).toEqual('bearer'); + expect(user.data.scope).toEqual(undefined); + }); + + it('when scopes is an empty array, it should send empty scope string to an auth server', async () => { + const authClient = createAuthClient([]); + mockTokenCall(''); + + const user = await authClient.credentials.getToken(); + expect(user).toBeInstanceOf(ClientOAuth2Token); + expect(user.accessToken).toEqual(config.accessToken); + expect(user.tokenType).toEqual('bearer'); + expect(user.data.scope).toEqual(''); + }); + + describe('#sign', () => { + it('should be able to sign a standard request object', async () => { + const authClient = createAuthClient(['notifications']); + mockTokenCall('notifications'); + + const token = await authClient.credentials.getToken(); + const requestOptions = token.sign({ + method: 'GET', + url: `${config.baseUrl}/test`, + }); + + expect(requestOptions.headers?.Authorization).toEqual(`Bearer ${config.accessToken}`); + }); + }); + + describe('#refresh', () => { + const mockRefreshCall = () => + nock(config.baseUrl) + .post( + '/login/oauth/access_token', + ({ refresh_token, grant_type }) => + refresh_token === config.refreshToken && grant_type === 'refresh_token', + ) + .once() + .reply(200, { + access_token: config.refreshedAccessToken, + refresh_token: config.refreshedRefreshToken, + }); + + it('should make a request to get a new access token', async () => { + const authClient = createAuthClient(['notifications']); + mockTokenCall('notifications'); + + const token = await authClient.credentials.getToken(); + expect(token.accessToken).toEqual(config.accessToken); + + mockRefreshCall(); + const token1 = await token.refresh(); + expect(token1).toBeInstanceOf(ClientOAuth2Token); + expect(token1.accessToken).toEqual(config.refreshedAccessToken); + expect(token1.tokenType).toEqual('bearer'); + }); + }); + }); +}); diff --git a/packages/@n8n/client-oauth2/test/config.ts b/packages/@n8n/client-oauth2/test/config.ts new file mode 100644 index 0000000000..a6dd28a3ef --- /dev/null +++ b/packages/@n8n/client-oauth2/test/config.ts @@ -0,0 +1,15 @@ +export const baseUrl = 'https://mock.auth.service'; +export const accessTokenUri = baseUrl + '/login/oauth/access_token'; +export const authorizationUri = baseUrl + '/login/oauth/authorize'; +export const redirectUri = 'http://example.com/auth/callback'; + +export const accessToken = '4430eb1615fb6127cbf828a8e403'; +export const refreshToken = 'def456token'; +export const refreshedAccessToken = 'f456okeendt'; +export const refreshedRefreshToken = 'f4f6577c0f3af456okeendt'; + +export const clientId = 'abc'; +export const clientSecret = '123'; + +export const code = 'fbe55d970377e0686746'; +export const state = '7076840850058943'; diff --git a/packages/@n8n/client-oauth2/tsconfig.build.json b/packages/@n8n/client-oauth2/tsconfig.build.json new file mode 100644 index 0000000000..c8f44354c7 --- /dev/null +++ b/packages/@n8n/client-oauth2/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "types": ["node"], + "noEmit": false, + "tsBuildInfoFile": "dist/build.tsbuildinfo" + }, + "include": ["src/**/*.ts"], + "exclude": ["test/**"] +} diff --git a/packages/@n8n/client-oauth2/tsconfig.json b/packages/@n8n/client-oauth2/tsconfig.json new file mode 100644 index 0000000000..a693815582 --- /dev/null +++ b/packages/@n8n/client-oauth2/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "types": ["node", "jest"], + "composite": true, + "noEmit": true, + "baseUrl": "src", + "paths": { + "@/*": ["./*"] + }, + "tsBuildInfoFile": "dist/typecheck.tsbuildinfo" + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 222bc5b31c..3ec9e33056 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -117,6 +117,7 @@ }, "dependencies": { "@n8n_io/license-sdk": "~2.4.0", + "@n8n/client-oauth2": "workspace:*", "@oclif/command": "^1.8.16", "@oclif/core": "^1.16.4", "@oclif/errors": "^1.3.6", @@ -133,7 +134,6 @@ "change-case": "^4.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", - "client-oauth2": "^4.2.5", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", "convict": "^6.2.4", diff --git a/packages/cli/src/credentials/oauth2Credential.api.ts b/packages/cli/src/credentials/oauth2Credential.api.ts index e286ae116a..b394dfefb5 100644 --- a/packages/cli/src/credentials/oauth2Credential.api.ts +++ b/packages/cli/src/credentials/oauth2Credential.api.ts @@ -1,4 +1,5 @@ -import ClientOAuth2 from 'client-oauth2'; +import type { ClientOAuth2Options } from '@n8n/client-oauth2'; +import { ClientOAuth2 } from '@n8n/client-oauth2'; import Csrf from 'csrf'; import express from 'express'; import get from 'lodash.get'; @@ -119,7 +120,7 @@ oauth2CredentialController.get( }; const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64'); - const oAuthOptions: ClientOAuth2.Options = { + const oAuthOptions: ClientOAuth2Options = { clientId: get(oauthCredentials, 'clientId') as string, clientSecret: get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: get(oauthCredentials, 'accessTokenUrl', '') as string, @@ -250,11 +251,11 @@ oauth2CredentialController.get( return renderCallbackError(res, errorMessage); } - let options = {}; + let options: Partial = {}; - const oAuth2Parameters = { + const oAuth2Parameters: ClientOAuth2Options = { clientId: get(oauthCredentials, 'clientId') as string, - clientSecret: get(oauthCredentials, 'clientSecret', '') as string | undefined, + clientSecret: get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: get(oauthCredentials, 'authUrl', '') as string, redirectUri: `${getInstanceBaseUrl()}/${restEndpoint}/oauth2-credential/callback`, @@ -268,6 +269,7 @@ oauth2CredentialController.get( client_secret: get(oauthCredentials, 'clientSecret', '') as string, }, }; + // @ts-ignore delete oAuth2Parameters.clientSecret; } @@ -278,7 +280,8 @@ oauth2CredentialController.get( const queryParameters = req.originalUrl.split('?').splice(1, 1).join(''); const oauthToken = await oAuthObj.code.getToken( - `${oAuth2Parameters.redirectUri}?${queryParameters}`, + `${oAuth2Parameters.redirectUri as string}?${queryParameters}`, + // @ts-ignore options, ); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 5326caf458..461b082baa 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -21,6 +21,7 @@ "include": ["src/**/*.ts", "test/**/*.ts", "src/sso/saml/saml-schema-metadata-2.0.xsd"], "references": [ { "path": "../workflow/tsconfig.build.json" }, - { "path": "../core/tsconfig.build.json" } + { "path": "../core/tsconfig.build.json" }, + { "path": "../@n8n/client-oauth2/tsconfig.build.json" } ] } diff --git a/packages/core/package.json b/packages/core/package.json index f70492ded2..2807bfbbb2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "axios": "^0.21.1", - "client-oauth2": "^4.2.5", + "@n8n/client-oauth2": "workspace:*", "concat-stream": "^2.0.0", "cron": "~1.7.2", "crypto-js": "~4.1.1", diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index b32a67f32d..6bd3f5433b 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -82,7 +82,12 @@ import { IncomingMessage } from 'http'; import { stringify } from 'qs'; import type { Token } from 'oauth-1.0a'; import clientOAuth1 from 'oauth-1.0a'; -import clientOAuth2 from 'client-oauth2'; +import type { + ClientOAuth2Options, + ClientOAuth2RequestObject, + ClientOAuth2TokenData, +} from '@n8n/client-oauth2'; +import { ClientOAuth2 } from '@n8n/client-oauth2'; import crypto, { createHmac } from 'crypto'; import get from 'lodash.get'; import type { Request, Response } from 'express'; @@ -1081,14 +1086,14 @@ export async function requestOAuth2( throw new Error('OAuth credentials not connected!'); } - const oAuthClient = new clientOAuth2({ + const oAuthClient = new ClientOAuth2({ clientId: credentials.clientId as string, clientSecret: credentials.clientSecret as string, accessTokenUri: credentials.accessTokenUrl as string, scopes: (credentials.scope as string).split(' '), }); - let oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; + let oauthTokenData = credentials.oauthTokenData as ClientOAuth2TokenData; // if it's the first time using the credentials, get the access token and save it into the DB. if ( @@ -1116,15 +1121,20 @@ export async function requestOAuth2( oauthTokenData = data; } + const accessToken = + get(oauthTokenData, oAuth2Options?.property as string) || oauthTokenData.accessToken; + const refreshToken = oauthTokenData.refreshToken; const token = oAuthClient.createToken( - get(oauthTokenData, oAuth2Options?.property as string) || oauthTokenData.accessToken, - oauthTokenData.refreshToken, + { + ...oauthTokenData, + ...(accessToken ? { access_token: accessToken } : {}), + ...(refreshToken ? { refresh_token: refreshToken } : {}), + }, oAuth2Options?.tokenType || oauthTokenData.tokenType, - oauthTokenData, ); // Signs the request by adding authorization headers or query parameters depending // on the token-type used. - const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); + const newRequestOptions = token.sign(requestOptions as ClientOAuth2RequestObject); const newRequestHeaders = (newRequestOptions.headers = newRequestOptions.headers ?? {}); // If keep bearer is false remove the it from the authorization header if (oAuth2Options?.keepBearer === false && typeof newRequestHeaders.Authorization === 'string') { @@ -1164,7 +1174,7 @@ export async function requestOAuth2( if (OAuth2GrantType.clientCredentials === credentials.grantType) { newToken = await getClientCredentialsToken(token.client, credentials); } else { - newToken = await token.refresh(tokenRefreshOptions); + newToken = await token.refresh(tokenRefreshOptions as unknown as ClientOAuth2Options); } Logger.debug( @@ -1184,7 +1194,7 @@ export async function requestOAuth2( credentialsType, credentials, ); - const refreshedRequestOption = newToken.sign(requestOptions as clientOAuth2.RequestObject); + const refreshedRequestOption = newToken.sign(requestOptions as ClientOAuth2RequestObject); if (oAuth2Options?.keyToIncludeInAccessTokenHeader) { Object.assign(newRequestHeaders, { @@ -1197,6 +1207,11 @@ export async function requestOAuth2( throw error; }); } + const tokenExpiredStatusCode = + oAuth2Options?.tokenExpiredStatusCode === undefined + ? 401 + : oAuth2Options?.tokenExpiredStatusCode; + return this.helpers .request(newRequestOptions) .then((response) => { @@ -1204,21 +1219,14 @@ export async function requestOAuth2( if ( requestOptions.resolveWithFullResponse === true && requestOptions.simple === false && - response.statusCode === - (oAuth2Options?.tokenExpiredStatusCode === undefined - ? 401 - : oAuth2Options?.tokenExpiredStatusCode) + response.statusCode === tokenExpiredStatusCode ) { throw response; } return response; }) .catch(async (error: IResponseError) => { - const statusCodeReturned = - oAuth2Options?.tokenExpiredStatusCode === undefined - ? 401 - : oAuth2Options?.tokenExpiredStatusCode; - if (error.statusCode === statusCodeReturned) { + if (error.statusCode === tokenExpiredStatusCode) { // Token is probably not valid anymore. So try refresh it. const tokenRefreshOptions: IDataObject = {}; if (oAuth2Options?.includeCredentialsOnRefreshOnBody) { @@ -1243,7 +1251,7 @@ export async function requestOAuth2( if (OAuth2GrantType.clientCredentials === credentials.grantType) { newToken = await getClientCredentialsToken(token.client, credentials); } else { - newToken = await token.refresh(tokenRefreshOptions); + newToken = await token.refresh(tokenRefreshOptions as unknown as ClientOAuth2Options); } Logger.debug( `OAuth2 token for "${credentialsType}" used by node "${node.name}" has been renewed.`, @@ -1271,7 +1279,7 @@ export async function requestOAuth2( ); // Make the request again with the new token - const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); + const newRequestOptions = newToken.sign(requestOptions as ClientOAuth2RequestObject); newRequestOptions.headers = newRequestOptions.headers ?? {}; if (oAuth2Options?.keyToIncludeInAccessTokenHeader) { diff --git a/packages/core/src/OAuth2Helper.ts b/packages/core/src/OAuth2Helper.ts index a66763505c..dacb8d44a0 100644 --- a/packages/core/src/OAuth2Helper.ts +++ b/packages/core/src/OAuth2Helper.ts @@ -1,15 +1,15 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; -import type clientOAuth2 from 'client-oauth2'; +import type { ClientOAuth2, ClientOAuth2Options, ClientOAuth2Token } from '@n8n/client-oauth2'; export const getClientCredentialsToken = async ( - oAuth2Client: clientOAuth2, + oAuth2Client: ClientOAuth2, credentials: ICredentialDataDecryptedObject, -): Promise => { +): Promise => { const options = {}; if (credentials.authentication === 'body') { Object.assign(options, { headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention Authorization: '', }, body: { @@ -18,5 +18,5 @@ export const getClientCredentialsToken = async ( }, }); } - return oAuth2Client.credentials.getToken(options); + return oAuth2Client.credentials.getToken(options as ClientOAuth2Options); }; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 69536da448..ed61b927dd 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -14,5 +14,8 @@ "useUnknownInCatchVariables": false }, "include": ["src/**/*.ts", "test/**/*.ts"], - "references": [{ "path": "../workflow/tsconfig.build.json" }] + "references": [ + { "path": "../workflow/tsconfig.build.json" }, + { "path": "../@n8n/client-oauth2/tsconfig.build.json" } + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34f063fb3c..6f122dc473 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,12 @@ importers: specifier: ^1.0.24 version: 1.0.24(typescript@5.0.3) + packages/@n8n/client-oauth2: + dependencies: + axios: + specifier: ^0.21.1 + version: 0.21.4(debug@4.3.2) + packages/@n8n_io/eslint-config: devDependencies: '@types/eslint': @@ -187,6 +193,9 @@ importers: packages/cli: dependencies: + '@n8n/client-oauth2': + specifier: workspace:* + version: link:../@n8n/client-oauth2 '@n8n_io/license-sdk': specifier: ~2.4.0 version: 2.4.0 @@ -238,9 +247,6 @@ importers: class-validator: specifier: ^0.14.0 version: 0.14.0 - client-oauth2: - specifier: ^4.2.5 - version: 4.3.3 compression: specifier: ^1.7.4 version: 1.7.4 @@ -626,12 +632,12 @@ importers: packages/core: dependencies: + '@n8n/client-oauth2': + specifier: workspace:* + version: link:../@n8n/client-oauth2 axios: specifier: ^0.21.1 version: 0.21.4(debug@4.3.2) - client-oauth2: - specifier: ^4.2.5 - version: 4.3.3 concat-stream: specifier: ^2.0.0 version: 2.0.0 @@ -5970,10 +5976,6 @@ packages: - supports-color dev: true - /@servie/events@1.0.0: - resolution: {integrity: sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw==} - dev: false - /@sideway/address@4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -8165,10 +8167,6 @@ packages: resolution: {integrity: sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==} dev: true - /@types/tough-cookie@2.3.8: - resolution: {integrity: sha512-7axfYN8SW9pWg78NgenHasSproWQee5rzyPVLC9HpaQSDgNArsnKJD88EaMfi4Pl48AyciO3agYCFqpHS1gLpg==} - dev: false - /@types/tough-cookie@4.0.2: resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} dev: true @@ -10097,10 +10095,6 @@ packages: streamsearch: 1.1.0 dev: false - /byte-length@1.0.2: - resolution: {integrity: sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==} - dev: false - /bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -10543,14 +10537,6 @@ packages: engines: {node: '>= 10'} dev: false - /client-oauth2@4.3.3: - resolution: {integrity: sha512-k8AvUYJon0vv75ufoVo4nALYb/qwFFicO3I0+39C6xEdflqVtr+f9cy+0ZxAduoVSTfhP5DX2tY2XICAd5hy6Q==} - engines: {node: '>=4.2.0'} - dependencies: - popsicle: 12.1.0 - safe-buffer: 5.2.1 - dev: false - /cliui@3.2.0: resolution: {integrity: sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==} dependencies: @@ -14564,11 +14550,6 @@ packages: - supports-color dev: false - /ip-regex@2.1.0: - resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} - engines: {node: '>=4'} - dev: false - /ip@1.1.8: resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} dev: false @@ -16786,14 +16767,9 @@ packages: dependencies: semver: 6.3.0 - /make-error-cause@2.3.0: - resolution: {integrity: sha512-etgt+n4LlOkGSJbBTV9VROHA5R7ekIPS4vfh+bCAoJgRrJWdqJCBbpS3osRJ/HrT7R68MzMiY3L3sDJ/Fd8aBg==} - dependencies: - make-error: 1.3.6 - dev: false - /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true /make-fetch-happen@9.1.0: resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} @@ -18595,70 +18571,6 @@ packages: '@babel/runtime': 7.20.7 dev: true - /popsicle-content-encoding@1.0.0(servie@4.3.3): - resolution: {integrity: sha512-4Df+vTfM8wCCJVTzPujiI6eOl3SiWQkcZg0AMrOkD1enMXsF3glIkFUZGvour1Sj7jOWCsNSEhBxpbbhclHhzw==} - peerDependencies: - servie: ^4.0.0 - dependencies: - servie: 4.3.3 - dev: false - - /popsicle-cookie-jar@1.0.0(servie@4.3.3): - resolution: {integrity: sha512-vrlOGvNVELko0+J8NpGC5lHWDGrk8LQJq9nwAMIVEVBfN1Lib3BLxAaLRGDTuUnvl45j5N9dT2H85PULz6IjjQ==} - peerDependencies: - servie: ^4.0.0 - dependencies: - '@types/tough-cookie': 2.3.8 - servie: 4.3.3 - tough-cookie: 3.0.1 - dev: false - - /popsicle-redirects@1.1.1(servie@4.3.3): - resolution: {integrity: sha512-mC2HrKjdTAWDalOjGxlXw9j6Qxrz/Yd2ui6bPxpi2IQDYWpF4gUAMxbA8EpSWJhLi0PuWKDwTHHPrUPGutAoIA==} - peerDependencies: - servie: ^4.1.0 - dependencies: - servie: 4.3.3 - dev: false - - /popsicle-transport-http@1.2.1(servie@4.3.3): - resolution: {integrity: sha512-i5r3IGHkGiBDm1oPFvOfEeSGWR0lQJcsdTqwvvDjXqcTHYJJi4iSi3ecXIttDiTBoBtRAFAE9nF91fspQr63FQ==} - peerDependencies: - servie: ^4.2.0 - dependencies: - make-error-cause: 2.3.0 - servie: 4.3.3 - dev: false - - /popsicle-transport-xhr@2.0.0(servie@4.3.3): - resolution: {integrity: sha512-5Sbud4Widngf1dodJE5cjEYXkzEUIl8CzyYRYR57t6vpy9a9KPGQX6KBKdPjmBZlR5A06pOBXuJnVr23l27rtA==} - peerDependencies: - servie: ^4.2.0 - dependencies: - servie: 4.3.3 - dev: false - - /popsicle-user-agent@1.0.0(servie@4.3.3): - resolution: {integrity: sha512-epKaq3TTfTzXcxBxjpoKYMcTTcAX8Rykus6QZu77XNhJuRHSRxMd+JJrbX/3PFI0opFGSN0BabbAYCbGxbu0mA==} - peerDependencies: - servie: ^4.0.0 - dependencies: - servie: 4.3.3 - dev: false - - /popsicle@12.1.0: - resolution: {integrity: sha512-muNC/cIrWhfR6HqqhHazkxjob3eyECBe8uZYSQ/N5vixNAgssacVleerXnE8Are5fspR0a+d2qWaBR1g7RYlmw==} - dependencies: - popsicle-content-encoding: 1.0.0(servie@4.3.3) - popsicle-cookie-jar: 1.0.0(servie@4.3.3) - popsicle-redirects: 1.1.1(servie@4.3.3) - popsicle-transport-http: 1.2.1(servie@4.3.3) - popsicle-transport-xhr: 2.0.0(servie@4.3.3) - popsicle-user-agent: 1.0.0(servie@4.3.3) - servie: 4.3.3 - throwback: 4.1.0 - dev: false - /posix-character-classes@0.1.1: resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} engines: {node: '>=0.10.0'} @@ -20234,14 +20146,6 @@ packages: transitivePeerDependencies: - supports-color - /servie@4.3.3: - resolution: {integrity: sha512-b0IrY3b1gVMsWvJppCf19g1p3JSnS0hQi6xu4Hi40CIhf0Lx8pQHcvBL+xunShpmOiQzg1NOia812NAWdSaShw==} - dependencies: - '@servie/events': 1.0.0 - byte-length: 1.0.2 - ts-expect: 1.3.0 - dev: false - /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -21382,10 +21286,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - /throwback@4.1.0: - resolution: {integrity: sha512-dLFe8bU8SeH0xeqeKL7BNo8XoPC/o91nz9/ooeplZPiso+DZukhoyZcSz9TFnUNScm+cA9qjU1m1853M6sPOng==} - dev: false - /time-stamp@1.1.0: resolution: {integrity: sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==} engines: {node: '>=0.10.0'} @@ -21547,15 +21447,6 @@ packages: psl: 1.9.0 punycode: 2.2.0 - /tough-cookie@3.0.1: - resolution: {integrity: sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==} - engines: {node: '>=6'} - dependencies: - ip-regex: 2.1.0 - psl: 1.9.0 - punycode: 2.2.0 - dev: false - /tough-cookie@4.1.2: resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} @@ -21604,10 +21495,6 @@ packages: typescript: 5.0.3 dev: true - /ts-expect@1.3.0: - resolution: {integrity: sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==} - dev: false - /ts-jest@29.1.0(@babel/core@7.21.8)(jest@29.5.0)(typescript@5.0.3): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4a89a5858c..ce18506b38 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - packages/* + - packages/@n8n/* - packages/@n8n_io/*