From 19d4a1bd24800f25dc491abac714cfcd731b622f Mon Sep 17 00:00:00 2001 From: Jay Vercellone Date: Tue, 17 Sep 2024 14:24:15 -0700 Subject: [PATCH 1/3] Abstract OAuth implementation in the SDK * Install `simple-oauth2` to handle OAuth client requests * Rename some methods and types to reuse them * Abstract the HTTP requests so that it can be used for Connect and for normal API requests * Implement logic to fetch OAuth client credentials * Bump minor version of the SDK --- packages/sdk/package-lock.json | 4 +- packages/sdk/package.json | 6 ++- packages/sdk/src/server/index.ts | 91 +++++++++++++++++++++++++++++--- pnpm-lock.yaml | 42 +++++++++++++++ 4 files changed, 132 insertions(+), 11 deletions(-) diff --git a/packages/sdk/package-lock.json b/packages/sdk/package-lock.json index b8bf291230a60..3a610b100520a 100644 --- a/packages/sdk/package-lock.json +++ b/packages/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pipedream/sdk", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pipedream/sdk", - "version": "0.0.1", + "version": "0.1.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@types/node": "^20.14.9", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f8b8b75ec5e01..9a9979a5dbf02 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sdk", - "version": "0.0.13", + "version": "0.1.0", "description": "Pipedream SDK", "type": "module", "main": "dist/server/index.js", @@ -40,6 +40,10 @@ ], "devDependencies": { "@types/node": "^20.14.9", + "@types/simple-oauth2": "^5.0.7", "typescript": "^5.5.2" + }, + "dependencies": { + "simple-oauth2": "^5.1.0" } } diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts index 7f7e879f0e0c6..4f26faaff5da9 100644 --- a/packages/sdk/src/server/index.ts +++ b/packages/sdk/src/server/index.ts @@ -2,6 +2,8 @@ // Pipedream project's public and secret keys and access customer credentials. // See the browser/ directory for the browser client. +import { ClientCredentials } from "simple-oauth2"; + /** * Options for creating a server-side client. * This is used to configure the ServerClient instance. @@ -22,6 +24,16 @@ export type CreateServerClientOpts = { */ secretKey: string; + /** + * The client ID of your workspace's OAuth application. + */ + oauthClientId: string; + + /** + * The client secret of your workspace's OAuth application. + */ + oauthClientSecret: string; + /** * The API host URL. Used by Pipedream employees. Defaults to "api.pipedream.com" if not provided. */ @@ -233,7 +245,7 @@ export type ConnectAPIResponse = T | ErrorResponse; /** * Options for making a request to the Connect API. */ -interface ConnectRequestOptions extends Omit { +interface RequestOptions extends Omit { /** * Query parameters to include in the request URL. */ @@ -269,6 +281,7 @@ class ServerClient { environment?: string; secretKey: string; publicKey: string; + oauthClient: ClientCredentials; baseURL: string; /** @@ -283,20 +296,37 @@ class ServerClient { const { apiHost = "api.pipedream.com" } = opts; this.baseURL = `https://${apiHost}/v1`; + + this.oauthClient = new ClientCredentials({ + client: { + id: opts.oauthClientId, + secret: opts.oauthClientSecret, + }, + auth: { + tokenHost: this.baseURL, + tokenPath: "/v1/oauth/token", + }, + }); } /** - * Generates an Authorization header using the public and secret keys. + * Generates an Authorization header for Connect using the public and secret + * keys of the target project. * * @returns The authorization header as a string. */ - private _authorizationHeader(): string { + private _connectAuthorizationHeader(): string { const encoded = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64"); return `Basic ${encoded}`; } + async _oauthAuthorizationHeader(): Promise { + const { token: { access_token: accessToken } } = await this.oauthClient.getToken({}); + return `Bearer ${accessToken}`; + } + /** - * Makes a request to the Connect API. + * Makes an HTTP request * * @template T - The expected response type. * @param path - The API endpoint path. @@ -304,9 +334,9 @@ class ServerClient { * @returns A promise resolving to the API response. * @throws Will throw an error if the response status is not OK. */ - async _makeConnectRequest( + async _makeRequest( path: string, - opts: ConnectRequestOptions = {}, + opts: RequestOptions = {}, ): Promise { const { params, @@ -315,7 +345,7 @@ class ServerClient { method = "GET", ...fetchOpts } = opts; - const url = new URL(`${this.baseURL}/connect${path}`); + const url = new URL(`${this.baseURL}${path}`); if (params) { Object.entries(params).forEach(([ @@ -329,7 +359,6 @@ class ServerClient { } const headers = { - "Authorization": this._authorizationHeader(), "Content-Type": "application/json", ...customHeaders, }; @@ -358,6 +387,52 @@ class ServerClient { return result; } + /** + * Makes a request to the Connect API. + * + * @template T - The expected response type. + * @param path - The API endpoint path. + * @param opts - The options for the request. + * @returns A promise resolving to the API response. + * @throws Will throw an error if the response status is not OK. + */ + async _makeApiRequest( + path: string, + opts: RequestOptions = {}, + ): Promise { + const headers = { + ...opts.headers ?? {}, + "Authorization": await this._oauthAuthorizationHeader(), + }; + return this._makeRequest(path, { + headers, + ...opts, + }); + } + + /** + * Makes a request to the Connect API. + * + * @template T - The expected response type. + * @param path - The API endpoint path. + * @param opts - The options for the request. + * @returns A promise resolving to the API response. + * @throws Will throw an error if the response status is not OK. + */ + async _makeConnectRequest( + path: string, + opts: RequestOptions = {}, + ): Promise { + const headers = { + ...opts.headers ?? {}, + "Authorization": this._connectAuthorizationHeader(), + }; + return this._makeRequest(`/connect${path}`, { + headers, + ...opts, + }); + } + /** * Creates a new connect token. * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6d11dd9a0fc7..1aa916cee0305 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11485,9 +11485,14 @@ importers: packages/sdk: specifiers: '@types/node': ^20.14.9 + '@types/simple-oauth2': ^5.0.7 + simple-oauth2: ^5.1.0 typescript: ^5.5.2 + dependencies: + simple-oauth2: 5.1.0 devDependencies: '@types/node': 20.16.1 + '@types/simple-oauth2': 5.0.7 typescript: 5.5.4 packages/snowflake-sdk: @@ -16534,6 +16539,20 @@ packages: yargs: 17.7.2 dev: false + /@hapi/boom/10.0.1: + resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==} + dependencies: + '@hapi/hoek': 11.0.4 + dev: false + + /@hapi/bourne/3.0.0: + resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + dev: false + + /@hapi/hoek/11.0.4: + resolution: {integrity: sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==} + dev: false + /@hapi/hoek/9.3.0: resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} dev: false @@ -16544,6 +16563,14 @@ packages: '@hapi/hoek': 9.3.0 dev: false + /@hapi/wreck/18.1.0: + resolution: {integrity: sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/hoek': 11.0.4 + dev: false + /@huggingface/inference/1.8.0: resolution: {integrity: sha512-Dkh7PiyMf6TINRocQsdceiR5LcqJiUHgWjaBMRpCUOCbs+GZA122VH9q+wodoSptj6rIQf7wIwtDsof+/gd0WA==} engines: {node: '>=18'} @@ -21107,6 +21134,10 @@ packages: '@types/node': 20.16.1 dev: false + /@types/simple-oauth2/5.0.7: + resolution: {integrity: sha512-8JbWVJbiTSBQP/7eiyGKyXWAqp3dKQZpaA+pdW16FCi32ujkzRMG8JfjoAzdWt6W8U591ZNdHcPtP2D7ILTKuA==} + dev: true + /@types/source-map/0.5.7: resolution: {integrity: sha512-LrnsgZIfJaysFkv9rRJp4/uAyqw87oVed3s1hhF83nwbo9c7MG9g5DqR0seHP+lkX4ldmMrVolPjQSe2ZfD0yA==} deprecated: This is a stub types definition for source-map (https://github.com/mozilla/source-map). source-map provides its own type definitions, so you don't need @types/source-map installed! @@ -33039,6 +33070,17 @@ packages: resolution: {integrity: sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==} dev: false + /simple-oauth2/5.1.0: + resolution: {integrity: sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==} + dependencies: + '@hapi/hoek': 11.0.4 + '@hapi/wreck': 18.1.0 + debug: 4.3.6 + joi: 17.10.2 + transitivePeerDependencies: + - supports-color + dev: false + /simple-swizzle/0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: From e1e7efcdbaf4a485bd29870ae014766bc7628264 Mon Sep 17 00:00:00 2001 From: Jay Vercellone Date: Wed, 18 Sep 2024 12:10:41 -0700 Subject: [PATCH 2/3] Lazy token query --- packages/sdk/src/server/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts index 4f26faaff5da9..7917fef337b05 100644 --- a/packages/sdk/src/server/index.ts +++ b/packages/sdk/src/server/index.ts @@ -2,7 +2,10 @@ // Pipedream project's public and secret keys and access customer credentials. // See the browser/ directory for the browser client. -import { ClientCredentials } from "simple-oauth2"; +import { + AccessToken, + ClientCredentials, +} from "simple-oauth2"; /** * Options for creating a server-side client. @@ -282,6 +285,7 @@ class ServerClient { secretKey: string; publicKey: string; oauthClient: ClientCredentials; + oauthToken?: AccessToken; baseURL: string; /** @@ -321,8 +325,11 @@ class ServerClient { } async _oauthAuthorizationHeader(): Promise { - const { token: { access_token: accessToken } } = await this.oauthClient.getToken({}); - return `Bearer ${accessToken}`; + if (!this.oauthToken || this.oauthToken.expired()) { + this.oauthToken = await this.oauthClient.getToken({}); + } + + return `Bearer ${this.oauthToken.token.access_token}`; } /** From e8773e7ac3ab1c766502904f64fc88df87ce8b35 Mon Sep 17 00:00:00 2001 From: Jay Vercellone Date: Thu, 19 Sep 2024 09:18:33 -0700 Subject: [PATCH 3/3] Apply PR feedback --- packages/sdk/src/server/index.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts index 7917fef337b05..bf9a6f9521a84 100644 --- a/packages/sdk/src/server/index.ts +++ b/packages/sdk/src/server/index.ts @@ -30,12 +30,12 @@ export type CreateServerClientOpts = { /** * The client ID of your workspace's OAuth application. */ - oauthClientId: string; + oauthClientId?: string; /** * The client secret of your workspace's OAuth application. */ - oauthClientSecret: string; + oauthClientSecret?: string; /** * The API host URL. Used by Pipedream employees. Defaults to "api.pipedream.com" if not provided. @@ -246,7 +246,7 @@ export type ErrorResponse = { export type ConnectAPIResponse = T | ErrorResponse; /** - * Options for making a request to the Connect API. + * Options for making a request to the Pipedream API. */ interface RequestOptions extends Omit { /** @@ -301,13 +301,27 @@ class ServerClient { const { apiHost = "api.pipedream.com" } = opts; this.baseURL = `https://${apiHost}/v1`; + this._configureOauthClient(opts, this.baseURL); + } + + private _configureOauthClient( + { + oauthClientId: id, + oauthClientSecret: secret, + }: CreateServerClientOpts, + tokenHost: string, + ) { + if (!id || !secret) { + return; + } + this.oauthClient = new ClientCredentials({ client: { - id: opts.oauthClientId, - secret: opts.oauthClientSecret, + id, + secret, }, auth: { - tokenHost: this.baseURL, + tokenHost, tokenPath: "/v1/oauth/token", }, }); @@ -395,7 +409,7 @@ class ServerClient { } /** - * Makes a request to the Connect API. + * Makes a request to the Pipedream API. * * @template T - The expected response type. * @param path - The API endpoint path.