diff --git a/docs-v2/pages/_meta.json b/docs-v2/pages/_meta.json index ed932f71a75f6..b3b881ae820c2 100644 --- a/docs-v2/pages/_meta.json +++ b/docs-v2/pages/_meta.json @@ -10,8 +10,7 @@ "connected-accounts": "Connected Accounts", "apps": "Integrations", "connect": { - "title": "Pipedream Connect", - "display": "children" + "title": "Pipedream Connect" }, "components": "Components", "sources": "Sources", diff --git a/docs-v2/pages/connect/_meta.json b/docs-v2/pages/connect/_meta.json index d0fca12d120ce..00fcebe5381c7 100644 --- a/docs-v2/pages/connect/_meta.json +++ b/docs-v2/pages/connect/_meta.json @@ -1,10 +1,14 @@ { "index": { - "title": "Pipedream Connect", - "display": "hidden" + "title": "Overview" }, - "reference": { - "title": "API Reference", - "display": "hidden" + "use-cases": { + "title": "Use cases" + }, + "quickstart": { + "title": "Quickstart" + }, + "api": { + "title": "API Reference" } } diff --git a/docs-v2/pages/connect/api.mdx b/docs-v2/pages/connect/api.mdx new file mode 100644 index 0000000000000..7a61079803c70 --- /dev/null +++ b/docs-v2/pages/connect/api.mdx @@ -0,0 +1,384 @@ +import Callout from '@/components/Callout' + +# Connect API + + +Note that both the Base URL and authentication method for the Connect REST API are different from the rest of Pipedream's REST API. + + +**Base URL for all Connect requests** +``` +https://api.pipedream.com/v1/connect +``` + +**Authentication** + +You authenticate to the Connect REST API using **Basic Auth**. Send your project public key as the username and the project secret key as the password. When you make API requests, pass an +`Authorization` header of the following format: + +``` +Authorization: Basic base_64(public_key:secret_key) +``` + +Clients like `cURL` will often make this easy. For example, here's how you list all accounts on a project: + +```shell +curl 'https://api.pipedream.com/v1/connect/accounts' -u public_key:secret_key +``` + +### API Reference + +#### List all accounts + +List all connected accounts for all end users within your project + + +This endpoint is not currently paginated, so we'll attempt to return all connected accounts for all users within your project. We intend to add pagination soon. + + +``` +GET /accounts/ +``` + +##### Example response + +```json +{ + "data": { + "accounts": [ + { + "id": "apn_XehyZPr", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OkrhR1", + "name": "Slack" + }, + "created_at": "2024-07-30T22:52:48.000Z", + "updated_at": "2024-08-01T03:44:17.000Z" + }, + { + "id": "apn_b6h9QDK", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OrZhaO", + "name": "GitHub" + }, + "created_at": "2024-07-31T02:49:18.000Z", + "updated_at": "2024-08-01T03:58:17.000Z" + }, + { + "id": "apn_0WhJYxv", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OrZhaO", + "name": "GitHub" + }, + "created_at": "2024-07-31T20:28:16.000Z", + "updated_at": "2024-08-01T03:47:30.000Z" + }, + { + "id": "apn_kVh9PJx", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_OrZhaO", + "name": "GitHub" + }, + "created_at": "2024-07-31T21:17:03.000Z", + "updated_at": "2024-08-01T03:43:23.000Z" + }, + { + "id": "apn_WYhMlrz", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_XBxhAl", + "name": "Airtable" + }, + "created_at": "2024-08-01T04:04:03.000Z", + "updated_at": "2024-08-01T04:04:03.000Z" + } + ] + } +} +``` + +#### List all accounts for an app + +``` +GET /apps/:app_id/accounts +``` + +- `:app_id` can be `oauth_app_id` for [OAuth apps](/connect/quickstart#creating-a-custom-oauth-client) or [name slug](/connect/quickstart/#find-your-apps-name-slug) for key-based apps + +##### Example response + +```json +[ + { + "id": "apn_WYhMlrz", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "oa_aw4ib2", + "name_slug": "airtable_oauth", + "name": "Airtable", + "auth_type": "oauth", + "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", + "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", + "custom_fields_json": "[]", + "categories": ["Productivity"] + }, + "created_at": "2024-08-01T04:04:03.000Z", + "updated_at": "2024-08-01T04:04:03.000Z" + } +] +``` + +#### Retrieve account details + +Retrieve the account details for a specific account based on the account ID + +``` +GET /accounts/:account_id +``` +- `:account_id` is the ID of the account you want to retrieve +- Optionally include `?include_credentials=1` as a query-string parameter to include the account credentials in the response + +##### Example response (without account credentials) + +```json +{ + "data": { + "id": "apn_WYhMlrz", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "oa_aw4ib2", + "name_slug": "airtable_oauth", + "name": "Airtable", + "auth_type": "oauth", + "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", + "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", + "custom_fields_json": "[]", + "categories": ["Productivity"] + }, + "created_at": "2024-08-01T04:04:03.000Z", + "updated_at": "2024-08-01T04:04:03.000Z" + } +} +``` + +##### Example Response (with `include_credentials=1`) + +```json +{ + "data": { + "id": "apn_WYhMlrz", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "app_XBxhAl", + "name": "Airtable" + }, + "created_at": "2024-08-01T04:04:03.000Z", + "updated_at": "2024-08-01T04:04:03.000Z", + "credentials": { + "oauth_client_id": "dd7a26ca-ba11-4f80-8667-xxxxxxxx", + "oauth_access_token": "oaaLa2Ob1umiregWa.v1.xxxxxxxx.xxxxxxxx", + "oauth_refresh_token": "oaaLa2Ob1umiregWa.v1.refresh.xxxxxxxx", + "oauth_uid": "usrnbIhrxxxxxxxx" + }, + "expires_at": "2024-08-01T05:04:03.000Z", + "project_id": 279440, + "user_id": "danny", + "error": null, + "last_refreshed_at": null, + "next_refresh_at": "2024-08-01T04:17:33.000Z" + } +} +``` + +#### Retrieve account details for an external user + +Retrieve the account details for a specific account based on the external user ID + +``` +GET /users/:external_id/accounts +``` +- `:external_id` is the end user's ID in the Connect Partner's system +- Optionally include `?include_credentials=1` as a query-string parameter to include the account credentials in the response + +##### Example response (without account credentials) + +```json +[ + { + "id": "apn_WYhM5ov", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "oa_aw4ib2", + "name_slug": "airtable_oauth", + "name": "Airtable", + "auth_type": "oauth", + "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", + "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", + "custom_fields_json": "[]", + "categories": [ + "Productivity" + ] + }, + "created_at": "2024-08-06T21:51:30.000Z", + "updated_at": "2024-08-06T21:51:30.000Z", + "expires_at": "2024-08-06T22:51:30.000Z", + "error": null, + "last_refreshed_at": null, + "next_refresh_at": "2024-08-06T22:04:41.000Z" + }, + { + "id": "apn_KAh7JwW", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "oa_aPXiQd", + "name_slug": "github", + "name": "GitHub", + "auth_type": "oauth", + "description": "Where the world builds software. Millions of developers and companies build, ship, and maintain their software on GitHub—the largest and most advanced development platform in the world.", + "img_src": "https://assets.pipedream.net/s.v0/app_OrZhaO/logo/orig", + "custom_fields_json": "[]", + "categories": [ + "Developer Tools" + ] + }, + "created_at": "2024-08-06T21:53:05.000Z", + "updated_at": "2024-08-06T21:53:05.000Z", + "expires_at": null, + "error": null, + "last_refreshed_at": null, + "next_refresh_at": "2024-08-06T22:50:01.000Z" + } +] +``` + +##### Example Response (with `include_credentials=1`) + +```json +[ + { + "id": "apn_WYhM5ov", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "oa_aw4ib2", + "name_slug": "airtable_oauth", + "name": "Airtable", + "auth_type": "oauth", + "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", + "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", + "custom_fields_json": "[]", + "categories": [ + "Productivity" + ] + }, + "created_at": "2024-08-06T21:51:30.000Z", + "updated_at": "2024-08-06T21:51:30.000Z", + "credentials": { + "oauth_client_id": "dd7a26ca-ba11-4f80-8667-xxxxxxxx", + "oauth_access_token": "oaaLa2Ob1umiregWa.v1.xxxxxxxx.xxxxxxxx", + "oauth_refresh_token": "oaaLa2Ob1umiregWa.v1.refresh.xxxxxxxx", + "oauth_uid": "usrnbIhrxxxxxxxx" + }, + "expires_at": "2024-08-06T22:51:30.000Z", + "error": null, + "last_refreshed_at": null, + "next_refresh_at": "2024-08-06T22:04:41.000Z" + }, + { + "id": "apn_KAh7JwW", + "name": null, + "external_id": "user-abc-123", + "healthy": true, + "dead": false, + "app": { + "id": "oa_aPXiQd", + "name_slug": "github", + "name": "GitHub", + "auth_type": "oauth", + "description": "Where the world builds software. Millions of developers and companies build, ship, and maintain their software on GitHub—the largest and most advanced development platform in the world.", + "img_src": "https://assets.pipedream.net/s.v0/app_OrZhaO/logo/orig", + "custom_fields_json": "[]", + "categories": [ + "Developer Tools" + ] + }, + "created_at": "2024-08-06T21:53:05.000Z", + "updated_at": "2024-08-06T21:53:05.000Z", + "credentials": { + "oauth_client_id": "57dc28xxxxxxxxxxxxx", + "oauth_access_token": "gho_xxxxxxxxxxxxxxxxxx", + "oauth_uid": "104484339" + }, + "expires_at": null, + "error": null, + "last_refreshed_at": null, + "next_refresh_at": "2024-08-06T22:50:01.000Z" + } +] +``` + +#### Delete connected account +Delete a specific connected account for an end user + +``` +DELETE /v1/connect/accounts/:account_id +``` + +- `:account_id` is the ID of the account you want to retrieve +- We'll return a `204 No Content` response if the account was successfully deleted + +#### Delete all connected accounts for an app +Delete all connected accounts for a specific app + +``` +DELETE /apps/:app_id/accounts +``` + +- `:app_id` can be `oauth_app_id` for [OAuth apps](/connect/quickstart#creating-a-custom-oauth-client) or [name slug](/connect/quickstart/#find-your-apps-name-slug) for key-based apps +- We'll return a `204 No Content` response if the accounts were successfully deleted + +#### Delete an end user +Delete an end user and all their connected accounts + +``` +DELETE /users/:external_id +``` + +- `external_id` refers to the end user's ID in your system — whatever unique identifer you assign to your customers. +- We'll return a `204 No Content` response if the user was successfully deleted \ No newline at end of file diff --git a/docs-v2/pages/connect/index.mdx b/docs-v2/pages/connect/index.mdx index 9d57857848cbd..26e6636b260fb 100644 --- a/docs-v2/pages/connect/index.mdx +++ b/docs-v2/pages/connect/index.mdx @@ -1,461 +1,81 @@ import Callout from '@/components/Callout' -import { Steps } from 'nextra/components' +import { Steps, Tabs } from 'nextra/components' +import Image from 'next/image' # Pipedream Connect -## Overview - -Pipedream Connect is the easiest way for your users to connect to any API, **right in your product**. - -Connect enables you to easily integrate more than {process.env.PUBLIC_APPS}+ APIs directly into your application, letting you make authenticated requests on behalf of your end users, while not having to handle authorization grants or refresh tokens yourself, and it includes these features: -1. [Client SDK](https://github.com/PipedreamHQ/pipedream/tree/master/packages/sdk) to initiate authorization for any one of the [{process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps) available on Pipedream -2. [REST API](#rest-api-overview) to retrieve credentials (access token, API key, etc.) for your end users -3. The Pipedream platform, which includes [a serverless runtime](/), [thousands of pre-built source-available triggers and actions](https://github.com/PipedreamHQ/pipedream/tree/master/components), and a first-class developer experience to build, test, and deploy custom workflows - - - -**Pipedream Connect is currently in preview and we're looking for feedback!** - -**While in the preview phase, the API may change without notice. We'll do our best to communicate any changes, but please be aware that breaking changes may occur.** - -Please [let us know](mailto:connect@pipedream.com) how you're using it, what's not working, and what else you'd like to see! - - -## Glossary of terms - -- **App**: GitHub, Notion, Slack, Google Sheets, etc. — the API you want to make requests to on behalf of your end user. See the [full list here](https://pipedream.com/apps). -- **Connect Partner**: This is probably you, the Pipedream customer who is developing an application and wants to use Connect to make authenticated requests on behalf of their end users. -- **Connected Account**: Read more about connected accounts [here](/connected-accounts) — this is the account connection for your end user, and is also referred to as **auth provision**. -- **End User**: The customer of the Connect Partner and whose data you want to access on their behalf (`external_id` in the API). -- **OAuth Client**: The OAuth client you create in Pipedream to connect to an app. Read more about OAuth clients [here](/connected-accounts/oauth-clients). - -## Known limitations -- The only apps that work with Pipedream Connect today are apps that use OAuth 2.0 to authenticate, and that do not require any user-input fields during the OAuth flow. We're working on extending support to all integrated apps on Pipedream. -- There is very limited error handling today, but this is top of mind for us and is on the near-term roadmap. If you or your end users encounter any issues, please reach out to us at [connect@pipedream.com](mailto:connect@pipedream.com). -- This is the first iteration of the API and developer experience. We expect both to evolve as we get feedback from you and other developers. - -## Getting started - - - -### Enable the feature flag -- Enable the feature flag for **Pipedream Connect** in your [account settings here](https://pipedream.com/settings/alpha). - -Please reach out to our team at [connect@pipedream.com](mailto:connect@pipedream.com) to activate your account for Connect once you've enabled the feature flag and send us your [workspace ID](https://pipedream.com/settings/account). - - -### Verify your application's domain -- To integrate the Pipedream SDK on your site, you'll need to verify ownership of your web domain. [Check out the docs](workspaces/domain-verification#pipedream-connect) to get started. - -### Configure an OAuth client in Pipedream -- To use Pipedream Connect, you'll need to create an OAuth client in Pipedream for whatever app you want to integrate. [Check out the docs](/connected-accounts/oauth-clients#configuring-custom-oauth-clients) to get started. -- Make sure to check the box to **Allow external user connections** in the OAuth client configuration in Pipedream - -### Open your Pipedream project -- Open an existing Pipedream project or create a new one: [https://pipedream.com/projects](https://pipedream.com/projects) -- Note your **Project Public Key** and **Project Secret Key** from the **Connect** tab within your project — you'll need these when configuring the SDK and making API requests - -### Run the example app -- Install the [SDK](https://www.npmjs.com/package/@pipedream/sdk) via npm: `npm install @pipedream/sdk` -- The code for the Pipedream SDK lives in the [public pipedream repo](https://github.com/PipedreamHQ/pipedream/tree/master/packages/sdk) -- Check out the example app and README there to get started - -### Retrieve your user credentials from Pipedream's API -- Check out to the API reference below to retrieve your users' credentials from Pipedream's API after they've connected their account. - - +**Pipedream Connect is currently in preview, and we're looking for feedback!** -## Retrieving account details and user credentials -After your end users have connected their account using Pipedream Connect, you can retrieve their credentials using Pipedream's REST API, which enables you to make authenticated requests on their behalf. +**In the preview phase, the API may change without notice. We'll do our best to communicate changes, but please be aware that breaking changes may occur.** -### REST API Overview - -Note that both the Base URL and authentication method for the Connect REST API are different from the rest of Pipedream's REST API. +Please reach out at `connect@pipedream.com` or our [Slack community](https://pipedream.com/support) to let us know how you're using it, what's not working, and what else you'd like to see. -**Base URL for all Connect requests** -``` -https://api.pipedream.com/v1/connect -``` +Pipedream Connect is the easiest way for your users to connect to [over {process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps), **right in your product**. You can build in-app messaging, CRM syncs, AI-driven products, [and much more](/connect/use-cases), all in a few minutes. Visit [the quickstart](/connect/quickstart) to build your first integration. -**Authentication** +Connect lets your users authorize access to any API, directly in your app. You can then retrieve fresh credentials for any account, making requests on their behalf. Pipedream handles the security of credentials and the whole OAuth flow — **no need to manage authorization grants or token refresh yourself.** -You authenticate to the Connect REST API using **Basic Auth**. Send your Pipedream [Project Public Key]() as the username and the [Project Secret Key]() as the password. When you make API requests, pass an -`Authorization` header of the following format: +You have full, code-level control over how these integrations work. You handle the product, Pipedream takes care of the auth. Connect provides: -``` -Authorization: Basic -``` +1. A [Client SDK](https://github.com/PipedreamHQ/pipedream/tree/master/packages/sdk) to initiate authorization or accept API keys on behalf of your users, for any of the [{process.env.PUBLIC_APPS}+ APIs](https://pipedream.com/apps) available on Pipedream +2. A [REST API](/connect/api) to retrieve credentials for your end users — OAuth access tokens, API keys, and other credentials stored for them +3. The Pipedream platform and its [workflow builder](/workflows), [serverless runtime](/), and thousands of no-code [triggers](/workflows/triggers) and [actions](/workflows/actions) -For example, here's how you can use `cURL` to fetch a list of all accounts in your project: +
+Pipedream Connect overview -```shell -curl 'https://api.pipedream.com/v1/connect/accounts' \ - -H 'Authorization: Basic ' -``` +## Use cases -### API Reference +Pipedream Connect lets you build any API integration into your product in minutes. Our customers build: -#### List all accounts -List all connected accounts for all end users within your project +- **In-app messaging**: Send messages to Slack, Discord, Microsoft Teams, or any app directly from your product. +- **CRM syncs**: Sync data between your app and Salesforce, HubSpot, or any CRM +- **AI products**: Talk to any AI API or LLM, interacting with your users or running AI-driven asynchronous tasks +- **Spreadsheet integrations**: Sync data between your app and Google Sheets, Airtable, or any spreadsheet - -This endpoint is not currently paginated, so we'll attempt to return all connected accounts for all users within your project. We intend to add pagination soon. - - -``` -GET /accounts/ -``` - -##### Example response - -```json -{ - "data": { - "accounts": [ - { - "id": "apn_XehyZPr", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "app_OkrhR1", - "name": "Slack" - }, - "created_at": "2024-07-30T22:52:48.000Z", - "updated_at": "2024-08-01T03:44:17.000Z" - }, - { - "id": "apn_b6h9QDK", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "app_OrZhaO", - "name": "GitHub" - }, - "created_at": "2024-07-31T02:49:18.000Z", - "updated_at": "2024-08-01T03:58:17.000Z" - }, - { - "id": "apn_0WhJYxv", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "app_OrZhaO", - "name": "GitHub" - }, - "created_at": "2024-07-31T20:28:16.000Z", - "updated_at": "2024-08-01T03:47:30.000Z" - }, - { - "id": "apn_kVh9PJx", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "app_OrZhaO", - "name": "GitHub" - }, - "created_at": "2024-07-31T21:17:03.000Z", - "updated_at": "2024-08-01T03:43:23.000Z" - }, - { - "id": "apn_WYhMlrz", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "app_XBxhAl", - "name": "Airtable" - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z" - } - ] - } -} -``` - -#### List all connected accounts for an app - -``` -GET /apps/:app_id/accounts -``` - -- `:app_id` can be `oauth_app_id` or `app_id` - -##### Example response - -```json -[ - { - "id": "apn_WYhMlrz", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "oa_aw4ib2", - "name_slug": "airtable_oauth", - "name": "Airtable", - "auth_type": "oauth", - "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", - "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", - "custom_fields_json": "[]", - "categories": ["Productivity"] - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z" - } -] -``` - -#### Retrieve account details for a specific auth provision - -Retrieve the account details for a specific account based on the auth provision - -``` -GET /accounts/:apn_id -``` -- `:apn_id` is the auth provision of the account you want to retrieve -- Optionally include `?include_credentials=1` as a query-string parameter to include the account credentials in the response +[and much more](/connect/use-cases). -##### Example response (without account credentials) - -```json -{ - "data": { - "id": "apn_WYhMlrz", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "oa_aw4ib2", - "name_slug": "airtable_oauth", - "name": "Airtable", - "auth_type": "oauth", - "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", - "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", - "custom_fields_json": "[]", - "categories": ["Productivity"] - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z" - } -} -``` - -##### Example Response (with `include_credentials=1`) - -```json -{ - "data": { - "id": "apn_WYhMlrz", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "app_XBxhAl", - "name": "Airtable" - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z", - "credentials": { - "oauth_client_id": "dd7a26ca-ba11-4f80-8667-xxxxxxxx", - "oauth_access_token": "oaaLa2Ob1umiregWa.v1.xxxxxxxx.xxxxxxxx", - "oauth_refresh_token": "oaaLa2Ob1umiregWa.v1.refresh.xxxxxxxx", - "oauth_uid": "usrnbIhrxxxxxxxx" - }, - "expires_at": "2024-08-01T05:04:03.000Z", - "project_id": 279440, - "user_id": "danny", - "error": null, - "last_refreshed_at": null, - "next_refresh_at": "2024-08-01T04:17:33.000Z" - } -} -``` - -#### Retrieve account details for a specific external user - -Retrieve the account details for a specific account based on the external user ID - -``` -GET /users/:external_id/accounts -``` -- `:external_id` is the end user's ID in the Connect Partner's system -- Optionally include `?include_credentials=1` as a query-string parameter to include the account credentials in the response - -##### Example response (without account credentials) +## Getting started -```json -[ - { - "id": "apn_WYhM5ov", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "oa_aw4ib2", - "name_slug": "airtable_oauth", - "name": "Airtable", - "auth_type": "oauth", - "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", - "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", - "custom_fields_json": "[]", - "categories": [ - "Productivity" - ] - }, - "created_at": "2024-08-06T21:51:30.000Z", - "updated_at": "2024-08-06T21:51:30.000Z", - "expires_at": "2024-08-06T22:51:30.000Z", - "error": null, - "last_refreshed_at": null, - "next_refresh_at": "2024-08-06T22:04:41.000Z" - }, - { - "id": "apn_KAh7JwW", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "oa_aPXiQd", - "name_slug": "github", - "name": "GitHub", - "auth_type": "oauth", - "description": "Where the world builds software. Millions of developers and companies build, ship, and maintain their software on GitHub—the largest and most advanced development platform in the world.", - "img_src": "https://assets.pipedream.net/s.v0/app_OrZhaO/logo/orig", - "custom_fields_json": "[]", - "categories": [ - "Developer Tools" - ] - }, - "created_at": "2024-08-06T21:53:05.000Z", - "updated_at": "2024-08-06T21:53:05.000Z", - "expires_at": null, - "error": null, - "last_refreshed_at": null, - "next_refresh_at": "2024-08-06T22:50:01.000Z" - } -] -``` +Visit [the quickstart](/connect/quickstart) to build your first integration. -##### Example Response (with `include_credentials=1`) +## Plans and pricing -```json -[ - { - "id": "apn_WYhM5ov", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "oa_aw4ib2", - "name_slug": "airtable_oauth", - "name": "Airtable", - "auth_type": "oauth", - "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", - "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", - "custom_fields_json": "[]", - "categories": [ - "Productivity" - ] - }, - "created_at": "2024-08-06T21:51:30.000Z", - "updated_at": "2024-08-06T21:51:30.000Z", - "credentials": { - "oauth_client_id": "dd7a26ca-ba11-4f80-8667-xxxxxxxx", - "oauth_access_token": "oaaLa2Ob1umiregWa.v1.xxxxxxxx.xxxxxxxx", - "oauth_refresh_token": "oaaLa2Ob1umiregWa.v1.refresh.xxxxxxxx", - "oauth_uid": "usrnbIhrxxxxxxxx" - }, - "expires_at": "2024-08-06T22:51:30.000Z", - "error": null, - "last_refreshed_at": null, - "next_refresh_at": "2024-08-06T22:04:41.000Z" - }, - { - "id": "apn_KAh7JwW", - "name": null, - "external_id": "user-abc-123", - "healthy": true, - "dead": false, - "app": { - "id": "oa_aPXiQd", - "name_slug": "github", - "name": "GitHub", - "auth_type": "oauth", - "description": "Where the world builds software. Millions of developers and companies build, ship, and maintain their software on GitHub—the largest and most advanced development platform in the world.", - "img_src": "https://assets.pipedream.net/s.v0/app_OrZhaO/logo/orig", - "custom_fields_json": "[]", - "categories": [ - "Developer Tools" - ] - }, - "created_at": "2024-08-06T21:53:05.000Z", - "updated_at": "2024-08-06T21:53:05.000Z", - "credentials": { - "oauth_client_id": "57dc28xxxxxxxxxxxxx", - "oauth_access_token": "gho_xxxxxxxxxxxxxxxxxx", - "oauth_uid": "104484339" - }, - "expires_at": null, - "error": null, - "last_refreshed_at": null, - "next_refresh_at": "2024-08-06T22:50:01.000Z" - } -] -``` +During the preview phase, **Connect is free to use for any workspace on a paid plan**. -#### Delete individual connected account -Delete a specific connected account for an end user +After the preview phase, Pipedream is likely to charge on the number of end users who have active accounts in your workspace's projects. Any prices will be clearly communicated, and you can delete unused end user accounts at any time. -``` -DELETE /v1/connect/accounts/:apn_id -``` +Please let us know if you have any feedback on the value of Connect and how you'd like to see it priced. -- `:apn_id` is the auth provision of the account you want to retrieve -- We'll return a `204 No Content` response if the account was successfully deleted +## Security -#### Delete all connected accounts for a specific app +Pipedream takes the security of our products seriously. Please [review our security docs](/privacy-and-security) and send us any questions or [suspected vulnerabilities](/privacy-and-security#reporting-a-vulnerability). You can also get a copy of our [SOC 2 Type 2 report](/privacy-and-security#soc-2), [sign HIPAA BAAs](/privacy-and-security#hipaa), and get information on other practices and controls. -``` -/DELETE /apps/:app_id/accounts -``` +### Storing user credentials, token refresh -- `:app_id` can be `oauth_app_id` or `app_id` -- We'll return a `204 No Content` response if the accounts were successfully deleted +All credentials and tokens are sent to Pipedream securely over HTTPS, and encrypted at rest. [See our security docs on credentials](/privacy-and-security#third-party-oauth-grants-api-keys-and-environment-variables) for more information. -#### Delete an end user and all their connected accounts +### How to secure your Connect apps -``` -DELETE /users/:external_id -``` +- **Secure all secrets** — Secure your Pipedream project's secret key and user credentials. Never expose secrets in your client-side code. Make all requests to Pipedream's API and third-party APIs from your server-side code. +- **Use HTTPS** — Always use HTTPS to secure your connections between your client and server. Requests to Pipedream's API will be automatically redirected to HTTPS. +- **Use secure, session-based auth between your client and server** — authorize all requests from your client to your server using a secure, session-based auth mechanism. Use well-known identity providers with services like [Clerk](https://clerk.com/), [Firebase](https://firebase.google.com/), or [Auth0](https://auth0.com/) to securely generate and validate authentication tokens. The same follows for Pipedream workflows — if you trigger Pipedream workflows from your client or server, validate all requests in the workflow before executing workflow code. +- **Secure your workflows** — See our [standard security practices](/privacy-and-security/best-practices) for recommendations on securing your Pipedream workflows. -- `:external_id` corresponds to the the end user's ID in your system -- We'll return a `204 No Content` response if the user was successfully deleted +## Glossary of terms -## Plans and pricing -- The core billing model will be based on the number of end users in your Pipedream workspace -- During the preview phase, Pipedream Connect is available to Advanced and Business customers at no additional cost, with a limited number of end users during the preview phase +- **App**: GitHub, Notion, Slack, Google Sheets, and more. The app is the API you want your users to connect to in your product. See the [full list here](https://pipedream.com/apps). +- **Developer**: This is probably you, the Pipedream customer who's developing an app and wants to use Connect to make API requests on behalf of your end users. +- **End User**: Your customer or user, whose data you want to access on their behalf. End users are identifed via the `external_id` param in the Connect SDK and API. +- **Connected Account**: The account your end user connects. [Read more about connected accounts](/connected-accounts). +- **OAuth Client**: Custom OAuth clients you create in Pipedream. [Read more about OAuth clients](/connected-accounts/oauth-clients). ## Product roadmap for Connect + - Address bugs and feedback during the preview phase -- Extend support to all integrated apps on Pipedream +- Simplify the developer experience and SDK integration +- Invoke Pipedream workflows on behalf of end users +- Support hosted UIs for connecting accounts — native support for mobile environments that can't execute JavaScript or load iframes. - Improve error handling for Connect developers and end users -- Expand the capabilities of the client SDK to directly invoke Pipedream workflows -- And more... \ No newline at end of file +- And more! \ No newline at end of file diff --git a/docs-v2/pages/connect/quickstart.mdx b/docs-v2/pages/connect/quickstart.mdx new file mode 100644 index 0000000000000..ddeca9cdd0bb0 --- /dev/null +++ b/docs-v2/pages/connect/quickstart.mdx @@ -0,0 +1,990 @@ +import Callout from '@/components/Callout' +import { Steps, Tabs } from 'nextra/components' +import Image from 'next/image' + +# Quickstart + +
+Connect developer flow + + + +### Enable the feature flag + +Enable the feature flag for **Pipedream Connect** in your [account settings](https://pipedream.com/settings/alpha). + +### Choose the apps you want to integrate + +There are two types of apps in Pipedream: + +1. **Key-based**: These apps require static credentials, like API keys. Pipedream stores these credentials securely and exposes them via API. +2. **OAuth**: These apps require OAuth authorization. Pipedream manages the OAuth flow for these apps, ensuring you always have a fresh access token for requests. + +Connecting Key-based apps with the Pipedream API requires only [the app's name slug](#find-your-apps-name-slug). OAuth apps require you to [create your own OAuth client](#creating-a-custom-oauth-client) and pass the OAuth app ID, as well. + +#### Find your app's name slug + +Typically, the name slug is the name of the app in lowercase, with spaces replaced by underscores. For example, the name slug for the GitHub app is `github`. The name slug for Google Sheets is `google_sheets`. + +To find the name slug for an app: + +1. Visit [https://pipedream.com/apps](https://pipedream.com/apps) +2. Search and select the app you want to integrate +3. In the **Authentication** section, copy the **Name slug**. + +
+Google Sheets name slug + +#### Creating a custom OAuth client + +First, [create an OAuth client](/connected-accounts/oauth-clients#configuring-custom-oauth-clients) for the app you'd like to integrate. + + +Make sure to check the **Enable for Pipedream Connect** setting in the OAuth client configuration in Pipedream to allow your end users to connect their accounts. By default, OAuth clients are accessible only to members of your Pipedream workspace. Checking this box allows your end users to create accounts for this app, too. + +
+Enable OAuth client for Pipedream Connect +
+ +Once that's done, copy the **OAuth App ID** from the app configuration. + +Copy your OAuth app ID + +### Get your project keys + +1. Open an existing Pipedream project or create a new one at [https://pipedream.com/projects](https://pipedream.com/projects). +2. Visit the **Connect** tab. +3. Find your project's **Public Key** and **Secret Key** + +You'll need these when configuring the SDK and making API requests. + +Project keys in the Connect tab + +### Run the Pipedream demo app, or configure your own + +You'll need to do two things to integrate Pipedream Connect into your app: + +1. [Connect to the Pipedream API from your server](#connect-to-the-pipedream-api-from-your-server). This lets you make secure calls to the Pipedream API to initiate the account connection flow and retrieve account credentials. If you're running a JavaScript framework like Node.js on the server, you can use the Pipedream SDK. +2. [Add the Pipedream SDK to your frontend](#add-the-pipedream-sdk-to-your-frontend). This lets you start the account connection flow for your end users. + +We'll walk through these steps below, using [an example Next.js app](https://github.com/PipedreamHQ/pipedream/tree/master/packages/sdk/examples/next-app/). To follow along, clone [the repo](https://github.com/PipedreamHQ/pipedream) and follow the instructions in [the app's `README`](https://github.com/PipedreamHQ/pipedream/tree/master/packages/sdk/examples/next-app/). That will run the app on `localhost:3000`. + +First, copy the `.env.example` file to `.env.local`: + +```bash +cp .env.example .env.local +``` + +and fill the `.env.local` file with your project and app details: + +```bash +# Specific to Next.js — see `app/page.tsx` for how these are used +NEXT_PUBLIC_PIPEDREAM_APP_SLUG=github +NEXT_PUBLIC_PIPEDREAM_APP_ID=oa_abc123 + +# Used by `app/server.ts` to authorize requests to the Pipedream API — see below +PIPEDREAM_PROJECT_PUBLIC_KEY=pub_abc123 +PIPEDREAM_PROJECT_SECRET_KEY=sec_abc123 +``` + +If you're building your own app, you'll need to provide these values to the environment, or retrieve them from your secrets store. + +### Connect to the Pipedream API from your server and create a token + + +**Why do I need to talk to the Pipedream API from my server?** + +You need to secure specific operations, for example: + +- You need to initiate the account connection flow for your end users. In Pipedream Connect, **you exchange your project keys for a short-lived token that allows a specific user to connect a specific app**, and return that token to your frontend. If you expose your Pipedream project keys directly to the frontend, anyone could initiate the account connection flow for any user, and you'd be charged for those accounts. +- You need to retrieve account credentials for your end users. Again, this should happen securely on your server, not in the frontend, to protect your users' data. + + + +In this Next.js example, we're running Node.js code on our server, via [Next server components](https://nextjs.org/docs/app/building-your-application/rendering/server-components), so we'll use the Pipedream SDK. Additional examples in Python, Ruby, and other languages are noted below. + +To install the [Pipedream SDK](https://www.npmjs.com/package/@pipedream/sdk) in your own project, run: + +```bash +npm i --save @pipedream/sdk +``` + +To create a short-lived token via TypeScript / JavaScript SDK, you'll need to create a Pipedream API client and call the `connectTokenCreate` method. In our example app, this code is in `app/server.ts`. + +In other languages, you'll need to make an HTTP POST request to the `/v1/connect/tokens` endpoint to create a token, then return the token to your frontend. Click into other tabs to see examples in additional languages. + + + +```typescript +import { + createClient, + type ConnectTokenCreateOpts, + type ConnectTokenResponse, +} from "@pipedream/sdk"; + +const pd = createClient({ + publicKey: process.env.PIPEDREAM_PROJECT_PUBLIC_KEY, + secretKey: process.env.PIPEDREAM_PROJECT_SECRET_KEY, +}); + +export async function serverConnectTokenCreate(opts: ConnectTokenCreateOpts): Promise { + return pd.connectTokenCreate(opts); +} +``` + + +```javascript +const fetch = require('node-fetch'); + +class Client { + constructor(opts) { + this.secretKey = opts.secretKey; + this.publicKey = opts.publicKey; + + const apiHost = 'api.pipedream.com'; + this.baseURL = `https://${apiHost}`; + } + + _authorizationHeader() { + const encoded = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString('base64'); + return `Basic ${encoded}`; + } + + async connectTokenCreate(opts) { + const auth = this._authorizationHeader(); + const response = await fetch(`${this.baseURL}/v1/connect/tokens`, { + method: 'POST', + headers: { + 'Authorization': auth, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts), + }); + + return response.json(); + } +} + +const client = new Client({ + publicKey: 'YOUR_PUBLIC_KEY', + secretKey: 'YOUR_SECRET_KEY', +}); + +const connectTokenOpts = { + app_slug: "YOUR_APP_SLUG", // The app's name slug + oauth_app_id: "o_abc123", // The OAuth app ID, if you're connecting an OAuth app — keep this in config / a DB, pass here + external_id: "USER_ID" // The end user's ID in your system +} + +// Expose this code as an API endpoint in your server to fetch the token from the frontend +client.connectTokenCreate(connectTokenOpts) + .then(response => { + // return the token to the frontend + }) + .catch(error => { + // handle errors + }); +``` + + +```python +import base64 +import json +import requests + +class Client: + def __init__(self, opts): + self.secret_key = opts['secret_key'] + self.public_key = opts['public_key'] + + api_host = 'api.pipedream.com' + self.base_url = f"https://{api_host}" + + def _authorization_header(self): + encoded = base64.b64encode(f"{self.public_key}:{self.secret_key}".encode()).decode() + return f"Basic {encoded}" + + def connect_token_create(self, opts): + auth = self._authorization_header() + response = requests.post( + f"{self.base_url}/v1/connect/tokens", + headers={ + "Authorization": auth, + "Content-Type": "application/json", + }, + data=json.dumps(opts) + ) + return response.json() + +# Usage example +client = Client({ + 'public_key': 'YOUR_PUBLIC_KEY', + 'secret_key': 'YOUR_SECRET_KEY', +}) + +connect_token_opts = { + 'app_slug': "YOUR_APP_SLUG", + 'oauth_app_id': "o_abc123", + 'external_id': "USER_ID" +} + +# Expose this code as an API endpoint in your server to fetch the token from the frontend +response = client.connect_token_create(connect_token_opts) + +``` + + +```java +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Base64; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public class Client { + private String secretKey; + private String publicKey; + private String baseURL; + + public Client(String secretKey, String publicKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + + String apiHost = "api.pipedream.com"; + this.baseURL = "https://" + apiHost; + } + + private String authorizationHeader() { + String encoded = Base64.getEncoder().encodeToString((publicKey + ":" + secretKey).getBytes(StandardCharsets.UTF_8)); + return "Basic " + encoded; + } + + public String connectTokenCreate(String appSlug, String oauthClientId, String externalId) throws Exception { + String auth = authorizationHeader(); + URL url = new URL(baseURL + "/v1/connect/tokens"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Authorization", auth); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + + String jsonInputString = String.format("{\"app_slug\":\"%s\",\"oauth_app_id\":\"%s\",\"external_id\":\"%s\"}", appSlug, oauthClientId, externalId); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + + return new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } + + public static void main(String[] args) throws Exception { + Client client = new Client("YOUR_SECRET_KEY", "YOUR_PUBLIC_KEY"); + + // Expose this code as an API endpoint in your server to fetch the token from the frontend + String response = client.connectTokenCreate("YOUR_APP_SLUG", "o_abc123", "USER_ID"); + } +} + +``` + + +```csharp +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +public class Client { + private string secretKey; + private string publicKey; + private string baseURL; + + public Client(string secretKey, string publicKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + + string apiHost = "api.pipedream.com"; + this.baseURL = $"https://{apiHost}"; + } + + private string AuthorizationHeader() { + string encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}")); + return $"Basic {encoded}"; + } + + public async Task ConnectTokenCreate(string appSlug, string oauthClientId, string externalId) { + string auth = AuthorizationHeader(); + using (HttpClient client = new HttpClient()) { + client.DefaultRequestHeaders.Add("Authorization", auth); + client.DefaultRequestHeaders.Add("Content-Type", "application/json"); + + var content = new StringContent($"{{\"app_slug\":\"{appSlug}\",\"oauth_app_id\":\"{oauthClientId}\",\"external_id\":\"{externalId}\"}}", Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{baseURL}/v1/connect/tokens", content); + + return await response.Content.ReadAsStringAsync(); + } + } + + public static async Task Main(string[] args) { + var client = new Client("YOUR_SECRET_KEY", "YOUR_PUBLIC_KEY"); + + // Expose this code as an API endpoint in your server to fetch the token from the frontend + string response = await client.ConnectTokenCreate("YOUR_APP_SLUG", "o_abc123", "USER_ID"); + } +} + +``` + + +```go +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +type Client struct { + SecretKey string + PublicKey string + BaseURL string +} + +func NewClient(secretKey, publicKey string) *Client { + apiHost := "api.pipedream.com" + baseURL := fmt.Sprintf("https://%s", apiHost) + + return &Client{ + PublicKey: publicKey, + SecretKey: secretKey, + } +} + +func (c *Client) authorizationHeader() string { + encoded := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.PublicKey, c.SecretKey))) + return fmt.Sprintf("Basic %s", encoded) +} + +func (c *Client) ConnectTokenCreate(appSlug, oauthClientId, externalId string) (map[string]interface{}, error) { + auth := c.authorizationHeader() + url := fmt.Sprintf("%s/v1/connect/tokens", c.BaseURL) + + opts := map[string]string{ + "app_slug": appSlug, + "oauth_app_id": oauthClientId, + "external_id": externalId, + } + + jsonData, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", auth) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + var result map[string]interface{} + json.Unmarshal(body, &result) + return result, nil +} + +func main() { + client := NewClient("YOUR_SECRET_KEY", "YOUR_PUBLIC_KEY") + + // Expose this code as an API endpoint in your server to fetch the token from the frontend + response, err := client.ConnectTokenCreate("YOUR_APP_SLUG", "o_abc123", "USER_ID") + if err != nil { + fmt.Println("Error:", err) + return + } +} + +``` + + +```php +publicKey = $publicKey; + $this->secretKey = $secretKey; + + $apiHost = 'api.pipedream.com'; + $this->baseURL = "https://$apiHost"; + } + + private function authorizationHeader() { + $encoded = base64_encode("$this->publicKey:$this->secretKey"); + return "Basic $encoded"; + } + + public function connectTokenCreate($appSlug, $oauthClientId, $externalId) { + $auth = $this->authorizationHeader(); + $url = "$this->baseURL/v1/connect/tokens"; + + $data = json_encode([ + 'app_slug' => $appSlug, + 'oauth_app_id' => $oauthClientId, + 'external_id' => $externalId + ]); + + $options = [ + 'http' => [ + 'header' => [ + "Authorization: $auth", + "Content-Type: application/json", + ], + 'method' => 'POST', + 'content' => $data, + ], + ]; + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + return json_decode($result, true); + } +} + +// Usage example +$client = new Client('YOUR_SECRET_KEY', 'YOUR_PUBLIC_KEY'); + +$connectTokenOpts = [ + 'app_slug' => "YOUR_APP_SLUG", + 'oauth_app_id' => "o_abc123", + 'external_id' => "USER_ID" +]; + +// Expose this code as an API endpoint in your server to fetch the token from the frontend +$response = $client->connectTokenCreate($connectTokenOpts['app_slug'], $connectTokenOpts['oauth_app_id'], $connectTokenOpts['external_id']); +?> +``` + + +```ruby +require 'base64' +require 'json' +require 'net/http' +require 'uri' + +class Client + def initialize(secret_key, public_key) + @public_key = public_key + @secret_key = secret_key + + api_host = 'api.pipedream.com' + @base_url = "https://#{api_host}" + end + + def authorization_header + encoded = Base64.strict_encode64("#{@public_key}:#{@secret_key}") + "Basic #{encoded}" + end + + def connect_token_create(app_slug, oauth_app_id, external_id) + uri = URI("#{@base_url}/v1/connect/tokens") + req = Net::HTTP::Post.new(uri) + req['Authorization'] = authorization_header + req['Content-Type'] = 'application/json' + req.body = { app_slug: app_slug, oauth_app_id: oauth_app_id, external_id: external_id }.to_json + + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(req) + end + + JSON.parse(res.body) + end +end + +client = Client.new('YOUR_SECRET_KEY', 'YOUR_PUBLIC_KEY') + +connect_token_opts = { + app_slug: "YOUR_APP_SLUG", + oauth_app_id: "o_abc123", + external_id: "USER_ID" +} + +# Expose this code as an API endpoint in your server to fetch the token from the frontend +response = client.connect_token_create(connect_token_opts[:app_slug], connect_token_opts[:oauth_app_id], connect_token_opts[:external_id]) +``` + + + +In our Next.js app, we call the `serverConnectTokenCreate` method from the frontend to retrieve a token **for a specific user, and a specific app**. + +```typescript +import { serverConnectTokenCreate } from "./server" + +const { token, expires_at } = await serverConnectTokenCreate({ + app_slug: appSlug, // The app's name slug, passed from the frontend + oauth_app_id: oauthAppId, // The OAuth app ID, if you're connecting an OAuth app — keep this in config / a DB, pass here + external_id: externalUserId // The end user's ID in your system +}); +``` + +If you're using a different server / API framework, you'll need to make secure calls to that API to create a new token for your users. For example, you might validate a user's session, then call the Pipedream API to create a new token for that user. + +### Connect your account from the frontend + +First, install the [Pipedream SDK](https://www.npmjs.com/package/@pipedream/sdk) in your frontend: + +```bash +npm i --save @pipedream/sdk +``` + +When the user connects an account in your product, [fetch a token from your backend](#connect-to-the-pipedream-api-from-your-server-and-create-a-token) and call `connectAccount`. This opens a Pipedream iframe that guides the user through the account connection. + +In our example, `app/page.tsx` calls the `connectAccount` method from the Pipedream SDK when the user clicks the **Connect your account** button. + +
+Connect your account button + +```typescript +// Note that we import the browser-specific SDK client here +import { createClient } from "@pipedream/sdk/browser" + +export default function Home() { + function connectAccount() { + pd.connectAccount({ + app: process.env.NEXT_PUBLIC_PIPEDREAM_APP_SLUG, // From the Next.js example — adjust to pass your own app name slug + token: "YOUR_TOKEN", // The token you received from your server + onSuccess: ({ id: accountId }) => { + console.log(`Account successfully connected: ${accountId}`) + } + }) + } + + return ( +
+ +
+ ) +} +``` + +Press that button to connect an account for the app you configured. + +### Retrieve the credentials from the backend + +Once the user connects an account, you can retrieve their credentials from your backend with your project keys. + +This example shows you how to fetch credentials by your end user's `external_id` and the app's name slug. You can also fetch all connected accounts for a specific app, or a specific user — see the [Connect API reference](/connect/api). + + + +```typescript +import { + createClient, +} from "@pipedream/sdk"; + +const pd = createClient({ + publicKey: PIPEDREAM_PROJECT_PUBLIC_KEY, + secretKey: PIPEDREAM_PROJECT_SECRET_KEY, +}); + +export async function getUserAccounts(externalId: string) { + const data = await pd.getAccount({ + externalId, + includeCredentials: true, + }) + if (!data?.accounts.length) { + return null; + } + + // Parse and return data?.accounts. These may contain credentials, + // which you should never return to the client +} +``` + + +```javascript +const fetch = require('node-fetch'); + +class Client { + constructor({ publicKey, secretKey }) { + this.publicKey = publicKey; + this.secretKey = secretKey; + this.baseURL = 'https://api.pipedream.com'; + } + + _authorizationHeader() { + const encoded = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString('base64'); + return `Basic ${encoded}`; + } + + async getUserAccounts(externalId) { + const auth = this._authorizationHeader(); + const url = `${this.baseURL}/v1/connect/users/${externalId}/accounts`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': auth, + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (!data?.accounts?.length) { + return null; + } + + // Parse and return data.accounts. Ensure to handle sensitive data appropriately. + return data.accounts; + } +} + +// Usage example +const client = new Client({ + publicKey: 'YOUR_PUBLIC_KEY', + secretKey: 'YOUR_SECRET_KEY', +}); + +client.getUserAccounts('USER_ID') + .then(response => { + // handle response + }) + .catch(error => { + console.error('Error:', error); + }); +``` + + +```python +import base64 +import requests + +class Client: + def __init__(self, public_key, secret_key): + self.public_key = public_key + self.secret_key = secret_key + self.base_url = "https://api.pipedream.com" + + def _authorization_header(self): + encoded = base64.b64encode(f"{self.public_key}:{self.secret_key}".encode()).decode() + return f"Basic {encoded}" + + def get_user_accounts(self, external_id): + auth = self._authorization_header() + url = f"{self.base_url}/v1/connect/users/{external_id}/accounts" + response = requests.get(url, headers={"Authorization": auth}) + data = response.json() + + if not data.get('accounts'): + return None + + # Parse and return data['accounts']. Ensure to handle sensitive data appropriately. + return data['accounts'] + +# Usage example +client = Client('YOUR_PUBLIC_KEY', 'YOUR_SECRET_KEY') +response = client.get_user_accounts('USER_ID') +``` + + +```java +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Base64; +import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.nio.charset.StandardCharsets; + +public class Client { + private String publicKey; + private String secretKey; + private String baseURL; + + public Client(String publicKey, String secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + this.baseURL = "https://api.pipedream.com"; + } + + private String authorizationHeader() { + String encoded = Base64.getEncoder().encodeToString((publicKey + ":" + secretKey).getBytes(StandardCharsets.UTF_8)); + return "Basic " + encoded; + } + + public String getUserAccounts(String externalId) throws Exception { + String auth = authorizationHeader(); + URL url = new URL(baseURL + "/v1/connect/users/" + externalId + "/accounts"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Authorization", auth); + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String inputLine; + StringBuilder content = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + in.close(); + + String response = content.toString(); + + // Parse response and return accounts data. Ensure to handle sensitive data appropriately. + if (!response.contains("accounts")) { + return null; + } + + return response; // Modify to parse and handle accounts as needed. + } + + public static void main(String[] args) throws Exception { + Client client = new Client("YOUR_PUBLIC_KEY", "YOUR_SECRET_KEY"); + String response = client.getUserAccounts("USER_ID"); + } +} +``` + + +```csharp +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +public class Client { + private string publicKey; + private string secretKey; + private string baseURL; + + public Client(string publicKey, string secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + this.baseURL = "https://api.pipedream.com"; + } + + private string AuthorizationHeader() { + string encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}")); + return $"Basic {encoded}"; + } + + public async Task GetUserAccounts(string externalId) { + string auth = AuthorizationHeader(); + using (HttpClient client = new HttpClient()) { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth); + HttpResponseMessage response = await client.GetAsync($"{baseURL}/v1/connect/users/{externalId}/accounts"); + + string result = await response.Content.ReadAsStringAsync(); + + // Parse and return accounts data. Ensure to handle sensitive data appropriately. + if (!result.Contains("accounts")) { + return null; + } + + return result; // Modify to parse and handle accounts as needed. + } + } + + public static async Task Main(string[] args) { + var client = new Client("YOUR_PUBLIC_KEY", "YOUR_SECRET_KEY"); + string response = await client.GetUserAccounts("USER_ID"); + } +} +``` + + +```go +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +type Client struct { + PublicKey string + SecretKey string + BaseURL string +} + +func NewClient(publicKey, secretKey string) *Client { + return &Client{ + PublicKey: publicKey, + SecretKey: secretKey, + BaseURL: "https://api.pipedream.com", + } +} + +func (c *Client) authorizationHeader() string { + encoded := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.PublicKey, c.SecretKey))) + return fmt.Sprintf("Basic %s", encoded) +} + +func (c *Client) GetUserAccounts(externalId string) (map[string]interface{}, error) { + url := fmt.Sprintf("%s/v1/connect/users/%s/accounts", c.BaseURL, externalId) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", c.authorizationHeader()) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result map[string]interface{} + err = json.Unmarshal(body, &result) + if err != nil { + return nil, err + } + + if accounts, exists := result["accounts"]; exists { + return accounts.([]interface{}), nil + } + + return nil, nil +} + +func main() { + client := NewClient("YOUR_PUBLIC_KEY", "YOUR_SECRET_KEY") + + accounts, err := client.GetUserAccounts("USER_ID") + if err != nil { + fmt.Println("Error:", err) + return + } +} +``` + + +```php +publicKey = $publicKey; + $this->secretKey = $secretKey; + $this->baseURL = "https://api.pipedream.com"; + } + + private function authorizationHeader() { + return "Basic " . base64_encode("$this->publicKey:$this->secretKey"); + } + + public function getUserAccounts($externalId) { + $url = "$this->baseURL/v1/connect/users/$externalId/accounts"; + $opts = [ + 'http' => [ + 'header' => "Authorization: " . $this->authorizationHeader(), + 'method' => 'GET', + ], + ]; + + $context = stream_context_create($opts); + $result = file_get_contents($url, false, $context); + + if ($result === FALSE) { + return null; + } + + $data = json_decode($result, true); + + if (empty($data['accounts'])) { + return null; + } + + return $data['accounts']; // Modify to parse and handle accounts as needed. + } +} + +// Usage example +$client = new Client('YOUR_PUBLIC_KEY', 'YOUR_SECRET_KEY'); +$response = $client->getUserAccounts('USER_ID'); +?> +``` + + +```ruby +require 'base64' +require 'json' +require 'net/http' +require 'uri' + +class Client + def initialize(public_key, secret_key) + @public_key = public_key + @secret_key = secret_key + @base_url = "https://api.pipedream.com" + end + + def authorization_header + encoded = Base64.strict_encode64("#{@public_key}:#{@secret_key}") + "Basic #{encoded}" + end + + def get_user_accounts(external_id) + uri = URI("#{@base_url}/v1/connect/users/#{external_id}/accounts") + req = Net::HTTP::Get.new(uri) + req['Authorization'] = authorization_header + + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(req) + end + + data = JSON.parse(res.body) + + # Parse and return accounts data. Ensure to handle sensitive data appropriately. + return nil if data['accounts'].nil? || data['accounts'].empty? + + data['accounts'] + end +end + +# Usage example +client = Client.new('YOUR_PUBLIC_KEY', 'YOUR_SECRET_KEY') +response = client.get_user_accounts('USER_ID') +``` + + + +### Deploy your app to production + +Once you have the app working locally and want to deploy it to production, you'll need to [verify the domain](/workspaces/domain-verification#pipedream-connect) where the app will be hosted. This secures the account-connection process by ensuring that the app is hosted on a domain you control. + +If you don't verify your production domain, the account connection process will fail with an error message on iframe load. + +
\ No newline at end of file diff --git a/docs-v2/pages/connect/reference.mdx b/docs-v2/pages/connect/reference.mdx deleted file mode 100644 index 21dd3c3e6b0fd..0000000000000 --- a/docs-v2/pages/connect/reference.mdx +++ /dev/null @@ -1,220 +0,0 @@ -# API Reference -After your end users have connected their account using Pipedream Connect, you can retrieve their credentials using Pipedream's REST API, which enables you to make authenticated requests on their behalf. - -## Overview -The base URL for all Connect requests is `https://api.pipedream.com/v1/connect`. - -You authenticate to the Connect REST API using **Basic Auth**. Send your Pipedream [Project Public Key]() as the username and the [Project Secret Key]() as the password. When you make API requests, pass an -`Authorization` header of the following format: - -``` -Authorization: Basic -``` - -For example, here's how you can use `cURL` to fetch a list of all accounts in your project: - -```shell -curl 'https://api.pipedream.com/v1/connect/accounts' \ - -H 'Authorization: Basic ' -``` - -## API Reference - -### List accounts -List all connected accounts for end users within your project - -``` -GET /accounts/ -``` - -#### Example response - -```json -{ - "data": { - "accounts": [ - { - "id": "apn_XehyZPr", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "app_OkrhR1", - "name": "Slack" - }, - "created_at": "2024-07-30T22:52:48.000Z", - "updated_at": "2024-08-01T03:44:17.000Z" - }, - { - "id": "apn_b6h9QDK", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "app_OrZhaO", - "name": "GitHub" - }, - "created_at": "2024-07-31T02:49:18.000Z", - "updated_at": "2024-08-01T03:58:17.000Z" - }, - { - "id": "apn_0WhJYxv", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "app_OrZhaO", - "name": "GitHub" - }, - "created_at": "2024-07-31T20:28:16.000Z", - "updated_at": "2024-08-01T03:47:30.000Z" - }, - { - "id": "apn_kVh9PJx", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "app_OrZhaO", - "name": "GitHub" - }, - "created_at": "2024-07-31T21:17:03.000Z", - "updated_at": "2024-08-01T03:43:23.000Z" - }, - { - "id": "apn_WYhMlrz", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "app_XBxhAl", - "name": "Airtable" - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z" - } - ] - } -} -``` - -### List all connected accounts for an app - -``` -GET /apps/:app_id/accounts -``` - -- `:app_id` can be `oauth_app_id` or `app_id` - -#### Example response - -```json -[ - { - "id": "apn_WYhMlrz", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "oa_aw4ib2", - "name_slug": "airtable_oauth", - "name": "Airtable", - "auth_type": "oauth", - "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", - "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", - "custom_fields_json": "[]", - "categories": ["Productivity"] - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z" - } -] -``` - -### Retrieve account details -Retrieve the account details for a specific account - -``` -GET /accounts/:apn_id -``` -- `:apn_id` is the auth provision of the account you want to retrieve -- Optionally include `?include_credentials=1` as a query-string parameter to include the account credentials in the response - -#### Example response (without account credentials) - -```json -{ - "data": { - "id": "apn_WYhMlrz", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "oa_aw4ib2", - "name_slug": "airtable_oauth", - "name": "Airtable", - "auth_type": "oauth", - "description": "Airtable is a low-code platform to build next-gen apps. Move beyond rigid tools, operationalize your critical data, and reimagine workflows with AI.", - "img_src": "https://assets.pipedream.net/s.v0/app_XBxhAl/logo/orig", - "custom_fields_json": "[]", - "categories": ["Productivity"] - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z" - } -} -``` - -#### Example Response (with `include_credentials=1`) - -```json -{ - "data": { - "id": "apn_WYhMlrz", - "name": null, - "healthy": true, - "dead": false, - "app": { - "id": "app_XBxhAl", - "name": "Airtable" - }, - "created_at": "2024-08-01T04:04:03.000Z", - "updated_at": "2024-08-01T04:04:03.000Z", - "credentials": { - "oauth_client_id": "dd7a26ca-ba11-4f80-8667-xxxxxxxx", - "oauth_access_token": "oaaLa2Ob1umiregWa.v1.xxxxxxxx.xxxxxxxx", - "oauth_refresh_token": "oaaLa2Ob1umiregWa.v1.refresh.xxxxxxxx", - "oauth_uid": "usrnbIhrdiaOwPf7q" - }, - "expires_at": "2024-08-01T05:04:03.000Z", - "project_id": 279440, - "user_id": "danny", - "error": null, - "last_refreshed_at": null, - "next_refresh_at": "2024-08-01T04:17:33.000Z" - } -} -``` - -### Delete individual connected account -Delete a specific connected account for an end user - -``` -DELETE /v1/connect/accounts/:apn_id -``` -- `:apn_id` is the auth provision of the account you want to retrieve -- We'll return a `204 No Content` response if the account was successfully deleted - -### Delete all connected accounts for a specific app - -`/DELETE /apps/:app_id/accounts` - -- `:app_id` can be `oauth_app_id` or `app_id` -- We'll return a `204 No Content` response if the accounts were successfully deleted - -### Delete an end user and all their connected accounts - -`DELETE /users/:external_id` - -- `:external_id` corresponds to the the end user's ID in your system -- We'll return a `204 No Content` response if the user was successfully deleted \ No newline at end of file diff --git a/docs-v2/pages/connect/use-cases.mdx b/docs-v2/pages/connect/use-cases.mdx new file mode 100644 index 0000000000000..9bdcb3a7c55d7 --- /dev/null +++ b/docs-v2/pages/connect/use-cases.mdx @@ -0,0 +1,41 @@ +# Pipedream Connect use cases + +Developers use Pipedream Connect to build customer-facing API integrations into their products. It lets you build [in-app messaging](#in-app-messaging), [CRM syncs](#crm-syncs), [AI-driven products](#ai-products), and much more, all in a few minutes. + +## Core value to app developers + +In 20 years of building software, we've seen a common theme. No matter the product, your customers end up needing to connect your app to third-party APIs. + +You might build real-time notifications with messaging apps, export customer data to databases or spreadsheets, ingest data from CRMs, or connect to any of the thousands of APIs and SaaS services your customers are using. These features are often tied to large contracts and Enterprise customers. + +But it's hard to justify the engineering effort required for these integrations. They're a distraction from the core product. Once built, they're hard to maintain. You have to securely manage auth, learn the nuances of each API, and improve the integration as your customers ask for new features. Managing these integrations is a huge context switch for any engineer. Most teams have trouble scaling this. + +At Pipedream, our customers tell us a variant of this story every day. Pipedream Connect helps you build these features **in minutes**, for any app. + +Once you add the core integration UI to your app, non-technical employees can also help to manage [the workflows](/workflows) that drive the backend logic. For example, if you're building [in-app messaging](#in-app-messaging), once you add the UI to let users connect Slack, Discord, and other tools, anyone on your team can build workflows that format and deliver messages to your customers. This is a huge plus for many orgs: you still get to build a bespoke UI, directly in your app, suited to your customer need. But anyone in the company can collaborate on the workflows that power it. + +## Value to your customers + +Shipping new customer-facing integrations can happen in minutes. + +## How customers are using Connect + +### In-app messaging + +Most apps build email notifications, since it's easy. But most teams work in Slack, Discord, Microsoft Teams, or a variety of other messaging apps. Sometimes you want to send messages via SMS or push notifications. It's hard to maintain integrations for all the apps your customers are using. Pipedream makes this simple. + +### CRM syncs + +Sync data between your app and Salesforce, HubSpot, or any CRM. Pipedream lets your customers connect their accounts directly from your UI, define the sync logic, and run it on Pipedream's infrastructure. Pull data from your customers' CRMs in real-time, or push data from your app. + +### AI products + +Talk to any AI API or LLM. Build chat apps or interact in real-time with your users. Or run asynchronous tasks in the background, like image classification, article summarization, or other tasks you want to offload to an AI agent. You can use built-in functions like [`$.flow.suspend`](/code/nodejs/rerun#flowsuspend) to send a message to your team, or directly to the user, to approve specific actions. + +### Spreadsheet integrations + +Sync data between your app and Google Sheets, Airtable, or any spreadsheet. Pipedream Connect lets your users auth with any app, select the sheet, and define custom sync logic. + +### And much more + +Building an app with Pipedream and want to be profiled here (anonymously or otherwise)? Email connect@pipedream.com to let us know! \ No newline at end of file diff --git a/docs-v2/pages/connected-accounts/index.mdx b/docs-v2/pages/connected-accounts/index.mdx index ccc58a27f6a13..edb4aff970777 100644 --- a/docs-v2/pages/connected-accounts/index.mdx +++ b/docs-v2/pages/connected-accounts/index.mdx @@ -233,7 +233,7 @@ You can access credentials for any connected account via API, letting you build ## Passing external credentials at runtime -If you use a secrets store like [HashiCorp Vault](https://www.vaultproject.io/) or [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), store credentials in a database, or use a service like [Nango](https://www.nango.dev/) to manage auth, you can retrieve these secrets at runtime and pass them to any step. [See the full guide here](/connected-accounts/external-auth/). +If you use a secrets store like [Pipedream Connect](/connect) or [HashiCorp Vault](https://www.vaultproject.io/), or if you store credentials in a database, you can retrieve these secrets at runtime and pass them to any step. [See the full guide here](/connected-accounts/external-auth/). ## Connecting to apps with IP restrictions diff --git a/packages/sdk/.gitignore b/packages/sdk/.gitignore index 369ad896577ae..b12c26f6f2e29 100644 --- a/packages/sdk/.gitignore +++ b/packages/sdk/.gitignore @@ -1,3 +1,3 @@ -dist/ -node_modules/ +dist +node_modules *.env* diff --git a/packages/sdk/README.md b/packages/sdk/README.md new file mode 100644 index 0000000000000..742ffe9acb771 --- /dev/null +++ b/packages/sdk/README.md @@ -0,0 +1,57 @@ +# `@pipedream/sdk` + +TypeScript SDK for [Pipedream](https://pipedream.com). [See the docs](https://pipedream.com/docs/connect) for usage instructions. + +## Install + +```bash +npm i @pipedream/sdk +``` + +## Usage + +[See the docs](https://pipedream.com/docs/connect) for full usage instructions. This package installs code that can be used by both a server, to make Pipedream API requests: + +```typescript +import { + createClient, + type ConnectTokenCreateOpts, + type ConnectTokenResponse, +} from "@pipedream/sdk"; + +const pd = createClient({ + publicKey: process.env.PIPEDREAM_PROJECT_PUBLIC_KEY, + secretKey: process.env.PIPEDREAM_PROJECT_SECRET_KEY, +}); + +export async function serverConnectTokenCreate( + opts: ConnectTokenCreateOpts +): Promise> { + return pd.connectTokenCreate(opts); +} +``` + +and a browser client, to perform user-facing operations like connecting accounts: + +```typescript +// Note that we import the browser-specific SDK client here +import { createClient } from "@pipedream/sdk/browser"; + +export default function Home() { + function connectAccount() { + pd.connectAccount({ + app: "app_slug", // the app you're connecting + token: "YOUR_TOKEN", // The token you received from your server + onSuccess: ({ id: accountId }) => { + console.log(`Account successfully connected: ${accountId}`); + }, + }); + } + + return ( +
+ +
+ ); +} +``` diff --git a/packages/sdk/dist/browser/index.d.ts b/packages/sdk/dist/browser/index.d.ts deleted file mode 100644 index 7b36a3635643b..0000000000000 --- a/packages/sdk/dist/browser/index.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -type CreateBrowserClientOpts = { - environment?: string; - frontendHost?: string; - publicKey: string; -}; -type AppId = string; -type StartConnectApp = { - id: AppId; -}; -type ConnectResult = {}; -declare class ConnectError extends Error { -} -type StartConnectOpts = { - token: string; - app: AppId | StartConnectApp; - onSuccess?: (res: ConnectResult) => void; - onError?: (err: ConnectError) => void; -}; -export declare function createClient(opts: CreateBrowserClientOpts): BrowserClient; -declare class BrowserClient { - environment?: string; - baseURL: string; - publicKey: string; - iframeURL: string; - iframe?: HTMLIFrameElement; - iframeId: number; - constructor(opts: CreateBrowserClientOpts); - startConnect(opts: StartConnectOpts): void; -} -export {}; diff --git a/packages/sdk/dist/browser/index.js b/packages/sdk/dist/browser/index.js deleted file mode 100644 index c7862022c359d..0000000000000 --- a/packages/sdk/dist/browser/index.js +++ /dev/null @@ -1,82 +0,0 @@ -var __rest = (this && this.__rest) || function (s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -}; -class ConnectError extends Error { -} -export function createClient(opts) { - return new BrowserClient(opts); -} -class BrowserClient { - constructor(opts) { - this.iframeId = 0; - this.environment = opts.environment; - this.publicKey = opts.publicKey; - this.baseURL = `https://${opts.frontendHost || "pipedream.com"}`; - this.iframeURL = `${this.baseURL}/_static/connect.html`; - } - startConnect(opts) { - const onMessage = (e) => { - var _a, _b, _c, _d, _e; - switch ((_a = e.data) === null || _a === void 0 ? void 0 : _a.type) { - case "verify-domain": - // The Application should respond with it's domain to the iframe for security - console.log("Sending Response to", e.origin); - (_b = e.source) === null || _b === void 0 ? void 0 : _b.postMessage({ type: "domain-response", origin: window.origin }, { targetOrigin: e.origin }); - break; - case "success": - const _f = e.data, { authProvisionId: id } = _f, rest = __rest(_f, ["authProvisionId"]); - console.log("SUCCESS!!!", e); - (_c = opts.onSuccess) === null || _c === void 0 ? void 0 : _c.call(opts, Object.assign({ id }, rest)); - break; - case "error": - // Return the error to the parent if there was a problem with the Authorization - console.log("ERROR!!!", e); - (_d = opts.onError) === null || _d === void 0 ? void 0 : _d.call(opts, new ConnectError(e.data.error)); - break; - case "close": - console.log("CLOSE!!!", e); - (_e = this.iframe) === null || _e === void 0 ? void 0 : _e.remove(); - window.removeEventListener("message", onMessage); - break; - default: - console.debug('Unknown Connect Event type', e); - break; - } - }; - window.addEventListener("message", onMessage); - const qp = new URLSearchParams(); - qp.set("token", opts.token); - if (this.environment) { - qp.set("environment", this.environment); - } - qp.set("public_key", this.publicKey); - if (typeof opts.app === "string") { - qp.set("app", opts.app); - } - else { - const err = new ConnectError("object app not yet supported"); - if (opts.onError) { - opts.onError(err); - return; - } - throw err; - } - const iframe = document.createElement("iframe"); - iframe.id = `pipedream-connect-iframe-${this.iframeId++}`; - iframe.title = "Pipedream Connect"; - iframe.src = `${this.iframeURL}?${qp.toString()}`; - iframe.setAttribute("style", "position:fixed;inset:0;z-index:2147483647;border:0;display:block;overflow:hidden auto"); - iframe.setAttribute("height", "100%"); - iframe.setAttribute("width", "100%"); - document.body.appendChild(iframe); - this.iframe = iframe; - } -} diff --git a/packages/sdk/dist/index.d.ts b/packages/sdk/dist/index.d.ts deleted file mode 100644 index b99bcb874fbd2..0000000000000 --- a/packages/sdk/dist/index.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -type CreateServerClientOpts = { - apiHost?: string; - environment?: string; - publicKey: string; - secretKey: string; -}; -export type ConnectTokenCreateOpts = { - app_id: string; - client_name?: string; - external_id: string; -}; -type AccountId = string; -type AccountKeyFields = { - externalId: string; - appId: string; -}; -type AccountKey = AccountId | AccountKeyFields; -export declare function createClient(opts: CreateServerClientOpts): ServerClient; -declare class ServerClient { - environment?: string; - secretKey: string; - publicKey: string; - baseURL: string; - constructor(opts: CreateServerClientOpts); - private _authorizationHeader; - connectTokenCreate(opts: ConnectTokenCreateOpts): Promise; - getAccount(key: AccountKey, opts?: { - includeCredentials?: boolean; - }): Promise; -} -export {}; diff --git a/packages/sdk/dist/index.js b/packages/sdk/dist/index.js deleted file mode 100644 index 78b96c166d967..0000000000000 --- a/packages/sdk/dist/index.js +++ /dev/null @@ -1,77 +0,0 @@ -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -export function createClient(opts) { - return new ServerClient(opts); -} -class ServerClient { - constructor(opts) { - this.environment = opts.environment; - this.secretKey = opts.secretKey; - this.publicKey = opts.publicKey; - const { apiHost = "pipedream.com" } = opts; - this.baseURL = `https://${apiHost}`; - } - _authorizationHeader() { - const encoded = Buffer - .from(`${this.publicKey}:${this.secretKey}`) - .toString("base64"); - return `Basic ${encoded}`; - } - // XXX move to REST API endpoint - connectTokenCreate(opts) { - return __awaiter(this, void 0, void 0, function* () { - const auth = this._authorizationHeader(); - const resp = yield fetch(`${this.baseURL}/v1/connect/tokens`, { - method: "POST", - headers: { - "authorization": auth, - "content-type": "application/json", - }, - body: JSON.stringify(opts), - }); - const res = yield resp.json(); - // XXX expose error here - return res === null || res === void 0 ? void 0 : res.token; - }); - } - getAccount(key, opts) { - return __awaiter(this, void 0, void 0, function* () { - let url; - let id; - const baseAccountURL = `${this.baseURL}/v1/accounts`; - if (typeof key === "string") { - id = key; - url = `${baseAccountURL}/${id}`; - } - else { - url = `${baseAccountURL}?app=${key.appId}&limit=100&external_id=${key.externalId}`; - } - if (opts === null || opts === void 0 ? void 0 : opts.includeCredentials) { - url += `${id - ? "?" - : "&"}include_credentials=1`; - } - const resp = yield fetch(url, { - headers: { - Authorization: this._authorizationHeader(), - }, - }); - const res = yield resp.json(); - const { data, error, } = res; - if (error) { - if (error === "record not found") { - return null; - } - throw new Error(error); - } - return data; - }); - } -} diff --git a/packages/sdk/examples/next-app/.env.example b/packages/sdk/examples/next-app/.env.example index 4033409f44708..9de6fe94583d5 100644 --- a/packages/sdk/examples/next-app/.env.example +++ b/packages/sdk/examples/next-app/.env.example @@ -1,5 +1,7 @@ # Config for the Next.js app -# Key based apps only require the app_slug value. OAuth apps require both. +# Key based apps only require the app_slug value. OAuth apps require both. +# To get the app slug, visit https://pipedream.com/apps, select your app, +# and copy the slug from the Authentication section. NEXT_PUBLIC_PIPEDREAM_APP_SLUG= NEXT_PUBLIC_PIPEDREAM_APP_ID=oa_ diff --git a/packages/sdk/examples/next-app/README.md b/packages/sdk/examples/next-app/README.md index dd3fa42c7b169..0e7614abfde47 100644 --- a/packages/sdk/examples/next-app/README.md +++ b/packages/sdk/examples/next-app/README.md @@ -11,13 +11,19 @@ npm i # installs dependencies npm run dev # runs app locally ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the example app running +Open [http://localhost:3000](http://localhost:3000) with your browser to see the example app running. Start editing by modifying `app/page.tsx` or `app/server.ts`. The app auto-updates as you edit files. ### Environment variables -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +First, -## Learn More about Next.js +```bash +cp .env.example .env.local +``` + +Then fill in the values of all environment variables. + +## Learn more about Next.js This example app is built with Next.js, a framework for quickly developing web apps with React. To learn more about Next, take a look at the following resources: diff --git a/packages/sdk/examples/next-app/app/page.tsx b/packages/sdk/examples/next-app/app/page.tsx index 1b31c5fd7d677..62b579bd7fd1f 100644 --- a/packages/sdk/examples/next-app/app/page.tsx +++ b/packages/sdk/examples/next-app/app/page.tsx @@ -2,8 +2,8 @@ import CodePanel from "./CodePanel"; import { useEffect, useState } from "react"; -import { serverConnectTokenCreate, getAppsData } from "./server" -import { createClient } from "../../../src/browser" +import { serverConnectTokenCreate, getUserAccounts } from "./server" +import { createClient } from "@pipedream/sdk/browser" const frontendHost = process.env.NEXT_PUBLIC_PIPEDREAM_FRONTEND_HOST || "pipedream.com" const appSlug = process.env.NEXT_PUBLIC_PIPEDREAM_APP_SLUG // required @@ -29,7 +29,7 @@ export default function Home() { } setApp(app) if (oauthAppId) setOauthAppId(oauthAppId) - pd.startConnect({ + pd.connectAccount({ app, token, onSuccess: ({ id: authProvisionId }) => { @@ -59,13 +59,11 @@ export default function Home() { try { const { token, expires_at } = await serverConnectTokenCreate({ app_slug: appSlug, - oauth_client_id: oauthAppId, - external_id: externalUserId + oauth_app_id: oauthAppId, + external_user_id: externalUserId }) setToken(token) setExpiresAt(expires_at) - const appsData = await getAppsData(externalUserId) - setGithubData(appsData.github) } catch (error) { console.error("Error fetching data:", error) // Handle error appropriately @@ -123,8 +121,8 @@ PIPEDREAM_PROJECT_SECRET_KEY=sec_abc123`} const { token, expires_at } = await serverConnectTokenCreate({ app_slug: "github", - oauth_client_id: "oa_abc123", // Only required for OAuth apps - external_id: "${externalUserId}", + oauth_app_id: "oa_abc123", // Only required for OAuth apps + external_user_id: "${externalUserId}", })`} /> @@ -137,14 +135,15 @@ const { token, expires_at } = await serverConnectTokenCreate({ {expiresAt}

- When a user wants to connect an app from your frontend, you'll call pd.startConnect with the token and the OAuth App ID of the app you'd like to connect. + When a user wants to connect an app from your frontend, you'll call pd.connectAccount with the token and the OAuth App ID of the app you'd like to connect.

{ @@ -156,13 +155,9 @@ pd.startConnect({ {apn ?

- Auth Provision ID: + Pipedream Account ID: {apn}

-

- OAuth App ID: - {oa} -

:

@@ -170,12 +165,6 @@ pd.startConnect({

} - { - githubData?.login && -

Your GitHub username: - {githubData.login} -

- }
} diff --git a/packages/sdk/examples/next-app/app/server.ts b/packages/sdk/examples/next-app/app/server.ts index 139ef8fe5b85a..6f59de45652d6 100644 --- a/packages/sdk/examples/next-app/app/server.ts +++ b/packages/sdk/examples/next-app/app/server.ts @@ -2,9 +2,10 @@ import { createClient, - type ConnectTokenCreateOpts, + type ConnectAPIResponse, + type ConnectTokenCreateOpts, type ConnectTokenResponse, -} from "../../../src"; +} from "@pipedream/sdk"; const { PIPEDREAM_API_HOST, @@ -26,41 +27,15 @@ const pd = createClient({ apiHost: PIPEDREAM_API_HOST, }); -export async function serverConnectTokenCreate(opts: ConnectTokenCreateOpts): Promise { +export async function serverConnectTokenCreate(opts: ConnectTokenCreateOpts): Promise> { return pd.connectTokenCreate(opts); } -export async function getAppsData(externalId: string) { - const [ - github, - ] = await Promise.all([ - getGithubData(externalId), - ]); - return { - github, - }; -} - -export async function getGithubData(externalId: string) { - if (!NEXT_PUBLIC_PIPEDREAM_APP_SLUG) - throw new Error("NEXT_PUBLIC_PIPEDREAM_APP_ID not set in environment"); - - const data = await pd.getAccount({ - appId: NEXT_PUBLIC_PIPEDREAM_APP_SLUG ?? "", - externalId, - }, { - includeCredentials: true, - }); - if (!data?.accounts.length) { - return null; - } - const account = data.accounts[data.accounts.length - 1]; - const resp = await fetch("https://api.github.com/user", { - headers: { - Authorization: `Bearer ${account.credentials.oauth_access_token}`, - }, - }); - const res = await resp.json(); +export async function getUserAccounts(externalId: string, include_credentials: number = 0) { + await pd.getAccountsByExternalId(externalId, { + include_credentials, // set to 1 to include credentials + }) - return res; + // Parse and return the data you need. These may contain credentials, + // which you should never return to the client } diff --git a/packages/sdk/examples/next-app/package-lock.json b/packages/sdk/examples/next-app/package-lock.json index 9749d612b3ccf..328b87c288c57 100644 --- a/packages/sdk/examples/next-app/package-lock.json +++ b/packages/sdk/examples/next-app/package-lock.json @@ -8,6 +8,7 @@ "name": "next-app", "version": "0.1.0", "dependencies": { + "@pipedream/sdk": "^0.0.6", "dompurify": "^3.1.6", "next": "14.1.0", "prismjs": "^1.29.0", @@ -409,6 +410,11 @@ "node": ">= 8" } }, + "node_modules/@pipedream/sdk": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@pipedream/sdk/-/sdk-0.0.6.tgz", + "integrity": "sha512-GMfQKWYlo//VJD+5Mtep3aAKF8wd43U3/Hts5cSa3eyThjyB1TzbOYXdrK8d1Sl0yXs8QNttwfF5hwaC+iZT6A==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", diff --git a/packages/sdk/examples/next-app/package.json b/packages/sdk/examples/next-app/package.json index 52f06eecf6dff..003bc09188546 100644 --- a/packages/sdk/examples/next-app/package.json +++ b/packages/sdk/examples/next-app/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@pipedream/sdk": "^0.0.6", "dompurify": "^3.1.6", "next": "14.1.0", "prismjs": "^1.29.0", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8a4870cd61581..52586e3e0646f 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,14 +1,27 @@ { "name": "@pipedream/sdk", - "version": "0.0.1", + "version": "0.0.6", "description": "Pipedream SDK", "type": "module", - "main": "dist/index.js", - "browser": "dist/browser/index.js", - "types": "dist/index.d.ts", - "homepage": "https://pipedream.com/", - "author": "Pipedream (https://pipedream.com/)", - "exports": "./dist/index.js", + "main": "dist/server/index.js", + "module": "dist/server/index.js", + "types": "dist/server/index.d.ts", + "browser": { + "import": "dist/browser/index.js", + "require": "dist/browser/index.js" + }, + "exports": { + ".": { + "import": "./dist/server/index.js", + "require": "./dist/server/index.js", + "types": "./dist/server/index.d.ts" + }, + "./browser": { + "import": "./dist/browser/index.js", + "require": "./dist/browser/index.js", + "types": "./dist/browser/index.d.ts" + } + }, "keywords": [ "pipedream" ], @@ -17,12 +30,13 @@ "access": "public" }, "scripts": { - "prepublish": "rm -rf dist && tsc", - "build": "tsc" + "prepublish": "rm -rf dist && npm run build", + "build": "npm run build:node && npm run build:browser", + "build:node": "tsc -p tsconfig.node.json", + "build:browser": "tsc -p tsconfig.browser.json" }, "files": [ - "dist", - "lib" + "dist" ], "devDependencies": { "@types/node": "^20.14.9", diff --git a/packages/sdk/src/browser/index.ts b/packages/sdk/src/browser/index.ts index af13f78f322db..8e14fa0779676 100644 --- a/packages/sdk/src/browser/index.ts +++ b/packages/sdk/src/browser/index.ts @@ -13,7 +13,7 @@ type ConnectResult = { id: string; }; -class ConnectError extends Error { } +class ConnectError extends Error {} type StartConnectOpts = { token: string; @@ -27,47 +27,39 @@ export function createClient(opts: CreateBrowserClientOpts) { } class BrowserClient { - environment?: string; - baseURL: string; - iframeURL: string; - iframe?: HTMLIFrameElement; - iframeId = 0; + private environment?: string; + private baseURL: string; + private iframeURL: string; + private iframe?: HTMLIFrameElement; + private iframeId = 0; constructor(opts: CreateBrowserClientOpts) { this.environment = opts.environment; - this.baseURL = `https://${opts.frontendHost || "https://pipedream.com"}`; + this.baseURL = `https://${opts.frontendHost || "pipedream.com"}`; this.iframeURL = `${this.baseURL}/_static/connect.html`; } - startConnect(opts: StartConnectOpts) { + connectAccount(opts: StartConnectOpts) { const onMessage = (e: MessageEvent) => { + if (e.origin !== this.baseURL || !this.iframe?.contentWindow) { + console.warn("Untrusted origin or iframe not ready:", e.origin); + return; + } + switch (e.data?.type) { case "verify-domain": - if (e.origin === this.baseURL && this.iframe?.contentWindow) { - this.iframe.contentWindow.postMessage( - { - type: "domain-response", - origin: window.origin, - }, { - targetOrigin: e.origin, - }, - ); - } else { - console.warn("Untrusted origin or iframe not ready:", e.origin); - } + this.handleVerifyDomain(e); break; - case "success": { + case "success": opts.onSuccess?.({ id: e.data?.authProvisionId, }); break; - } case "error": opts.onError?.(new ConnectError(e.data.error)); break; case "close": - this.iframe?.remove(); - window.removeEventListener("message", onMessage); + this.cleanup(onMessage); break; default: break; @@ -76,32 +68,53 @@ class BrowserClient { window.addEventListener("message", onMessage); - const qp = new URLSearchParams(); - qp.set("token", opts.token); + try { + this.createIframe(opts); + } catch (err) { + opts.onError?.(err as ConnectError); + } + } + + private handleVerifyDomain(e: MessageEvent) { + if (this.iframe?.contentWindow) { + this.iframe.contentWindow.postMessage( + { + type: "domain-response", + origin: window.origin, + }, + e.origin, + ); + } + } + + private cleanup(onMessage: (e: MessageEvent) => void) { + this.iframe?.remove(); + window.removeEventListener("message", onMessage); + } + + private createIframe(opts: StartConnectOpts) { + const qp = new URLSearchParams({ + token: opts.token, + }); + if (this.environment) { qp.set("environment", this.environment); } + if (typeof opts.app === "string") { qp.set("app", opts.app); } else { - const err = new ConnectError("object app not yet supported"); - if (opts.onError) { - opts.onError(err); - return; - } - throw err; + throw new ConnectError("Object app not yet supported"); } const iframe = document.createElement("iframe"); iframe.id = `pipedream-connect-iframe-${this.iframeId++}`; iframe.title = "Pipedream Connect"; iframe.src = `${this.iframeURL}?${qp.toString()}`; - iframe.setAttribute( - "style", - "position:fixed;inset:0;z-index:2147483647;border:0;display:block;overflow:hidden auto", - ); - iframe.setAttribute("height", "100%"); - iframe.setAttribute("width", "100%"); + iframe.style.cssText = + "position:fixed;inset:0;z-index:2147483647;border:0;display:block;overflow:hidden auto"; + iframe.width = "100%"; + iframe.height = "100%"; iframe.onload = () => { this.iframe = iframe; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts deleted file mode 100644 index 016ed30e1b134..0000000000000 --- a/packages/sdk/src/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -type CreateServerClientOpts = { - apiHost?: string; - environment?: string; - publicKey: string; - secretKey: string; -}; - -export type ConnectTokenCreateOpts = { - app_slug: string; - oauth_client_id?: string; - client_name?: string; - external_id: string; -}; - -export type ConnectTokenResponse = { - token: string; - expires_at: string; -}; - -type AccountId = string; -type AccountKeyFields = { - externalId: string; - appId: string; -}; -type AccountKey = AccountId | AccountKeyFields; - -export function createClient(opts: CreateServerClientOpts) { - return new ServerClient(opts); -} - -class ServerClient { - environment?: string; - secretKey: string; - publicKey: string; - baseURL: string; - - constructor(opts: CreateServerClientOpts) { - this.environment = opts.environment; - this.secretKey = opts.secretKey; - this.publicKey = opts.publicKey; - - const { apiHost = "api.pipedream.com" } = opts; - this.baseURL = `https://${apiHost}`; - } - - private _authorizationHeader(): string { - const encoded = Buffer - .from(`${this.publicKey}:${this.secretKey}`) - .toString("base64"); - return `Basic ${encoded}`; - } - - async connectTokenCreate(opts: ConnectTokenCreateOpts): Promise { - const auth = this._authorizationHeader(); - const resp = await fetch(`${this.baseURL}/v1/connect/tokens`, { - method: "POST", - headers: { - "authorization": auth, - "content-type": "application/json", - }, - body: JSON.stringify(opts), - }); - - return resp.json(); - } - - async getAccount(key: AccountKey, opts?: { includeCredentials?: boolean; }) { - let url: string; - let id: string | undefined; - const baseAccountURL = `${this.baseURL}/v1/connect`; - if (typeof key === "string") { - id = key; - url = `${baseAccountURL}/accounts/${id}`; - } else if (key.externalId) { - url = `${baseAccountURL}/users/${key.externalId}/accounts`; - } else if (key.appId) { - url = `${baseAccountURL}/apps/${key.appId}/accounts`; - } else { - url = `${baseAccountURL}/accounts`; - } - - if (opts?.includeCredentials) { - url += "?include_credentials=1"; - } - - const resp = await fetch(url, { - headers: { - "authorization": this._authorizationHeader(), - "content-type": "application/json", - }, - }); - const res = await resp.json(); - const { - data, error, - } = res; - if (error) { - if (error === "record not found") { - return null; - } - throw new Error(error); - } - return data; - } -} diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts new file mode 100644 index 0000000000000..800d94e74b0ff --- /dev/null +++ b/packages/sdk/src/server/index.ts @@ -0,0 +1,208 @@ +export type CreateServerClientOpts = { + apiHost?: string; + environment?: string; + publicKey: string; + secretKey: string; +}; + +export type ConnectTokenCreateOpts = { + app_slug: string; + oauth_app_id?: string; + external_user_id: string; +}; + +export type ConnectTokenResponse = { + token: string; + expires_at: string; +}; + +export type ConnectParams = { + include_credentials?: number; +}; + +export type AuthType = "oauth" | "keys" | "none"; + +export type AppResponse = { + id: string; + name_slug: string; + name: string; + auth_type: AuthType; + img_src: string; + custom_fields_json: string; + categories: string[]; + +}; + +export type CreateAccountOpts = { + app_slug: string; + connect_token: string; + cfmap_json: string; + name?: string; +}; + +// Updated Account type to include optional fields +export type Account = { + id: string; + name: string; + external_id: string; + healthy: boolean; + dead: boolean; + app: AppResponse; + created_at: string; + updated_at: string; + credentials?: Record; // Optional field for when include_credentials is true +}; + +interface SuccessResponse { + status: "success"; + data: T; +} + +export type ErrorResponse = { + error: string; +}; + +export type ConnectAPIResponse = SuccessResponse | ErrorResponse; + +interface ConnectRequestOptions extends Omit { + params?: Record; + headers?: Record; +} + +export function createClient(opts: CreateServerClientOpts) { + return new ServerClient(opts); +} + +class ServerClient { + environment?: string; + secretKey: string; + publicKey: string; + baseURL: string; + + constructor(opts: CreateServerClientOpts) { + this.environment = opts.environment; + this.secretKey = opts.secretKey; + this.publicKey = opts.publicKey; + + const { apiHost = "api.pipedream.com" } = opts; + this.baseURL = `https://${apiHost}/v1`; + } + + private _authorizationHeader(): string { + const encoded = Buffer + .from(`${this.publicKey}:${this.secretKey}`) + .toString("base64"); + return `Basic ${encoded}`; + } + + async _makeConnectRequest( + path: string, + opts: ConnectRequestOptions = {}, + ): Promise> { + const { + params, + headers: customHeaders, + body, + method = "GET", + ...fetchOpts + } = opts; + const url = new URL(`${this.baseURL}/connect${path}`); + + if (params) { + Object.entries(params).forEach(([ + key, + value, + ]) => { + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + url.searchParams.append(key, value.toString()); + } + }); + } + + const headers = { + "Authorization": this._authorizationHeader(), + "Content-Type": "application/json", + ...customHeaders, + }; + + // Prepare the request options + const requestOptions: RequestInit = { + method, + headers, + ...fetchOpts, + }; + + // Handle body for POST, PUT, PATCH requests + if ([ + "POST", + "PUT", + "PATCH", + ].includes(method.toUpperCase()) && body) { + requestOptions.body = body; + } + + const response: Response = await fetch(url.toString(), requestOptions); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json() as ConnectAPIResponse; + return result; + } + + async connectTokenCreate(opts: ConnectTokenCreateOpts): Promise> { + const body = { + // named external_id in the API, but from the developer's perspective, it's the user's ID + external_id: opts.external_user_id, + ...opts, + }; + return this._makeConnectRequest("/tokens", { + method: "POST", + body: JSON.stringify(body), + }); + } + + async getAccounts(params: ConnectParams = {}): Promise> { + return this._makeConnectRequest("/accounts", { + params, + }); + } + + async getAccount(accountId: string, params: ConnectParams = {}): Promise> { + return this._makeConnectRequest(`/accounts/${accountId}`, { + params, + }); + } + + async getAccountsByApp(appId: string, params: ConnectParams = {}): Promise> { + return this._makeConnectRequest(`/accounts/app/${appId}`, { + params, + }); + } + + async getAccountsByExternalId(externalId: string, params: ConnectParams = {}): Promise> { + return this._makeConnectRequest(`/accounts/external_id/${externalId}`, { + params, + }); + } + + async createAccount(opts: CreateAccountOpts): Promise> { + return this._makeConnectRequest("/accounts", { + method: "POST", + body: JSON.stringify(opts), + }); + } + + async deleteAccount(accountId: string): Promise { + await this._makeConnectRequest(`/accounts/${accountId}`, { + method: "DELETE", + }); + } + + async deleteAccountsByApp(appId: string): Promise { + await this._makeConnectRequest(`/accounts/app/${appId}`, { + method: "DELETE", + }); + } +} diff --git a/packages/sdk/tsconfig.browser.json b/packages/sdk/tsconfig.browser.json new file mode 100644 index 0000000000000..a0cafbd664f44 --- /dev/null +++ b/packages/sdk/tsconfig.browser.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ES6", + "lib": ["ES5", "DOM"], + "declaration": true, + "outDir": "dist/browser", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "skipLibCheck": true, + "types": [] + }, + "include": [ + "src/browser/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json deleted file mode 100644 index 7254286117e97..0000000000000 --- a/packages/sdk/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "module": "NodeNext", - "target": "ES6", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "outDir": "dist" - }, - "include": [ - "./src/**/*" - ] -} diff --git a/packages/sdk/tsconfig.node.json b/packages/sdk/tsconfig.node.json new file mode 100644 index 0000000000000..5c15c2bd18e77 --- /dev/null +++ b/packages/sdk/tsconfig.node.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ES6", + "lib": ["ES6"], + "declaration": true, + "outDir": "dist/server", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "NodeNext", + "skipLibCheck": true, + "types": ["node"] + }, + "include": [ + "src/server/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c5d1dd6b016b..94c937a1e423d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11187,6 +11187,7 @@ importers: packages/sdk/examples/next-app: specifiers: + '@pipedream/sdk': ^0.0.6 '@types/dompurify': ^3.0.5 '@types/node': ^20 '@types/prismjs': ^1.26.4 @@ -11206,6 +11207,7 @@ importers: tailwindcss: ^3.3.0 typescript: ^5 dependencies: + '@pipedream/sdk': 0.0.6 dompurify: 3.1.6 next: 14.1.0_nnrd3gsncyragczmpvfhocinkq prismjs: 1.29.0 @@ -15255,7 +15257,7 @@ packages: dependencies: '@definitelytyped/typescript-versions': 0.1.3 '@definitelytyped/utils': 0.1.7 - semver: 7.5.4 + semver: 7.6.3 dev: true /@definitelytyped/typescript-versions/0.1.3: @@ -16976,6 +16978,10 @@ packages: - debug dev: false + /@pipedream/sdk/0.0.6: + resolution: {integrity: sha512-GMfQKWYlo//VJD+5Mtep3aAKF8wd43U3/Hts5cSa3eyThjyB1TzbOYXdrK8d1Sl0yXs8QNttwfF5hwaC+iZT6A==} + dev: false + /@pipedream/snowflake-sdk/1.0.8_asn1.js@5.4.1: resolution: {integrity: sha512-/nLCQNjlSCz71MUnOUZqWmnjZTbEX7mie91mstPspb8uDG/GvaDk/RynLGhhYfgEP5d1KWj+OPaI71hmPSxReg==} dependencies: @@ -18385,7 +18391,7 @@ packages: request: 2.88.2 retry: 0.12.0 safe-buffer: 5.2.1 - semver: 7.5.4 + semver: 7.6.3 slide: 1.1.6 ssri: 8.0.1 optionalDependencies: @@ -20960,7 +20966,7 @@ packages: glob: 7.2.3 is-glob: 4.0.3 lodash: 4.17.21 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0_typescript@3.9.10 typescript: 3.9.10 transitivePeerDependencies: @@ -20981,7 +20987,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0_typescript@4.9.5 typescript: 4.9.5 transitivePeerDependencies: @@ -21002,7 +21008,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0_typescript@5.2.2 typescript: 5.2.2 transitivePeerDependencies: @@ -21023,7 +21029,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0_typescript@5.5.2 typescript: 5.5.2 transitivePeerDependencies: @@ -21066,7 +21072,7 @@ packages: '@typescript-eslint/typescript-estree': 5.62.0_typescript@4.9.5 eslint: 8.15.0 eslint-scope: 5.1.1 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -21086,7 +21092,7 @@ packages: '@typescript-eslint/typescript-estree': 5.62.0_typescript@5.2.2 eslint: 8.15.0 eslint-scope: 5.1.1 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -27364,7 +27370,7 @@ packages: '@babel/parser': 7.23.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -27909,7 +27915,7 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -29071,7 +29077,7 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.5.4 + semver: 7.6.3 dev: true /make-error/1.3.6: @@ -30503,7 +30509,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.14.0 - semver: 7.5.4 + semver: 7.6.3 validate-npm-package-license: 3.0.4 dev: true @@ -30542,7 +30548,7 @@ packages: engines: {node: '>=10'} dependencies: hosted-git-info: 4.1.0 - semver: 7.5.4 + semver: 7.6.3 validate-npm-package-name: 3.0.0 dev: true @@ -31721,7 +31727,7 @@ packages: jsdoc: 4.0.2 minimist: 1.2.8 protobufjs: 7.2.4 - semver: 7.5.4 + semver: 7.6.3 tmp: 0.2.1 uglify-js: 3.17.4 dev: false @@ -33119,7 +33125,6 @@ packages: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true - dev: true /sendbird-platform-sdk/0.0.14_@babel+core@7.23.0: resolution: {integrity: sha512-nuVX2mwGBdMUys/c6MLOrjbTavfo34HDbrVjcjbL9UNeWXWK1hJ9/CUnxpnviCNzB9BCv4SEZhEQ2K6w4dZYoQ==} @@ -34068,7 +34073,7 @@ packages: mime: 2.6.0 qs: 6.12.0 readable-stream: 3.6.2 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: false