diff --git a/.github/workflows/pipedream-sdk-markdown-lint.yaml b/.github/workflows/pipedream-sdk-markdown-lint.yaml new file mode 100644 index 0000000000000..8332eee516a6b --- /dev/null +++ b/.github/workflows/pipedream-sdk-markdown-lint.yaml @@ -0,0 +1,24 @@ +name: Lint SDK Markdown Files + +on: + pull_request: + types: [opened, edited, synchronize] + paths: + - 'packages/sdk/**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Lint markdown files + uses: DavidAnson/markdownlint-cli2-action@v17 + with: + globs: 'packages/sdk/**/*.md' diff --git a/.github/workflows/pipedream-sdk-test.yaml b/.github/workflows/pipedream-sdk-test.yaml index 6cf15d4048966..32abb9bfbc939 100644 --- a/.github/workflows/pipedream-sdk-test.yaml +++ b/.github/workflows/pipedream-sdk-test.yaml @@ -6,13 +6,17 @@ on: paths: - 'packages/sdk/**' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/packages/sdk/.tool-versions b/packages/sdk/.tool-versions new file mode 100644 index 0000000000000..38ba13262919f --- /dev/null +++ b/packages/sdk/.tool-versions @@ -0,0 +1 @@ +nodejs 22.10.0 diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md new file mode 100644 index 0000000000000..e02e531f4cdf7 --- /dev/null +++ b/packages/sdk/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## [1.0.0-rc.1] - 2024-11-01 + +### Changed + +- Renamed the server-side client class from `ServerClient` to `BackendClient` to + better indicate its purpose. +- Removed the `oauthClient` optional argument to the `BackendClient` constructor +- Renamed the client factory methods so that developers can know exactly the + kind of client they are creating. +- Removed project-key-based authentication in favor of a more secure + token-based authentication using OAuth. diff --git a/packages/sdk/CONTRIBUTING.md b/packages/sdk/CONTRIBUTING.md new file mode 100644 index 0000000000000..db2171db77866 --- /dev/null +++ b/packages/sdk/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# How to Contribute to the SDK + +## Local Environment Setup + +### Global Dependencies + +Clone the repo (ideally your own fork of it) and initialize the global +dependencies like Node.js, NPM, etc. We use [`asdf`](https://asdf-vm.com/) to +manage these, so assuming we're located in this directory (i.e. `packages/sdk/`) +we can run the following command to install them: + +```shell +asdf install +``` + +If you prefer to use another tool make sure you use the same versions that are +specified in the `.tool-versions` files [in this current directory, and +recursively going up until you reach the root of the +repo](https://asdf-vm.com/guide/getting-started.html#_6-set-a-version). + +### Local Dependencies + +You can install the package's dependencies by using `pnpm` or `npm`: + +```shell +pnpm install +``` + +Since other packages in this repository already use `pnpm`, we recommend you use +it in this case too to keep your `node_modules` footprint low. + +## Build the Package + +There's a script that you can invoke with `pnpm` to build the package artifacts: + +```shell +pnpm build +``` + +You can also watch the code for changes, and automatically build a new artifact +after each change with the `watch` script: + +```shell +pnpm watch +``` + +### Use the Package + +You can use pnpm's `link` command to point other code to your local version of +this package during development. This lets you test the SDK in other local apps, +end-to-end. + +In this `packages/sdk/` directory: + +```shell +pnpm link --global +``` + +> [!NOTE] +When using the version of Node.js specified in +[`.tool-versions`](./.tool-versions) (via `asdf`), the command above will +install the package in the `asdf` Node.js environment. To use this package +elsewhere, you'll need to use the same version of Node.js. Please reference the +latest version of [the `.tool-versions` file](./.tool-versions) and add that to +the `.tool-versions` file in your local project where you'd like to use the SDK. + +For example, in your app's directory: + +```shell +# Replace /path/to/pipedream with the actual path to this repository +grep nodejs /path/to/pipedream/packages/sdk/.tool-versions >> .tool-versions +asdf install +pnpm install @pipedream/sdk +``` + +Then, link the SDK package to it's local path: + +```shell +pnpm link @pipedream/sdk +``` + +To confirm you successfully installed the correct version of the SDK, and that +it's tied to your local copy of the Pipedream SDK: + +```shell +ls -l node_modules/@pipedream +``` + +You should see an output like this one (notice the last line): + +```text +total 0 +drwxr-xr-x 9 jay staff 288 30 Oct 14:01 mysql +drwxr-xr-x 10 jay staff 320 30 Oct 14:01 platform +lrwxr-xr-x 1 jay staff 31 30 Oct 14:06 sdk -> ../../../pipedream/packages/sdk +``` diff --git a/packages/sdk/README.md b/packages/sdk/README.md index e757d0e98ab55..f9438aaaeb30b 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,6 +1,7 @@ # `@pipedream/sdk` -TypeScript SDK for [Pipedream](https://pipedream.com). [See the docs](https://pipedream.com/docs/connect) for usage instructions. +TypeScript SDK for [Pipedream](https://pipedream.com). [See the +docs](https://pipedream.com/docs/connect) for usage instructions. ## Install @@ -10,12 +11,39 @@ npm i @pipedream/sdk ## Quickstart -The [quickstart](https://pipedream.com/docs/connect/quickstart) is the easiest way to get started with the SDK and Pipedream Connect. +The [quickstart](https://pipedream.com/docs/connect/quickstart) is the easiest +way to get started with the SDK and Pipedream Connect. ## Usage -[See the SDK docs](https://pipedream.com/docs/connect) for full usage instructions and examples for each method. +[See the SDK docs](https://pipedream.com/docs/connect) for full usage +instructions and examples for each method. -## Example app +## Example App -Clone and run the [example app](https://github.com/PipedreamHQ/pipedream-connect-examples/) to get started. +Clone and run the [example +app](https://github.com/PipedreamHQ/pipedream-connect-examples/) to get started. + +## Importing the Client + +You can import the SDK from the root package name, and it will automatically +load the appropriate code depending on the environment (e.g. Node.js server, +browser, etc.). + +### CommonJS Modules + +```javascript +const { createClient } = require("@pipedream/sdk"); +``` + +### ES Modules + +```javascript +import { createClient } from "@pipedream/sdk"; +``` + +### Browser + +```javascript +import { createClient } from "@pipedream/sdk"; +``` diff --git a/packages/sdk/jest.setup.js b/packages/sdk/jest.setup.js index 202df059de11e..a9e3dad0c9c83 100644 --- a/packages/sdk/jest.setup.js +++ b/packages/sdk/jest.setup.js @@ -1,3 +1,5 @@ const fetchMock = require("jest-fetch-mock"); fetchMock.enableMocks(); global.fetch = fetchMock; + +jest.mock("simple-oauth2"); diff --git a/packages/sdk/package-lock.json b/packages/sdk/package-lock.json index 3554421b2bc08..b76751d1c47a2 100644 --- a/packages/sdk/package-lock.json +++ b/packages/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pipedream/sdk", - "version": "0.1.2", + "version": "1.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pipedream/sdk", - "version": "0.1.2", + "version": "1.0.0-rc.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "simple-oauth2": "^5.1.0" @@ -18,6 +18,7 @@ "@types/simple-oauth2": "^5.0.7", "jest": "^29.7.0", "jest-fetch-mock": "^3.0.3", + "nodemon": "^3.1.7", "ts-jest": "^29.2.5", "typescript": "^5.5.2" }, @@ -1337,6 +1338,19 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1481,6 +1495,31 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1949,6 +1988,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2000,6 +2052,13 @@ "node": ">=10.17.0" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2051,6 +2110,19 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -2066,6 +2138,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2084,6 +2166,19 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3026,6 +3121,71 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3257,6 +3417,13 @@ "node": ">= 6" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -3279,6 +3446,19 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3382,6 +3562,32 @@ "joi": "^17.6.4" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -3568,6 +3774,16 @@ "node": ">=8.0" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -3668,6 +3884,13 @@ "node": ">=14.17" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f1a81160434a7..008b282937167 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,24 +1,17 @@ { "name": "@pipedream/sdk", - "version": "0.1.9", + "version": "1.0.0", "description": "Pipedream SDK", "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" - }, + "browser": "./dist/browser/index.js", "exports": { ".": { + "browser": "./dist/browser/index.js", "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" + "default": "./dist/server/index.js" } }, "engines": { @@ -36,7 +29,8 @@ "build": "npm run build:node && npm run build:browser", "build:node": "tsc -p tsconfig.node.json", "build:browser": "tsc -p tsconfig.browser.json", - "test": "jest" + "test": "jest", + "watch": "nodemon --watch src --ext ts --exec 'npm run build'" }, "files": [ "dist" @@ -48,6 +42,7 @@ "@types/simple-oauth2": "^5.0.7", "jest": "^29.7.0", "jest-fetch-mock": "^3.0.3", + "nodemon": "^3.1.7", "ts-jest": "^29.2.5", "typescript": "^5.5.2" }, diff --git a/packages/sdk/src/browser/index.ts b/packages/sdk/src/browser/index.ts index a04c592d97e2b..74d3507770c7b 100644 --- a/packages/sdk/src/browser/index.ts +++ b/packages/sdk/src/browser/index.ts @@ -1,38 +1,34 @@ -// This code is meant to be run client-side. Never provide project keys to the browser client, -// or make API requests to the Pipedream API to fetch credentials. The browser client is -// meant for initiating browser-specific operations, like connecting accounts via Pipedream Connect. -// See the server/ directory for the server client. +// This code is meant to be run client-side. Never provide project keys to the +// browser client, or make API requests to the Pipedream API to fetch +// credentials. The browser client is meant for initiating browser-specific +// operations, like connecting accounts via Pipedream Connect. See the server/ +// directory for the server client. /** - * Options for creating a browser-side client. - * This is used to configure the BrowserClient instance. + * Options for creating a browser-side client. This is used to configure the + * BrowserClient instance. */ type CreateBrowserClientOpts = { /** - * The environment in which the browser client is running (e.g., "production", "development"). + * The environment in which the browser client is running (e.g., "production", + * "development"). */ environment?: string; /** - * The frontend host URL. Used by Pipedream employees only. Defaults to "pipedream.com" if not provided. + * The frontend host URL. Used by Pipedream employees only. Defaults to + * "pipedream.com" if not provided. */ frontendHost?: string; }; /** - * A unique identifier for an app. + * The name slug for an app, a unique, human-readable identifier like "github" + * or "google_sheets". Find this in the Authentication section for any app's + * page at https://pipedream.com/apps. For more information about name slugs, + * see https://pipedream.com/docs/connect/quickstart#find-your-apps-name-slug. */ -type AppId = string; - -/** - * Object representing an app to start connecting with. - */ -type StartConnectApp = { - /** - * The unique identifier of the app. - */ - id: AppId; -}; +type AppNameSlug = string; /** * The result of a successful connection. @@ -61,7 +57,7 @@ type StartConnectOpts = { /** * The app to connect to, either as an ID or an object containing the ID. */ - app: AppId | StartConnectApp; + app: AppNameSlug; /** * The OAuth app ID to connect to. @@ -88,14 +84,14 @@ type StartConnectOpts = { * * @example * ```typescript - * const client = createClient({ + * const client = createFrontendClient({ * environment: "production", * }); * ``` * @param opts - The options for creating the browser client. * @returns A new instance of `BrowserClient`. */ -export function createClient(opts: CreateBrowserClientOpts) { +export function createFrontendClient(opts: CreateBrowserClientOpts = {}) { return new BrowserClient(opts); } @@ -139,7 +135,7 @@ class BrowserClient { * }); * ``` */ - connectAccount(opts: StartConnectOpts) { + public connectAccount(opts: StartConnectOpts) { const onMessage = (e: MessageEvent) => { switch (e.data?.type) { case "success": @@ -168,7 +164,8 @@ class BrowserClient { } /** - * Cleans up the iframe and message event listener after the connection process is complete. + * Cleans up the iframe and message event listener after the connection + * process is complete. * * @param onMessage - The message event handler to remove. */ @@ -178,7 +175,8 @@ class BrowserClient { } /** - * Creates an iframe for the connection process and appends it to the document body. + * Creates an iframe for the connection process and appends it to the document + * body. * * @param opts - The options for starting the connection process. * diff --git a/packages/sdk/src/server/__tests__/server.test.ts b/packages/sdk/src/server/__tests__/server.test.ts index 65960ff97b801..d89e4fc6d0705 100644 --- a/packages/sdk/src/server/__tests__/server.test.ts +++ b/packages/sdk/src/server/__tests__/server.test.ts @@ -1,25 +1,56 @@ -import { - ServerClient, createClient, -} from "../index"; import fetchMock from "jest-fetch-mock"; import { ClientCredentials } from "simple-oauth2"; -describe("ServerClient", () => { - let client: ServerClient; +import { + BackendClient, + BackendClientOpts, + createBackendClient, + HTTPAuthType, +} from "../index"; - beforeEach(() => { - fetchMock.resetMocks(); +const projectId = "proj_abc123"; +const clientParams: BackendClientOpts = { + credentials: { + clientId: "test-client-id", + clientSecret: "test-client-secret", + }, + projectId, +}; + +let client: BackendClient; +let customDomainClient: BackendClient; + +beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ClientCredentials.mockImplementation(() => ({ + getToken: jest.fn().mockResolvedValue({ + token: { + access_token: "mocked-oauth-token", + }, + expired: jest.fn().mockReturnValue(false), + }), + })); + + client = new BackendClient( + clientParams, + ); + customDomainClient = new BackendClient({ + ...clientParams, + workflowDomain: "example.com", }); +}); - describe("createClient", () => { - it("should mock the createClient method and return a ServerClient instance", () => { - const params = { - publicKey: "test-public-key", - secretKey: "test-secret-key", - }; +afterEach(() => { + fetchMock.resetMocks(); + jest.clearAllMocks(); +}); - const client = createClient(params); - expect(client).toBeInstanceOf(ServerClient); +describe("BackendClient", () => { + describe("createBackendClient", () => { + it("should mock the createBackendClient method and return a BackendClient instance", () => { + const client = createBackendClient(clientParams); + expect(client).toBeInstanceOf(BackendClient); }); }); @@ -36,11 +67,6 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - const result = await client.makeRequest("/test-path", { method: "GET", }); @@ -66,11 +92,6 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - const result = await client.makeRequest("/test-path", { method: "POST", body: { @@ -103,11 +124,6 @@ describe("ServerClient", () => { }, }); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - await expect(client.makeRequest("/bad-path")).rejects.toThrow("HTTP error! status: 404, body: Not Found"); expect(fetchMock).toHaveBeenCalledWith( "https://api.pipedream.com/v1/bad-path", @@ -116,30 +132,8 @@ describe("ServerClient", () => { }); }); - describe("makeApiRequest", () => { + describe("makeAuthorizedRequest", () => { it("should include OAuth Authorization header and make an API request", async () => { - const getTokenMock = jest.fn().mockResolvedValue({ - token: { - access_token: "mocked-oauth-token", - }, - expired: jest.fn().mockReturnValue(false), - }); - - const oauthClientMock = { - getToken: getTokenMock, - } as unknown as ClientCredentials; - - // Inject the mock oauthClient into the ServerClient instance - client = new ServerClient( - { - publicKey: "test-public-key", - secretKey: "test-secret-key", - oauthClientId: "test-client-id", - oauthClientSecret: "test-client-secret", - }, - oauthClientMock, - ); - fetchMock.mockResponseOnce( JSON.stringify({ success: true, @@ -151,7 +145,7 @@ describe("ServerClient", () => { }, ); - const result = await client["makeApiRequest"]("/test-path"); + const result = await client["makeAuthorizedRequest"]("/test-path"); expect(result).toEqual({ success: true, @@ -168,34 +162,31 @@ describe("ServerClient", () => { }); it("should handle OAuth token retrieval failure", async () => { - // Create a mock oauthClient that fails to get a token - const getTokenMock = jest.fn().mockRejectedValue(new Error("Invalid credentials")); - - const oauthClientMock = { - getToken: getTokenMock, - } as unknown as ClientCredentials; - - client = new ServerClient( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ClientCredentials.mockImplementation(() => ({ + getToken: jest.fn().mockRejectedValue(new Error("Invalid credentials")), + })); + + // Need to create a new client instance to use the new mock implementation + const client = new BackendClient( { - publicKey: "test-public-key", - secretKey: "test-secret-key", - oauthClientId: "test-client-id", - oauthClientSecret: "test-client-secret", + credentials: { + clientId: "test-client-id", + clientSecret: "test-client-secret", + }, + projectId, }, - oauthClientMock, ); - await expect(client["makeApiRequest"]("/test-path")).rejects.toThrow("Failed to obtain OAuth token: Invalid credentials"); + await expect(client.makeAuthorizedRequest("/test-path")).rejects.toThrow( + "Failed to obtain OAuth token: Invalid credentials", + ); }); }); describe("makeConnectRequest", () => { it("should include Connect Authorization header and make a request", async () => { - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - fetchMock.mockResponseOnce( JSON.stringify({ success: true, @@ -213,17 +204,17 @@ describe("ServerClient", () => { success: true, }); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/test-path", + `https://api.pipedream.com/v1/connect/${projectId}/test-path`, expect.objectContaining({ headers: expect.objectContaining({ - "Authorization": expect.stringContaining("Basic "), + "Authorization": expect.stringContaining("Bearer "), }), }), ); }); }); - describe("connectTokenCreate", () => { + describe("createConnectToken", () => { it("should create a connect token", async () => { fetchMock.mockResponseOnce( JSON.stringify({ @@ -237,12 +228,7 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - - const result = await client.connectTokenCreate({ + const result = await client.createConnectToken({ external_user_id: "user-id", }); @@ -251,7 +237,7 @@ describe("ServerClient", () => { expires_at: "2024-01-01T00:00:00Z", }); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/tokens", + `https://api.pipedream.com/v1/connect/${projectId}/tokens`, expect.objectContaining({ method: "POST", body: JSON.stringify({ @@ -279,12 +265,7 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - - const result = await client.connectTokenCreate({ + const result = await client.createConnectToken({ external_user_id: "user-id", success_redirect_uri: "https://example.com/success", error_redirect_uri: "https://example.com/error", @@ -295,7 +276,7 @@ describe("ServerClient", () => { expires_at: "2024-01-01T00:00:00Z", }); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/tokens", + `https://api.pipedream.com/v1/connect/${projectId}/tokens`, expect.objectContaining({ method: "POST", body: JSON.stringify({ @@ -329,13 +310,8 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - const result = await client.getAccounts({ - include_credentials: 1, + include_credentials: true, }); expect(result).toEqual([ @@ -345,13 +321,13 @@ describe("ServerClient", () => { }, ]); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/accounts?include_credentials=1", + `https://api.pipedream.com/v1/connect/${projectId}/accounts?include_credentials=true`, expect.any(Object), ); }); }); - describe("getAccount", () => { + describe("getAccountById", () => { it("should retrieve a specific account by ID", async () => { fetchMock.mockResponseOnce( JSON.stringify({ @@ -365,25 +341,20 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - - const result = await client.getAccount("account-1"); + const result = await client.getAccountById("account-1"); expect(result).toEqual({ id: "account-1", name: "Test Account", }); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/accounts/account-1", + `https://api.pipedream.com/v1/connect/${projectId}/accounts/account-1`, expect.any(Object), ); }); }); - describe("getAccountsByApp", () => { + describe("Get accounts by app", () => { it("should retrieve accounts associated with a specific app", async () => { fetchMock.mockResponseOnce( JSON.stringify([ @@ -399,13 +370,10 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", + const result = await client.getAccounts({ + app: "app-1", }); - const result = await client.getAccountsByApp("app-1"); - expect(result).toEqual([ { id: "account-1", @@ -413,13 +381,13 @@ describe("ServerClient", () => { }, ]); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/accounts/app/app-1", + `https://api.pipedream.com/v1/connect/${projectId}/accounts?app=app-1`, expect.any(Object), ); }); }); - describe("getAccountsByExternalId", () => { + describe("Get accounts by external user ID", () => { it("should retrieve accounts associated with a specific external ID", async () => { fetchMock.mockResponseOnce( JSON.stringify([ @@ -435,13 +403,10 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", + const result = await client.getAccounts({ + external_user_id: "external-id-1", }); - const result = await client.getAccountsByExternalId("external-id-1"); - expect(result).toEqual([ { id: "account-1", @@ -449,7 +414,7 @@ describe("ServerClient", () => { }, ]); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/users/external-id-1/accounts", + `https://api.pipedream.com/v1/connect/${projectId}/accounts?external_user_id=external-id-1`, expect.any(Object), ); }); @@ -461,15 +426,10 @@ describe("ServerClient", () => { status: 204, }); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - await client.deleteAccount("account-1"); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/accounts/account-1", + `https://api.pipedream.com/v1/connect/${projectId}/accounts/account-1`, expect.objectContaining({ method: "DELETE", }), @@ -483,15 +443,10 @@ describe("ServerClient", () => { status: 204, }); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - await client.deleteAccountsByApp("app-1"); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/accounts/app/app-1", + `https://api.pipedream.com/v1/connect/${projectId}/accounts/app/app-1`, expect.objectContaining({ method: "DELETE", }), @@ -505,15 +460,10 @@ describe("ServerClient", () => { status: 204, }); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - await client.deleteExternalUser("external-id-1"); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/users/external-id-1", + `https://api.pipedream.com/v1/connect/${projectId}/users/external-id-1`, expect.objectContaining({ method: "DELETE", }), @@ -539,11 +489,6 @@ describe("ServerClient", () => { }, ); - client = new ServerClient({ - publicKey: "test-public-key", - secretKey: "test-secret-key", - }); - const result = await client.getProjectInfo(); expect(result).toEqual({ @@ -555,7 +500,7 @@ describe("ServerClient", () => { ], }); expect(fetchMock).toHaveBeenCalledWith( - "https://api.pipedream.com/v1/connect/projects/info", + `https://api.pipedream.com/v1/connect/${projectId}/projects/info`, expect.objectContaining({ method: "GET", }), @@ -564,30 +509,14 @@ describe("ServerClient", () => { }); describe("invokeWorkflow", () => { - it("should invoke a workflow with provided URL and body", async () => { - // Create a mock oauthClient - const getTokenMock = jest.fn().mockResolvedValue({ - token: { - access_token: "mocked-oauth-token", - }, - expired: jest.fn().mockReturnValue(false), + beforeEach(() => { + client = new BackendClient({ + ...clientParams, + workflowDomain: "example.com", }); + }); - const oauthClientMock = { - getToken: getTokenMock, - } as unknown as ClientCredentials; - - // Inject the mock oauthClient into the ServerClient instance - client = new ServerClient( - { - publicKey: "test-public-key", - secretKey: "test-secret-key", - oauthClientId: "test-client-id", - oauthClientSecret: "test-client-secret", - }, - oauthClientMock, - ); - + it("should invoke a workflow with provided URL and body, with no auth type", async () => { fetchMock.mockResponseOnce( JSON.stringify({ result: "workflow-response", @@ -616,7 +545,68 @@ describe("ServerClient", () => { foo: "bar", }), headers: expect.objectContaining({ - Authorization: "Bearer mocked-oauth-token", + "Content-Type": "application/json", + "X-PD-Environment": "production", + }), + }), + ); + }); + + it("should invoke a workflow with OAuth auth type", async () => { + fetchMock.mockResponseOnce( + JSON.stringify({ + result: "workflow-response", + }), + { + headers: { + "Content-Type": "application/json", + }, + }, + ); + + const result = await client.invokeWorkflow("https://example.com/workflow", {}, HTTPAuthType.OAuth); + + expect(result).toEqual({ + result: "workflow-response", + }); + + expect(fetchMock).toHaveBeenCalledWith( + "https://example.com/workflow", + expect.objectContaining({ + headers: expect.objectContaining({ + "Authorization": "Bearer mocked-oauth-token", + }), + }), + ); + }); + + it("should invoke a workflow with static bearer auth type", async () => { + fetchMock.mockResponseOnce( + JSON.stringify({ + result: "workflow-response", + }), + { + headers: { + "Content-Type": "application/json", + }, + }, + ); + + const result = await client.invokeWorkflow("https://example.com/workflow", { + headers: { + "Authorization": "Bearer static-token", + }, + }, HTTPAuthType.StaticBearer); + + expect(result).toEqual({ + result: "workflow-response", + }); + + expect(fetchMock).toHaveBeenCalledWith( + "https://example.com/workflow", + expect.objectContaining({ + headers: expect.objectContaining({ + "Authorization": "Bearer static-token", }), }), ); @@ -645,18 +635,21 @@ describe("ServerClient", () => { .mockResolvedValueOnce(expiredTokenMock) .mockResolvedValueOnce(newTokenMock); - const oauthClientMock = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ClientCredentials.mockImplementation(() => ({ getToken: getTokenMock, - } as unknown as ClientCredentials; + })); - const client = new ServerClient( + // Need to create a new client instance to use the new mock implementation + const client = new BackendClient( { - publicKey: "test-public-key", - secretKey: "test-secret-key", - oauthClientId: "test-client-id", - oauthClientSecret: "test-client-secret", + credentials: { + clientId: "test-client-id", + clientSecret: "test-client-secret", + }, + projectId: "proj_abc123", }, - oauthClientMock, ); fetchMock.mockResponse( @@ -671,7 +664,7 @@ describe("ServerClient", () => { ); // First request will get the expired token and fetch a new one - const result1 = await client["makeApiRequest"]("/test-path"); + const result1 = await client["makeAuthorizedRequest"]("/test-path"); expect(result1).toEqual({ success: true, @@ -687,4 +680,154 @@ describe("ServerClient", () => { ); }); }); + + describe("invokeWorkflowForExternalUser", () => { + let client: BackendClient; + + beforeEach(() => { + client = new BackendClient({ + ...clientParams, + workflowDomain: "example.com", + }); + }); + + it("should include externalUserId and environment headers", async () => { + fetchMock.mockResponseOnce( + JSON.stringify({ + result: "workflow-response", + }), + { + headers: { + "Content-Type": "application/json", + }, + }, + ); + + const result = await client.invokeWorkflowForExternalUser("https://example.com/workflow", "external-user-id", { + body: { + foo: "bar", + }, + }); + + expect(result).toEqual({ + result: "workflow-response", + }); + + expect(fetchMock).toHaveBeenCalledWith( + "https://example.com/workflow", + expect.objectContaining({ + headers: expect.objectContaining({ + "X-PD-External-User-ID": "external-user-id", + "X-PD-Environment": "production", + }), + }), + ); + }); + + it("should throw error when externalUserId is missing", async () => { + await expect(client.invokeWorkflowForExternalUser("https://example.com/workflow", "", { + body: { + foo: "bar", + }, + })).rejects.toThrow("External user ID is required"); + }); + + it("should throw error when externalUserId is blank", async () => { + await expect(client.invokeWorkflowForExternalUser("https://example.com/workflow", " ", { + body: { + foo: "bar", + }, + })).rejects.toThrow("External user ID is required"); + }); + + it("should throw error when the URL is blank", async () => { + await expect(client.invokeWorkflowForExternalUser(" ", "external-user-id", { + body: { + foo: "bar", + }, + })).rejects.toThrow("Workflow URL is required"); + }); + }); + + describe("BackendClient - buildWorkflowUrl", () => { + describe("Validations", () => { + it("should throw an error when the input is blank", () => { + expect(() => client["buildWorkflowUrl"](" ")).toThrow("URL or endpoint ID is required"); + }); + + it("should throw an error when the URL doesn't match the workflow domain", () => { + const url = "https://example.com"; + expect(() => client["buildWorkflowUrl"](url)).toThrow("Invalid workflow domain"); + }); + + it("should throw an error when the endpoint ID doesn't match the expected format", () => { + const input = "foo123"; + expect(() => client["buildWorkflowUrl"](input)).toThrow("Invalid endpoint ID format"); + }); + }); + + describe("Default domain (m.pipedream.net)", () => { + it("should return full URL if input is a full URL with protocol", () => { + const input = "https://en123.m.pipedream.net"; + const expected = "https://en123.m.pipedream.net/"; + expect(client["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should return full URL if input is a URL without protocol", () => { + const input = "en123.m.pipedream.net"; + const expected = "https://en123.m.pipedream.net/"; + expect(client["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should construct URL with 'm.pipedream.net' if input is an endpoint ID", () => { + const input = "en123"; + const expected = "https://en123.m.pipedream.net"; + expect(client["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should handle input with a path in full URL with protocol", () => { + const input = "https://en123.m.pipedream.net/foo"; + const expected = "https://en123.m.pipedream.net/foo"; + expect(client["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should handle input with a path when no protocol is provided", () => { + const input = "en123.m.pipedream.net/foo"; + const expected = "https://en123.m.pipedream.net/foo"; + expect(client["buildWorkflowUrl"](input)).toBe(expected); + }); + }); + + describe("Custom domain (example.com)", () => { + it("should return full URL if input is a full URL with protocol", () => { + const input = "https://en123.example.com"; + const expected = "https://en123.example.com/"; + expect(customDomainClient["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should return full URL if input is a URL without protocol", () => { + const input = "en123.example.com"; + const expected = "https://en123.example.com/"; + expect(customDomainClient["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should construct URL with 'example.com' if input is an endpoint ID", () => { + const input = "en123"; + const expected = "https://en123.example.com"; + expect(customDomainClient["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should handle input with a path in full URL with protocol", () => { + const input = "https://en123.example.com/foo"; + const expected = "https://en123.example.com/foo"; + expect(customDomainClient["buildWorkflowUrl"](input)).toBe(expected); + }); + + it("should handle input with a path when no protocol is provided", () => { + const input = "en123.example.com/foo"; + const expected = "https://en123.example.com/foo"; + expect(customDomainClient["buildWorkflowUrl"](input)).toBe(expected); + }); + }); + }); }); diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts index e961b9622f797..224ace2bd3391 100644 --- a/packages/sdk/src/server/index.ts +++ b/packages/sdk/src/server/index.ts @@ -7,41 +7,62 @@ import { ClientCredentials, } from "simple-oauth2"; +/** + * OAuth credentials for your Pipedream account, containing client ID and + * secret. + */ +export type OAuthCredentials = { + clientId: string; + clientSecret: string; +}; + /** * Options for creating a server-side client. - * This is used to configure the ServerClient instance. + * This is used to configure the BackendClient instance. */ -export type CreateServerClientOpts = { +export type BackendClientOpts = { /** - * The public API key for accessing the service. + * The environment in which the server client is running (e.g., "production", + * "development"). */ - publicKey?: string; + environment?: string; /** - * The secret API key for accessing the service. + * The credentials to use for authentication against the Pipedream API. */ - secretKey?: string; + credentials: OAuthCredentials; /** - * The client ID of your workspace's OAuth application. + * The base project ID tied to relevant API requests */ - oauthClientId?: string; + projectId: string; /** - * The client secret of your workspace's OAuth application. + * The API host URL. Used by Pipedream employees. Defaults to + * "api.pipedream.com" if not provided. */ - oauthClientSecret?: string; + apiHost?: string; /** - * The API host URL. Used by Pipedream employees. Defaults to "api.pipedream.com" if not provided. + * Base domain for workflows. Used for custom domains: + * https://pipedream.com/docs/workflows/domains */ - apiHost?: string; + workflowDomain?: string; }; +/** + * Different ways in which customers can authorize requests to HTTP endpoints + */ +export const enum HTTPAuthType { + None = "none", + StaticBearer = "static_bearer_token", + OAuth = "oauth" +} + /** * Options for creating a Connect token. */ -export type ConnectTokenCreateOpts = { +export type ConnectTokenOpts = { /** * An external user ID associated with the token. */ @@ -58,13 +79,14 @@ export type ConnectTokenCreateOpts = { error_redirect_uri?: string; /** - * An optional webhook uri that Pipedream can invoke on success or failure of connection requests. + * An optional webhook uri that Pipedream can invoke on success or failure of + * connection requests. */ webhook_uri?: string; /** - * Specify the environment ('production' or 'development') to use for the account connection flow. - * Defaults to 'production'. + * Specify the environment ("production" or "development") to use for the + * account connection flow. Defaults to "production". */ environment_name?: string; }; @@ -76,13 +98,14 @@ export type AppInfo = { id?: string; /** - * https://pipedream.com/docs/connect/quickstart#find-your-apps-name-slug + * The name slug of the target app (see + * https://pipedream.com/docs/connect/quickstart#find-your-apps-name-slug) */ name_slug: string; }; /** - * Response received after requesting project info. + * Response received after requesting a project's info. */ export type ProjectInfoResponse = { /** @@ -104,6 +127,7 @@ export type ConnectTokenResponse = { * The expiration time of the token in ISO 8601 format. */ expires_at: string; + /** * The Connect Link URL */ @@ -111,38 +135,18 @@ export type ConnectTokenResponse = { }; /** - * Parameters for the Connect API. + * The types of authentication that Pipedream apps support. */ -export type ConnectParams = { - /** - * Whether to include credentials in the request (1 to include, 0 to exclude). - */ - include_credentials?: number; -}; - -/** - * The authentication type for the app. - */ -export enum AuthType { +export const enum AppAuthType { OAuth = "oauth", Keys = "keys", None = "none", } /** - * Response object for Pipedream app metadata + * Response object for a Pipedream app's metadata */ -export type AppResponse = { - /** - * The unique ID of the app. - */ - id: string; - - /** - * https://pipedream.com/docs/connect/quickstart#find-your-apps-name-slug - */ - name_slug: string; - +export type AppResponse = AppInfo & { /** * The human-readable name of the app. */ @@ -151,7 +155,7 @@ export type AppResponse = { /** * The authentication type used by the app. */ - auth_type: AuthType; + auth_type: AppAuthType; /** * The URL to the app's logo. @@ -170,28 +174,31 @@ export type AppResponse = { }; /** - * Options for creating a connected account. + * Parameters for the retrieval of accounts from the Connect API */ -export type CreateAccountOpts = { +export type GetAccountOpts = { /** - * https://pipedream.com/docs/connect/quickstart#find-your-apps-name-slug + * The ID or name slug of the app, in case you want to only retrieve the + * accounts for a specific app. */ - app_slug: string; + app?: string; /** - * The connect token used to authenticate the account creation. + * The ID of the app (if it's an OAuth app), in case you want to only retrieve + * the accounts for a specific app. */ - connect_token: string; + oauth_app_id?: string; /** - * A JSON string representing a map of custom fields for the account. + * The external user ID associated with the account, in case you want to only + * retrieve the accounts of a specific user. */ - cfmap_json: string; + external_user_id?: string; /** - * The name of the account. + * Whether to retrieve the account's credentials or not. */ - name?: string; + include_credentials?: boolean; }; /** @@ -214,7 +221,8 @@ export type Account = { external_id: string; /** - * Indicates if the account is healthy. Pipedream will periodically retry token refresh and test requests for unhealthy accounts. + * Indicates if the account is healthy. Pipedream will periodically retry + * token refresh and test requests for unhealthy accounts. */ healthy: boolean; @@ -234,12 +242,14 @@ export type Account = { created_at: string; /** - * The date and time the account was last updated, an ISO 8601 formatted string. + * The date and time the account was last updated, an ISO 8601 formatted + * string. */ updated_at: string; /** - * The credentials associated with the account, if include_credentials was true. + * The credentials associated with the account, if the `include_credentials` + * parameter was set to true in the request. */ credentials?: Record; }; @@ -285,75 +295,77 @@ interface RequestOptions extends Omit { } /** - * Creates a new instance of ServerClient with the provided options. + * Creates a new instance of BackendClient with the provided options. * * @example * * ```typescript - * const client = createClient({ - * publicKey: "your-public-key", - * secretKey: "your-secret-key", + * const client = createBackendClient({ + * credentials: { + * clientId: "your-client-id", + * clientSecret: "your-client-secret", + * }, * }); * ``` * * @param opts - The options for creating the server client. - * @returns A new instance of ServerClient. + * @returns A new instance of BackendClient. */ -export function createClient(opts: CreateServerClientOpts) { - return new ServerClient(opts); +export function createBackendClient(opts: BackendClientOpts) { + return new BackendClient(opts); } /** * A client for interacting with the Pipedream Connect API on the server-side. */ -export class ServerClient { - private secretKey?: string; - private publicKey?: string; - private oauthClient?: ClientCredentials; +export class BackendClient { + private environment: string; + private oauthClient: ClientCredentials; private oauthToken?: AccessToken; - private readonly baseURL: string; + private projectId: string; + private readonly baseApiUrl: string; + private readonly workflowDomain: string; /** - * Constructs a new ServerClient instance. + * Constructs a new BackendClient instance. * * @param opts - The options for configuring the server client. - * @param oauthClient - An optional OAuth client to use for authentication in tests */ - constructor( - opts: CreateServerClientOpts, - oauthClient?: ClientCredentials, - ) { - this.secretKey = opts.secretKey; - this.publicKey = opts.publicKey; - - const { apiHost = "api.pipedream.com" } = opts; - this.baseURL = `https://${apiHost}/v1`; + constructor(opts: BackendClientOpts) { + this.environment = opts.environment ?? "production"; - if (oauthClient) { - // Use the provided OAuth client (useful for testing) - this.oauthClient = oauthClient; - } else { - // Configure the OAuth client normally - this.configureOauthClient(opts, this.baseURL); + this.projectId = opts.projectId; + if (!this.projectId) { + throw new Error("Project ID is required"); } + + const { + apiHost = "api.pipedream.com", + workflowDomain = "m.pipedream.net", + } = opts; + this.baseApiUrl = `https://${apiHost}/v1`; + this.workflowDomain = workflowDomain; + + this.oauthClient = this.newOauthClient(opts.credentials, this.baseApiUrl); } - private configureOauthClient( + private newOauthClient( { - oauthClientId: id, - oauthClientSecret: secret, - }: CreateServerClientOpts, + clientId, + clientSecret, + }: OAuthCredentials, tokenHost: string, ) { - if (!id || !secret) { - return; + if (!clientId || !clientSecret) { + throw new Error("OAuth client ID and secret are required"); } - this.oauthClient = new ClientCredentials({ - client: { - id, - secret, - }, + const client = { + id: clientId, + secret: clientSecret, + }; + return new ClientCredentials({ + client, auth: { tokenHost, tokenPath: "/v1/oauth/token", @@ -361,17 +373,6 @@ export class ServerClient { }); } - /** - * Generates an Authorization header for Connect using the public and secret - * keys of the target project. - * - * @returns The authorization header as a string. - */ - private connectAuthorizationHeader(): string { - const encoded = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64"); - return `Basic ${encoded}`; - } - private async oauthAuthorizationHeader(): Promise { if (!this.oauthClient) { throw new Error("OAuth client not configured"); @@ -420,7 +421,7 @@ export class ServerClient { headers: customHeaders, body, method = "GET", - baseURL = this.baseURL, + baseURL = this.baseApiUrl, ...fetchOpts } = opts; @@ -437,8 +438,9 @@ export class ServerClient { } } - const headers = { + const headers: Record = { ...customHeaders, + "X-PD-Environment": this.environment, }; let processedBody: string | Buffer | URLSearchParams | FormData | null = null; @@ -491,47 +493,25 @@ export class ServerClient { * @template T - The expected response type. * @param path - The API endpoint path. * @param opts - The options for the request. - * @param authType - The type of authorization to use ("oauth" or "connect"). * @returns A promise resolving to the API response. * @throws Will throw an error if the response status is not OK. */ - private async makeAuthorizedRequest( + public async makeAuthorizedRequest( path: string, opts: RequestOptions = {}, - authType: "oauth" | "connect" = "oauth", ): Promise { const headers: Record = { "Content-Type": "application/json", ...opts.headers, + "Authorization": await this.oauthAuthorizationHeader(), }; - if (authType === "oauth") { - headers["Authorization"] = await this.oauthAuthorizationHeader(); - } else { - headers["Authorization"] = this.connectAuthorizationHeader(); - } - - return this.makeRequest(path, { + return this.makeRequest(path, { headers, ...opts, }); } - /** - * Makes a request to the Pipedream API using OAuth authorization. - * - * @template T - The expected response type. - * @param path - The API endpoint path. - * @param opts - The options for the request. - * @returns A promise resolving to the API response. - */ - private async makeApiRequest( - path: string, - opts: RequestOptions = {}, - ): Promise { - return this.makeAuthorizedRequest(path, opts, "oauth"); - } - /** * Makes a request to the Connect API using Connect authorization. * @@ -540,54 +520,55 @@ export class ServerClient { * @param opts - The options for the request. * @returns A promise resolving to the API response. */ - private async makeConnectRequest( + private makeConnectRequest( path: string, opts: RequestOptions = {}, ): Promise { - return this.makeAuthorizedRequest(`/connect${path}`, opts, "connect"); + const fullPath = `/connect/${this.projectId}${path}`; + return this.makeAuthorizedRequest(fullPath, opts); } /** - * Creates a new connect token. + * Creates a new Pipedream Connect token. See + * https://pipedream.com/docs/connect/quickstart#connect-to-the-pipedream-api-from-your-server-and-create-a-token * * @param opts - The options for creating the connect token. * @returns A promise resolving to the connect token response. * * @example - * * ```typescript - * const tokenResponse = await client.connectTokenCreate({ + * const tokenResponse = await client.createConnectToken({ * external_user_id: "external-user-id", * }); * console.log(tokenResponse.token); * ``` */ - public async connectTokenCreate(opts: ConnectTokenCreateOpts): Promise { + public createConnectToken(opts: ConnectTokenOpts): Promise { const body = { ...opts, external_id: opts.external_user_id, }; - return this.makeConnectRequest("/tokens", { + return this.makeConnectRequest("/tokens", { method: "POST", body, }); } /** - * Retrieves a list of accounts. + * Retrieves the list of accounts associated with the project. * * @param params - The query parameters for retrieving accounts. * @returns A promise resolving to a list of accounts. * * @example - * * ```typescript * const accounts = await client.getAccounts({ include_credentials: 1 }); * console.log(accounts); * ``` */ - public async getAccounts(params: ConnectParams = {}): Promise { - return this.makeConnectRequest("/accounts", { + public getAccounts(params: GetAccountOpts = {}): Promise { + return this.makeConnectRequest("/accounts", { + method: "GET", params, }); } @@ -596,59 +577,17 @@ export class ServerClient { * Retrieves a specific account by ID. * * @param accountId - The ID of the account to retrieve. - * @param params - The query parameters for retrieving the account. * @returns A promise resolving to the account. * * @example - * * ```typescript - * const account = await client.getAccount("account-id"); + * const account = await client.getAccountById("account-id"); * console.log(account); * ``` */ - public async getAccount(accountId: string, params: ConnectParams = {}): Promise { - return this.makeConnectRequest(`/accounts/${accountId}`, { - params, - }); - } - - /** - * Retrieves accounts associated with a specific app. - * - * @param appId - The ID of the app. - * @param params - The query parameters for retrieving accounts. - * @returns A promise resolving to a list of accounts. - * - * @example - * - * ```typescript - * const accounts = await client.getAccountsByApp("app-id"); - * console.log(accounts); - * ``` - */ - public async getAccountsByApp(appId: string, params: ConnectParams = {}): Promise { - return this.makeConnectRequest(`/accounts/app/${appId}`, { - params, - }); - } - - /** - * Retrieves accounts associated with a specific external ID. - * - * @param externalId - The external ID associated with the accounts. - * @param params - The query parameters for retrieving accounts. - * @returns A promise resolving to a list of accounts. - * - * @example - * - * ```typescript - * const accounts = await client.getAccountsByExternalId("external-id"); - * console.log(accounts); - * ``` - */ - public async getAccountsByExternalId(externalId: string, params: ConnectParams = {}): Promise { - return this.makeConnectRequest(`/users/${externalId}/accounts`, { - params, + public getAccountById(accountId: string): Promise { + return this.makeConnectRequest(`/accounts/${accountId}`, { + method: "GET", }); } @@ -659,14 +598,13 @@ export class ServerClient { * @returns A promise resolving when the account is deleted. * * @example - * * ```typescript * await client.deleteAccount("account-id"); * console.log("Account deleted"); * ``` */ - public async deleteAccount(accountId: string): Promise { - await this.makeConnectRequest(`/accounts/${accountId}`, { + public deleteAccount(accountId: string): Promise { + return this.makeConnectRequest(`/accounts/${accountId}`, { method: "DELETE", }); } @@ -678,14 +616,13 @@ export class ServerClient { * @returns A promise resolving when all accounts are deleted. * * @example - * * ```typescript * await client.deleteAccountsByApp("app-id"); * console.log("All accounts deleted"); * ``` */ - public async deleteAccountsByApp(appId: string): Promise { - await this.makeConnectRequest(`/accounts/app/${appId}`, { + public deleteAccountsByApp(appId: string): Promise { + return this.makeConnectRequest(`/accounts/app/${appId}`, { method: "DELETE", }); } @@ -697,52 +634,126 @@ export class ServerClient { * @returns A promise resolving when all accounts are deleted. * * @example - * * ```typescript * await client.deleteExternalUser("external-id"); * console.log("All accounts deleted"); * ``` */ - public async deleteExternalUser(externalId: string): Promise { - await this.makeConnectRequest(`/users/${externalId}`, { + public deleteExternalUser(externalId: string): Promise { + return this.makeConnectRequest(`/users/${externalId}`, { method: "DELETE", }); } /** - * Retrieves project information. + * Retrieves the project's information, such as the list of apps linked to it. * * @returns A promise resolving to the project info response. * * @example - * * ```typescript * const projectInfo = await client.getProjectInfo(); * console.log(projectInfo); * ``` */ - public async getProjectInfo(): Promise { - return this.makeConnectRequest("/projects/info", { + public getProjectInfo(): Promise { + return this.makeConnectRequest("/projects/info", { method: "GET", }); } + /** + * Builds a full workflow URL based on the input. + * + * @param input - Either a full URL (with or without protocol) or just an + * endpoint ID. + * @returns The fully constructed URL. + * @throws If the input is a malformed URL, throws an error with a clear + * message. + * + * @example + * ```typescript + * // Full URL input + * this.buildWorkflowUrl("https://en123.m.pipedream.net"); + * // Returns: "https://en123.m.pipedream.net" + * ``` + * + * @example + * ```typescript + * // Partial URL (without protocol) + * this.buildWorkflowUrl("en123.m.pipedream.net"); + * // Returns: "https://en123.m.pipedream.net" + * ``` + * + * @example + * ```typescript + * // ID only input + * this.buildWorkflowUrl("en123"); + * // Returns: "https://en123.yourdomain.com" (where `yourdomain.com` is set in `workflowDomain`) + * ``` + */ + private buildWorkflowUrl(input: string): string { + const sanitizedInput = input + .trim() + .replace(/[^\w-./:]/g, "") + .toLowerCase(); + if (!sanitizedInput) { + throw new Error("URL or endpoint ID is required"); + } + + let url: string; + const isUrl = sanitizedInput.includes(".") || sanitizedInput.startsWith("http"); + + if (isUrl) { + // Try to parse the input as a URL + let parsedUrl: URL; + try { + const urlString = sanitizedInput.startsWith("http") + ? sanitizedInput + : `https://${sanitizedInput}`; + parsedUrl = new URL(urlString); + } catch (error) { + throw new Error(` + The provided URL is malformed: "${sanitizedInput}". + Please provide a valid URL. + `); + } + + // Validate the hostname to prevent potential DNS rebinding attacks + if (!parsedUrl.hostname.endsWith(this.workflowDomain)) { + throw new Error(`Invalid workflow domain. URL must end with ${this.workflowDomain}`); + } + + url = parsedUrl.href; + } else { + // If the input is an ID, construct the full URL using the base domain + if (!/^e(n|o)[a-z0-9-]+$/i.test(sanitizedInput)) { + throw new Error(` + Invalid endpoint ID format. + Must contain only letters, numbers, and hyphens, and start with either "en" or "eo". + `); + } + + url = `https://${sanitizedInput}.${this.workflowDomain}`; + } + + return url; + } + /** * Invokes a workflow using the URL of its HTTP interface(s), by sending an - * HTTP POST request with the provided body. * - * @param url - The URL of the workflow's HTTP interface. + * @param urlOrEndpoint - The URL of the workflow's HTTP interface, or the ID of the endpoint * @param opts - The options for the request. * @param opts.body - The body of the request. It must be a JSON-serializable * value (e.g. an object, null, a string, etc.). * @param opts.headers - The headers to include in the request. Note that the * Authorization header will always be set with an OAuth access token * retrieved by the client. - * + * @param authType - The type of authorization to use for the request. * @returns A promise resolving to the response from the workflow. * * @example - * * ```typescript * const response = await client.invokeWorkflow( * "https://your-workflow-url.m.pipedream.net", @@ -756,25 +767,108 @@ export class ServerClient { * "Accept": "application/json", * }, * }, + * "oauth", * ); * console.log(response); * ``` */ - public async invokeWorkflow(url: string, opts: RequestOptions = {}): Promise { + public async invokeWorkflow( + urlOrEndpoint: string, + opts: RequestOptions = {}, + authType: HTTPAuthType = HTTPAuthType.None, + ): Promise { const { body, headers = {}, } = opts; + const url = this.buildWorkflowUrl(urlOrEndpoint); + + let authHeader: string | undefined; + switch (authType) { + case HTTPAuthType.StaticBearer: + // It's expected that users will pass their own Authorization header in + // the static bearer case + authHeader = headers["Authorization"]; + break; + case HTTPAuthType.OAuth: + authHeader = await this.oauthAuthorizationHeader(); + break; + default: + break; + } + return this.makeRequest("", { ...opts, baseURL: url, method: opts.method || "POST", // Default to POST if not specified + headers: authHeader + ? { + ...headers, + "Authorization": authHeader, + } + : headers, + body, + }); + } + + /** + * Invokes a workflow for a Pipedream Connect user in a project + * + * @param url - The URL of the workflow's HTTP interface. + * @param externalUserId — Your end user ID, for whom you're invoking the + * workflow. + * @param opts - The options for the request. + * @param opts.body - The body of the request. It must be a JSON-serializable + * value (e.g. an object, null, a string, etc.). + * @param opts.headers - The headers to include in the request. Note that the + * Authorization header will always be set with an OAuth access token + * retrieved by the client. + * @returns A promise resolving to the response from the workflow. + * + * @example + * ```typescript + * const response = await client.invokeWorkflowForExternalUser( + * "https://your-workflow-url.m.pipedream.net", + * "your-external-user-id", + * { + * body: { + * foo: 123, + * bar: "abc", + * baz: null, + * }, + * headers: { + * "Accept": "application/json", + * }, + * }, + * ); + * console.log(response); + * ``` + */ + public async invokeWorkflowForExternalUser( + url: string, + externalUserId: string, + opts: RequestOptions = {}, + ): Promise { + if (!externalUserId?.trim()) { + throw new Error("External user ID is required"); + } + + if (!url.trim()) { + throw new Error("Workflow URL is required"); + } + + if (!this.oauthClient) { + throw new Error("OAuth is required for invoking workflows for external users. Please pass credentials for a valid OAuth client"); + } + + const { headers = {} } = opts; + return this.invokeWorkflow(url, { + ...opts, headers: { ...headers, - "Authorization": await this.oauthAuthorizationHeader(), + "X-PD-External-User-ID": externalUserId, }, - body, - }); + }, HTTPAuthType.OAuth); // OAuth auth is required for invoking workflows for external users } } diff --git a/packages/sdk/tsconfig.node.json b/packages/sdk/tsconfig.node.json index 425a651b774bf..d53df43f701a9 100644 --- a/packages/sdk/tsconfig.node.json +++ b/packages/sdk/tsconfig.node.json @@ -1,21 +1,31 @@ { - "compilerOptions": { - "module": "NodeNext", - "target": "ES6", - "lib": ["ES6"], - "declaration": true, - "outDir": "dist/server", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "types": ["node", "jest", "jest-fetch-mock"] - }, - "include": [ - "src/server/**/*" + "compilerOptions": { + "module": "NodeNext", + "target": "ES6", + "lib": [ + "ES6" ], - "exclude": [ - "node_modules" + "declaration": true, + "outDir": "dist/server", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "sourceMap": true, + "moduleResolution": "NodeNext", + "noImplicitAny": true, + "types": [ + "node", + "jest", + "jest-fetch-mock" ] + }, + "include": [ + "src/server/**/*" + ], + "exclude": [ + "**/*.test.ts", + "node_modules" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b90f9b4f23d0c..900b4d3f8584e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12014,6 +12014,7 @@ importers: '@types/simple-oauth2': ^5.0.7 jest: ^29.7.0 jest-fetch-mock: ^3.0.3 + nodemon: ^3.1.7 simple-oauth2: ^5.1.0 ts-jest: ^29.2.5 typescript: ^5.5.2 @@ -12026,6 +12027,7 @@ importers: '@types/simple-oauth2': 5.0.7 jest: 29.7.0_@types+node@20.16.1 jest-fetch-mock: 3.0.3 + nodemon: 3.1.7 ts-jest: 29.2.5_q3xqhaztsvh2r5udjscjs67zn4 typescript: 5.5.4 @@ -22923,8 +22925,6 @@ packages: /binary-extensions/2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - dev: false - optional: true /binascii/0.0.2: resolution: {integrity: sha512-rA2CrUl1+6yKrn+XgLs8Hdy18OER1UW146nM+ixzhQXDY+Bd3ySkyIJGwF2a4I45JwbvF1mDL/nWkqBwpOcdBA==} @@ -23368,8 +23368,6 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 - dev: false - optional: true /chownr/1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -23797,7 +23795,7 @@ packages: dev: false /concat-map/0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} /concat-stream/2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} @@ -24318,6 +24316,19 @@ packages: dependencies: ms: 2.1.2 + /debug/4.3.6_supports-color@5.5.0: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 5.5.0 + dev: true + /decamelize-keys/1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -27448,6 +27459,10 @@ packages: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false + /ignore-by-default/1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + /ignore/5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -27653,8 +27668,6 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.3.0 - dev: false - optional: true /is-boolean-object/1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} @@ -31131,6 +31144,23 @@ packages: engines: {node: '>=6.0.0'} dev: false + /nodemon/3.1.7: + resolution: {integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chokidar: 3.6.0 + debug: 4.3.6_supports-color@5.5.0 + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.6.3 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + dev: true + /normalize-package-data/2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -32441,6 +32471,10 @@ packages: /psl/1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + /pstree.remy/1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + /pump/2.0.1: resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} dependencies: @@ -32905,8 +32939,6 @@ packages: engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 - dev: false - optional: true /readline-sync/1.4.10: resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} @@ -33858,6 +33890,13 @@ packages: is-arrayish: 0.3.2 dev: false + /simple-update-notifier/2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.3 + dev: true + /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -34962,6 +35001,11 @@ packages: ieee754: 1.2.1 dev: false + /touch/3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + dev: true + /tough-cookie/2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} @@ -35473,6 +35517,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /undefsafe/2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + /underscore/1.12.1: resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} dev: false @@ -35983,7 +36031,7 @@ packages: dev: false /verror/1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} engines: {'0': node >=0.6.0} dependencies: assert-plus: 1.0.0