diff --git a/components/bitget/bitget.app.mjs b/components/bitget/bitget.app.mjs index 9e8459834a2f1..095dd54f7276b 100644 --- a/components/bitget/bitget.app.mjs +++ b/components/bitget/bitget.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/explorium/explorium.app.mjs b/components/explorium/explorium.app.mjs index 92fbaf841eea0..ec87724048738 100644 --- a/components/explorium/explorium.app.mjs +++ b/components/explorium/explorium.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/hana/hana.app.mjs b/components/hana/hana.app.mjs index 5f3ba1de0fdc4..fb83108e5ae02 100644 --- a/components/hana/hana.app.mjs +++ b/components/hana/hana.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/indiefunnels/indiefunnels.app.mjs b/components/indiefunnels/indiefunnels.app.mjs index 6dbeb22a485a6..30d2e49de457d 100644 --- a/components/indiefunnels/indiefunnels.app.mjs +++ b/components/indiefunnels/indiefunnels.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/joggai/joggai.app.mjs b/components/joggai/joggai.app.mjs index b0055b67aa8d5..0c55a40a7969a 100644 --- a/components/joggai/joggai.app.mjs +++ b/components/joggai/joggai.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/mumble/mumble.app.mjs b/components/mumble/mumble.app.mjs index 544d2c1b11131..9afc033b525e3 100644 --- a/components/mumble/mumble.app.mjs +++ b/components/mumble/mumble.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/sign_plus/sign_plus.app.mjs b/components/sign_plus/sign_plus.app.mjs index 124038260f41d..e0eed75cc9c2c 100644 --- a/components/sign_plus/sign_plus.app.mjs +++ b/components/sign_plus/sign_plus.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/webscrape_ai/webscrape_ai.app.mjs b/components/webscrape_ai/webscrape_ai.app.mjs index 548b7834f801f..b4ac92067596d 100644 --- a/components/webscrape_ai/webscrape_ai.app.mjs +++ b/components/webscrape_ai/webscrape_ai.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 6946daf37818b..43471d8d43a77 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -2,6 +2,12 @@ # Changelog +## [1.7.0] - 2025-07-03 + +### Added + +- Added optional scope parameter to backendClient creation. + ## [1.6.11] - 2025-07-02 ### Added diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 66b56180e1f9a..774c3089e24a4 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,7 +1,7 @@ { "name": "@pipedream/sdk", "type": "module", - "version": "1.6.11", + "version": "1.7.0", "description": "Pipedream SDK", "main": "./dist/server.js", "module": "./dist/server.js", diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts index ff426614bd5a5..c66abfa18b5c1 100644 --- a/packages/sdk/src/server/index.ts +++ b/packages/sdk/src/server/index.ts @@ -56,6 +56,11 @@ export type BackendClientOpts = { * https://pipedream.com/docs/workflows/domains */ workflowDomain?: string; + + /** + * OAuth scope to request when obtaining access tokens + */ + scope?: string[]; }; /** @@ -197,6 +202,7 @@ export class BackendClient extends BaseClient { }; protected override projectId: string = ""; private staticAccessToken?: string; + private scope?: string[]; /** * Constructs a new ServerClient instance. @@ -209,6 +215,7 @@ export class BackendClient extends BaseClient { this.ensureValidEnvironment(opts.environment); this.projectId = opts.projectId; + this.scope = opts.scope; if ("accessToken" in opts.credentials) { this.staticAccessToken = opts.credentials.accessToken; } else { @@ -264,6 +271,16 @@ export class BackendClient extends BaseClient { return this.ensureValidOauthAccessToken(); } + /** + * Returns true if the client is configured to use a static access token. + * + * @returns True if the client is configured to use a static access token. + * + */ + public isStaticAccessToken(): boolean { + return !!this.staticAccessToken; + } + protected authHeaders(): string | Promise { if (this.staticAccessToken) { return `Bearer ${this.staticAccessToken}`; @@ -281,31 +298,63 @@ export class BackendClient extends BaseClient { as, } = this.oauthClient - let attempts = 0; const maxAttempts = 2; while (!this.oauthAccessToken || this.oauthAccessToken.expiresAt - Date.now() < 1000) { - if (attempts > maxAttempts) { - throw new Error("ran out of attempts trying to retrieve oauth access token"); - } - if (attempts > 0) { - // Wait for a short duration before retrying to avoid rapid retries - await new Promise((resolve) => setTimeout(resolve, 100)); + for (let attempts = 0; attempts <= maxAttempts; attempts++) { + if (attempts > 0) { + // Wait for a short duration before retrying to avoid rapid retries + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + const parameters = new URLSearchParams(); + if (this.scope && this.scope.length > 0) { + parameters.set("scope", this.scope.join(" ")); + } + parameters.set("project_id", this.projectId); + parameters.set("environment", this.environment); + try { + const response = await oauth.clientCredentialsGrantRequest(as, client, clientAuth, parameters); + const oauthTokenResponse = await oauth.processClientCredentialsResponse(as, client, response); + this.oauthAccessToken = { + token: oauthTokenResponse.access_token, + expiresAt: Date.now() + (oauthTokenResponse.expires_in || 0) * 1000, + }; + break; // Successfully got token, exit retry loop + } catch (e) { + // Extract error details from OAuth response + let errorMessage = "OAuth token request failed"; + let wwwAuthenticate: string | undefined; + let statusCode: number | undefined; + if (e instanceof Error) { + errorMessage = e.message; + } + // Check if the error contains response information + if (e && typeof e === "object" && "response" in e) { + const errorResponse = (e as any).response; + if (errorResponse) { + statusCode = errorResponse.status; + wwwAuthenticate = errorResponse.headers?.get?.("www-authenticate") || + errorResponse.headers?.["www-authenticate"]; + // Create more specific error message based on status code + if (statusCode === 401) { + errorMessage = `OAuth authentication failed (401 Unauthorized)${wwwAuthenticate ? `: ${wwwAuthenticate}` : ""}`; + } else if (statusCode === 400) { + errorMessage = "OAuth request invalid (400 Bad Request) - check client credentials"; + } else if (statusCode) { + errorMessage = `OAuth request failed with status ${statusCode}`; + } + } + } + const error = new Error(errorMessage); + (error as any).statusCode = statusCode; + (error as any).wwwAuthenticate = wwwAuthenticate; + // If this is the last attempt, throw the error + if (attempts >= maxAttempts) { + throw error; + } + } } - - const parameters = new URLSearchParams(); - try { - const response = await oauth.clientCredentialsGrantRequest(as, client, clientAuth, parameters); - const oauthTokenResponse = await oauth.processClientCredentialsResponse(as, client, response); - this.oauthAccessToken = { - token: oauthTokenResponse.access_token, - expiresAt: Date.now() + (oauthTokenResponse.expires_in || 0) * 1000, - }; - } catch { - // pass - } - - attempts++; } return this.oauthAccessToken.token; diff --git a/packages/sdk/src/shared/index.ts b/packages/sdk/src/shared/index.ts index 5da267b89bb91..b5637c290b5d3 100644 --- a/packages/sdk/src/shared/index.ts +++ b/packages/sdk/src/shared/index.ts @@ -865,6 +865,14 @@ export type ErrorResponse = { * The error message returned by the API. */ error: string; + /** + * The www-authenticate header value, if present in the error response. + */ + wwwAuthenticate?: string; + /** + * Additional error headers that may be relevant for error handling. + */ + headers?: Record; }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f11d67ff96568..90f19102f9d1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14743,8 +14743,7 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/webscrape_ai: - specifiers: {} + components/webscrape_ai: {} components/webscraper_io: dependencies: