diff --git a/.fernignore b/.fernignore index 11e5282..9db674a 100644 --- a/.fernignore +++ b/.fernignore @@ -5,6 +5,9 @@ .prettierrc.yml LICENSE +src/api/resources/index.ts src/api/resources/proxy/client/* +src/api/resources/workflows/client/* +src/api/types/index.ts src/index.ts src/wrapper diff --git a/package.json b/package.json index 89d8150..6a8c6e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sdk", - "version": "2.0.0", + "version": "2.0.0-rc.1", "private": false, "repository": "github:PipedreamHQ/pipedream-sdk-typescript", "type": "commonjs", @@ -39,17 +39,17 @@ "test:wire": "jest --selectProjects wire" }, "devDependencies": { - "webpack": "^5.97.1", - "ts-loader": "^9.5.1", - "jest": "^29.7.0", "@jest/globals": "^29.7.0", "@types/jest": "^29.5.14", - "ts-jest": "^29.3.4", + "@types/node": "^18.19.70", + "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "msw": "^2.8.4", - "@types/node": "^18.19.70", "prettier": "^3.4.2", - "typescript": "~5.7.2" + "ts-jest": "^29.3.4", + "ts-loader": "^9.5.1", + "typescript": "~5.7.2", + "webpack": "^5.97.1" }, "browser": { "fs": false, diff --git a/reference.md b/reference.md index 9e65bab..3ffa14e 100644 --- a/reference.md +++ b/reference.md @@ -2291,3 +2291,139 @@ await client.oauthTokens.create({ + +## Workflows + +
client.workflows.invoke({ ...params }, authType?) -> unknown +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +// Invoke with URL +await client.workflows.invoke({ + urlOrEndpoint: "https://en-your-endpoint.m.pipedream.net", + body: { + foo: 123, + bar: "abc", + baz: null, + }, + headers: { + Accept: "application/json", + }, +}); + +// Invoke with endpoint ID +await client.workflows.invoke({ + urlOrEndpoint: "en123", + body: { + message: "Hello, World\!", + }, +}, Pipedream.HTTPAuthType.OAuth); +``` + +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `Pipedream.InvokeWorkflowOpts` + +
+
+ +
+
+ +**authType:** `Pipedream.HTTPAuthType` — The type of authorization to use for the request (defaults to None) + +
+
+ +
+
+ +**requestOptions:** `Workflows.RequestOptions` + +
+
+
+
+ +
+
+
+ +
client.workflows.invokeForExternalUser({ ...params }) -> unknown +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.workflows.invokeForExternalUser({ + urlOrEndpoint: "https://your-workflow-url.m.pipedream.net", + externalUserId: "your-external-user-id", + body: { + foo: 123, + bar: "abc", + baz: null, + }, + headers: { + Accept: "application/json", + }, +}); +``` + +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `Pipedream.InvokeWorkflowForExternalUserOpts` + +
+
+ +
+
+ +**requestOptions:** `Workflows.RequestOptions` + +
+
+
+
+ +
+
+
diff --git a/src/Client.ts b/src/Client.ts index 6cece9c..8a1fab1 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -51,7 +51,7 @@ export declare namespace PipedreamClient { export class PipedreamClient { protected readonly _options: PipedreamClient.Options; - private readonly _oauthTokenProvider: core.OAuthTokenProvider; + protected readonly _oauthTokenProvider: core.OAuthTokenProvider; protected _appCategories: AppCategories | undefined; protected _apps: Apps | undefined; protected _accounts: Accounts | undefined; diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index e98254e..8de5037 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -11,6 +11,7 @@ export * as projects from "./projects/index.js"; export * as proxy from "./proxy/index.js"; export * as tokens from "./tokens/index.js"; export * as oauthTokens from "./oauthTokens/index.js"; +export * as workflows from "./workflows/index.js"; export * from "./apps/client/requests/index.js"; export * from "./accounts/client/requests/index.js"; export * from "./components/client/requests/index.js"; @@ -20,3 +21,4 @@ export * from "./deployedTriggers/client/requests/index.js"; export * from "./proxy/client/requests/index.js"; export * from "./tokens/client/requests/index.js"; export * from "./oauthTokens/client/requests/index.js"; +export * from "./workflows/client/requests/index.js"; diff --git a/src/api/resources/workflows/client/Client.ts b/src/api/resources/workflows/client/Client.ts new file mode 100644 index 0000000..be17955 --- /dev/null +++ b/src/api/resources/workflows/client/Client.ts @@ -0,0 +1,278 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as environments from "../../../../environments.js"; +import * as core from "../../../../core/index.js"; +import * as Pipedream from "../../../index.js"; +import { mergeHeaders, mergeOnlyDefinedHeaders } from "../../../../core/headers.js"; +import * as errors from "../../../../errors/index.js"; + +export declare namespace Workflows { + export interface Options { + environment?: core.Supplier; + /** Specify a custom URL to connect the client to. */ + baseUrl?: core.Supplier; + projectId: string; + token?: core.Supplier; + /** Override the x-pd-environment header */ + projectEnvironment?: core.Supplier; + /** Additional headers to include in requests. */ + headers?: Record | undefined>; + /** Base domain for workflows. Used for custom domains. */ + workflowDomain?: string; + } + + export interface RequestOptions { + /** The maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** The number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A hook to abort the request. */ + abortSignal?: AbortSignal; + /** Override the x-pd-environment header */ + projectEnvironment?: Pipedream.ProjectEnvironment | undefined; + /** Additional query string parameters to include in the request. */ + queryParams?: Record; + /** Additional headers to include in the request. */ + headers?: Record | undefined>; + } +} + +export class Workflows { + protected readonly _options: Workflows.Options; + private readonly workflowDomain: string; + + constructor(_options: Workflows.Options) { + this._options = _options; + this.workflowDomain = _options.workflowDomain ?? this._defaultWorkflowDomain; + } + + private get _defaultWorkflowDomain(): string { + return this._options.environment !== environments.PipedreamEnvironment.Prod && + this._options.environment !== environments.PipedreamEnvironment.Canary + ? "m.d.pipedream.net" + : "m.pipedream.net"; + } + + private get _urlProtocol(): string { + return this._options.environment !== environments.PipedreamEnvironment.Prod && + this._options.environment !== environments.PipedreamEnvironment.Canary + ? "http" + : "https"; + } + + /** + * Invokes a workflow using the URL of its HTTP interface(s), by sending an + * HTTP request. + * + * @param {Pipedream.InvokeWorkflowOpts} request + * @param {Pipedream.HTTPAuthType} authType - The type of authorization to use + * for the request (defaults to None). + * @param {Workflows.RequestOptions} requestOptions - Request-specific + * configuration. + * + * @example + * await client.workflows.invoke({ + * urlOrEndpoint: "https://en-your-endpoint.m.pipedream.net", + * body: { + * foo: 123, + * bar: "abc", + * baz: null + * }, + * headers: { + * "Accept": "application/json" + * } + * }, Pipedream.HTTPAuthType.OAuth) + */ + public invoke( + request: Pipedream.InvokeWorkflowOpts, + authType: Pipedream.HTTPAuthType = Pipedream.HTTPAuthType.None, + requestOptions?: Workflows.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__invoke(request, authType, requestOptions)); + } + + private async __invoke( + request: Pipedream.InvokeWorkflowOpts, + authType: Pipedream.HTTPAuthType = Pipedream.HTTPAuthType.None, + requestOptions?: Workflows.RequestOptions, + ): Promise> { + const { urlOrEndpoint, body, method = "POST", headers = {} } = request; + + const url = this._buildWorkflowUrl(urlOrEndpoint); + + let authHeader: string | undefined; + switch (authType) { + case Pipedream.HTTPAuthType.StaticBearer: + // It's expected that users will pass their own Authorization header in + // the static bearer case + authHeader = headers["Authorization"]; + break; + case Pipedream.HTTPAuthType.OAuth: + authHeader = await this._getAuthorizationHeader(); + break; + default: + break; + } + + const _response = await core.fetcher({ + url, + method: method.toUpperCase(), + headers: mergeHeaders( + this._options?.headers, + mergeOnlyDefinedHeaders({ + Authorization: authHeader, + "x-pd-environment": requestOptions?.projectEnvironment, + }), + headers, + requestOptions?.headers, + ), + contentType: body != null ? "application/json" : undefined, + queryParameters: requestOptions?.queryParams, + requestType: body != null ? "json" : undefined, + body, + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + maxRetries: requestOptions?.maxRetries, + abortSignal: requestOptions?.abortSignal, + }); + + if (!_response.ok) { + throw new errors.PipedreamError({ + message: _response.error.reason, + statusCode: _response.rawResponse.status, + rawResponse: _response.rawResponse, + }); + } + + return { + data: _response.rawResponse, + rawResponse: _response.rawResponse, + }; + } + + /** + * Invokes a workflow for a Pipedream Connect user in a project. + * + * @param {Pipedream.InvokeWorkflowForExternalUserOpts} request + * @param {Workflows.RequestOptions} requestOptions - Request-specific + * configuration. + * + * @example + * await client.workflows.invokeForExternalUser({ + * urlOrEndpoint: "https://your-workflow-url.m.pipedream.net", + * externalUserId: "your-external-user-id", + * body: { + * foo: 123, + * bar: "abc", + * baz: null + * }, + * headers: { + * "Accept": "application/json" + * } + * }) + */ + public invokeForExternalUser( + request: Pipedream.InvokeWorkflowForExternalUserOpts, + requestOptions?: Workflows.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__invokeForExternalUser(request, requestOptions)); + } + + private async __invokeForExternalUser( + request: Pipedream.InvokeWorkflowForExternalUserOpts, + requestOptions?: Workflows.RequestOptions, + ): Promise> { + const { urlOrEndpoint, externalUserId, body, method, headers = {} } = request; + + if (!externalUserId?.trim()) { + throw new Error("External user ID is required"); + } + + if (!urlOrEndpoint.trim()) { + throw new Error("Workflow URL or endpoint ID is required"); + } + + const authHeader = await this._getAuthorizationHeader(); + if (!authHeader) { + throw new Error( + "OAuth or token is required for invoking workflows for external users. Please pass credentials for a valid OAuth client", + ); + } + + // Delegate to invoke method with external user ID header and OAuth auth + return this.__invoke( + { + urlOrEndpoint, + body, + method, + headers: { + ...headers, + "X-PD-External-User-ID": externalUserId, + }, + }, + Pipedream.HTTPAuthType.OAuth, + requestOptions, + ); + } + + /** + * Builds a full workflow URL based on the input. + * + * @param input - Either a full URL (with or without protocol) or just an + * endpoint ID. + * @returns The fully constructed URL. + * @throws If the input is a malformed URL, throws an error with a clear + * message. + */ + private _buildWorkflowUrl(input: string): string { + const sanitizedInput = input + .trim() + .replace(/[^\w-./:]/g, "") + .toLowerCase(); + if (!sanitizedInput) { + throw new Error("URL or endpoint ID is required"); + } + + let url: string; + const isUrl = sanitizedInput.includes(".") || sanitizedInput.startsWith("http"); + + if (isUrl) { + // Try to parse the input as a URL + let parsedUrl: URL; + try { + const urlString = sanitizedInput.startsWith("http") ? sanitizedInput : `https://${sanitizedInput}`; + parsedUrl = new URL(urlString); + } catch { + throw new Error(`The provided URL is malformed: "${sanitizedInput}". Please provide a valid URL.`); + } + + // Validate the hostname to prevent potential DNS rebinding attacks + if (!parsedUrl.hostname.endsWith(this.workflowDomain)) { + throw new Error(`Invalid workflow domain. URL must end with ${this.workflowDomain}`); + } + + url = parsedUrl.href; + } else { + // If the input is an ID, construct the full URL using the base domain + if (!/^e(n|o)[a-z0-9-]+$/i.test(sanitizedInput)) { + throw new Error( + `Invalid endpoint ID format. Must contain only letters, numbers, and hyphens, and start with either "en" or "eo".`, + ); + } + + url = `${this._urlProtocol}://${sanitizedInput}.${this.workflowDomain}`; + } + + return url; + } + + protected async _getAuthorizationHeader(): Promise { + const bearer = await core.Supplier.get(this._options.token); + if (bearer != null) { + return `Bearer ${bearer}`; + } + + return undefined; + } +} diff --git a/src/api/resources/workflows/client/index.ts b/src/api/resources/workflows/client/index.ts new file mode 100644 index 0000000..10df5b2 --- /dev/null +++ b/src/api/resources/workflows/client/index.ts @@ -0,0 +1,2 @@ +export * from "./Client.js"; +export * from "./requests/index.js"; diff --git a/src/api/resources/workflows/client/requests/InvokeWorkflowForExternalUserOpts.ts b/src/api/resources/workflows/client/requests/InvokeWorkflowForExternalUserOpts.ts new file mode 100644 index 0000000..c05b417 --- /dev/null +++ b/src/api/resources/workflows/client/requests/InvokeWorkflowForExternalUserOpts.ts @@ -0,0 +1,26 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface InvokeWorkflowForExternalUserOpts { + /** + * The URL of the workflow's HTTP interface, or the ID of the endpoint. + */ + urlOrEndpoint: string; + /** + * Your end user ID, for whom you're invoking the workflow. + */ + externalUserId: string; + /** + * The body of the request. It must be a JSON-serializable value (e.g. an object, null, a string, etc.). + */ + body?: unknown; + /** + * HTTP method to use for the request (defaults to POST if not specified). + */ + method?: string; + /** + * Additional headers to include in the request. + */ + headers?: Record; +} diff --git a/src/api/resources/workflows/client/requests/InvokeWorkflowOpts.ts b/src/api/resources/workflows/client/requests/InvokeWorkflowOpts.ts new file mode 100644 index 0000000..570df77 --- /dev/null +++ b/src/api/resources/workflows/client/requests/InvokeWorkflowOpts.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface InvokeWorkflowOpts { + /** + * The URL of the workflow's HTTP interface, or the ID of the endpoint. + */ + urlOrEndpoint: string; + /** + * The body of the request. It must be a JSON-serializable value (e.g. an object, null, a string, etc.). + */ + body?: unknown; + /** + * HTTP method to use for the request (defaults to POST if not specified). + */ + method?: string; + /** + * Additional headers to include in the request. + */ + headers?: Record; +} diff --git a/src/api/resources/workflows/client/requests/index.ts b/src/api/resources/workflows/client/requests/index.ts new file mode 100644 index 0000000..99a5d0a --- /dev/null +++ b/src/api/resources/workflows/client/requests/index.ts @@ -0,0 +1,2 @@ +export { type InvokeWorkflowOpts } from "./InvokeWorkflowOpts.js"; +export { type InvokeWorkflowForExternalUserOpts } from "./InvokeWorkflowForExternalUserOpts.js"; diff --git a/src/api/resources/workflows/index.ts b/src/api/resources/workflows/index.ts new file mode 100644 index 0000000..914b8c3 --- /dev/null +++ b/src/api/resources/workflows/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/src/api/types/HTTPAuthType.ts b/src/api/types/HTTPAuthType.ts new file mode 100644 index 0000000..5cf0500 --- /dev/null +++ b/src/api/types/HTTPAuthType.ts @@ -0,0 +1,12 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +/** + * Different ways in which customers can authorize requests to HTTP endpoints + */ +export enum HTTPAuthType { + None = "none", + StaticBearer = "static_bearer_token", + OAuth = "oauth", +} diff --git a/src/api/types/index.ts b/src/api/types/index.ts index ec4f2ae..94e5645 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -22,6 +22,7 @@ export * from "./CreateBrowserClientOpts.js"; export * from "./CreateOAuthTokenResponse.js"; export * from "./CreateTokenResponse.js"; export * from "./DeleteTriggerOpts.js"; +export * from "./HTTPAuthType.js"; export * from "./DeployedComponent.js"; export * from "./DeployTriggerResponse.js"; export * from "./EmittedEvent.js"; diff --git a/src/wrapper/Pipedream.ts b/src/wrapper/Pipedream.ts index ad4d580..0623217 100644 --- a/src/wrapper/Pipedream.ts +++ b/src/wrapper/Pipedream.ts @@ -1,4 +1,5 @@ import { ProjectEnvironment } from "../api/index.js"; +import { Workflows } from "../api/resources/workflows/client/Client.js"; import { PipedreamClient } from "../Client.js"; import * as environments from "../environments.js"; @@ -8,6 +9,7 @@ export interface BackendOpts { environment?: environments.PipedreamEnvironment; projectEnvironment?: ProjectEnvironment; projectId: string; + workflowDomain?: string; } function expandEnvVars(template: string) { @@ -15,6 +17,9 @@ function expandEnvVars(template: string) { } export class Pipedream extends PipedreamClient { + private _workflowDomain?: string; + private _workflows: Workflows | undefined; + public constructor(opts: BackendOpts) { const { clientId = process.env.PIPEDREAM_CLIENT_ID, @@ -22,6 +27,7 @@ export class Pipedream extends PipedreamClient { environment: rawEnvironment = environments.PipedreamEnvironment.Prod, projectEnvironment = process.env.PIPEDREAM_PROJECT_ENVIRONMENT ?? "production", projectId = process.env.PIPEDREAM_PROJECT_ID, + workflowDomain, } = opts; if (!projectEnvironment) { throw new Error("Project environment cannot be empty"); @@ -42,5 +48,15 @@ export class Pipedream extends PipedreamClient { projectEnvironment, projectId, }); + + this._workflowDomain = workflowDomain; + } + + public get workflows(): Workflows { + return (this._workflows ??= new Workflows({ + ...this._options, + token: async () => await this._oauthTokenProvider.getToken(), + workflowDomain: this._workflowDomain, + })); } }