From dbf1dd4d24620cfd14ce5769196a394605b729e5 Mon Sep 17 00:00:00 2001 From: Jorge Cortes Date: Tue, 6 May 2025 15:56:24 -0500 Subject: [PATCH] [TRIGGER] Use Webhooks for Vercel Sources --- .../cancel-deployment/cancel-deployment.mjs | 2 +- .../create-deployment/create-deployment.mjs | 2 +- .../list-deployments/list-deployments.mjs | 2 +- .../vercel_token_auth/common/constants.mjs | 2 + components/vercel_token_auth/package.json | 5 +- .../sources/common/events.mjs | 45 ++++++ .../sources/common/webhook.mjs | 151 ++++++++++++++++++ .../deployment-canceled-instant.mjs | 27 ++++ .../deployment-error-instant.mjs | 27 ++++ .../deployment-succeeded-instant.mjs | 27 ++++ .../new-deployment-instant.mjs | 27 ++++ .../sources/new-deployment/new-deployment.mjs | 2 +- .../vercel_token_auth.app.mjs | 14 +- pnpm-lock.yaml | 5 +- 14 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 components/vercel_token_auth/sources/common/events.mjs create mode 100644 components/vercel_token_auth/sources/common/webhook.mjs create mode 100644 components/vercel_token_auth/sources/deployment-canceled-instant/deployment-canceled-instant.mjs create mode 100644 components/vercel_token_auth/sources/deployment-error-instant/deployment-error-instant.mjs create mode 100644 components/vercel_token_auth/sources/deployment-succeeded-instant/deployment-succeeded-instant.mjs create mode 100644 components/vercel_token_auth/sources/new-deployment-instant/new-deployment-instant.mjs diff --git a/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs b/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs index b93afcab43215..8cde0395e4bb6 100644 --- a/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs +++ b/components/vercel_token_auth/actions/cancel-deployment/cancel-deployment.mjs @@ -4,7 +4,7 @@ export default { key: "vercel_token_auth-cancel-deployment", name: "Cancel Deployment", description: "Cancel a deployment which is currently building. [See the documentation](https://vercel.com/docs/rest-api/endpoints/deployments#cancel-a-deployment)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { vercelTokenAuth, diff --git a/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs b/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs index 3b8ca4131e72c..80509ea1b9a0f 100644 --- a/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs +++ b/components/vercel_token_auth/actions/create-deployment/create-deployment.mjs @@ -5,7 +5,7 @@ export default { key: "vercel_token_auth-create-deployment", name: "Create Deployment", description: "Create a new deployment from a GitHub repository. [See the documentation](https://vercel.com/docs/rest-api/endpoints/deployments#create-a-new-deployment)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { vercelTokenAuth, diff --git a/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs b/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs index d3a456581a761..3e09b50237204 100644 --- a/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs +++ b/components/vercel_token_auth/actions/list-deployments/list-deployments.mjs @@ -4,7 +4,7 @@ export default { key: "vercel_token_auth-list-deployments", name: "List Deployments", description: "List deployments under the account corresponding to the API token. [See the documentation](https://vercel.com/docs/rest-api/endpoints/deployments#list-deployments)", - version: "0.0.4", + version: "0.0.5", type: "action", props: { vercelTokenAuth, diff --git a/components/vercel_token_auth/common/constants.mjs b/components/vercel_token_auth/common/constants.mjs index 4cff1c579dbd9..c3badcacc702a 100644 --- a/components/vercel_token_auth/common/constants.mjs +++ b/components/vercel_token_auth/common/constants.mjs @@ -9,4 +9,6 @@ export default { "READY", "CANCELED", ], + WEBHOOK_ID: "webhookId", + WEBHOOK_SECRET: "webhookSecret", }; diff --git a/components/vercel_token_auth/package.json b/components/vercel_token_auth/package.json index 4902eddc8b216..61da02c0b539f 100644 --- a/components/vercel_token_auth/package.json +++ b/components/vercel_token_auth/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/vercel_token_auth", - "version": "0.0.5", + "version": "0.1.0", "description": "Pipedream Vercel (token-based auth) Components", "main": "vercel_token_auth.app.mjs", "keywords": [ @@ -13,6 +13,7 @@ "access": "public" }, "dependencies": { - "@pipedream/platform": "^3.0.3" + "@pipedream/platform": "^3.0.3", + "crypto": "^1.0.1" } } diff --git a/components/vercel_token_auth/sources/common/events.mjs b/components/vercel_token_auth/sources/common/events.mjs new file mode 100644 index 0000000000000..e502280c1c983 --- /dev/null +++ b/components/vercel_token_auth/sources/common/events.mjs @@ -0,0 +1,45 @@ +export default { + BUDGET_REACHED: "budget.reached", + BUDGET_RESET: "budget.reset", + DOMAIN_CREATED: "domain.created", + DEPLOYMENT_CREATED: "deployment.created", + DEPLOYMENT_ERROR: "deployment.error", + DEPLOYMENT_CANCELED: "deployment.canceled", + DEPLOYMENT_SUCCEEDED: "deployment.succeeded", + DEPLOYMENT_READY: "deployment.ready", + DEPLOYMENT_CHECK_REREQUESTED: "deployment.check-rerequested", + DEPLOYMENT_PROMOTED: "deployment.promoted", + DEPLOYMENT_INTEGRATION_ACTION_START: "deployment.integration.action.start", + DEPLOYMENT_INTEGRATION_ACTION_CANCEL: "deployment.integration.action.cancel", + DEPLOYMENT_INTEGRATION_ACTION_CLEANUP: "deployment.integration.action.cleanup", + EDGE_CONFIG_CREATED: "edge-config.created", + EDGE_CONFIG_DELETED: "edge-config.deleted", + EDGE_CONFIG_ITEMS_UPDATED: "edge-config.items.updated", + FIREWALL_ATTACK: "firewall.attack", + INTEGRATION_CONFIGURATION_PERMISSION_UPGRADED: "integration-configuration.permission-upgraded", + INTEGRATION_CONFIGURATION_REMOVED: "integration-configuration.removed", + INTEGRATION_CONFIGURATION_SCOPE_CHANGE_CONFIRMED: "integration-configuration.scope-change-confirmed", + INTEGRATION_RESOURCE_PROJECT_CONNECTED: "integration-resource.project-connected", + INTEGRATION_RESOURCE_PROJECT_DISCONNECTED: "integration-resource.project-disconnected", + PROJECT_CREATED: "project.created", + PROJECT_REMOVED: "project.removed", + DEPLOYMENT_CHECKS_COMPLETED: "deployment-checks-completed", + DEPLOYMENT_READY_: "deployment-ready", + DEPLOYMENT_PREPARED: "deployment-prepared", + DEPLOYMENT_ERROR_: "deployment-error", + DEPLOYMENT_CHECK_REREQUESTED_: "deployment-check-rerequested", + DEPLOYMENT_CANCELED_: "deployment-canceled", + PROJECT_CREATED_: "project-created", + PROJECT_REMOVED_: "project-removed", + DOMAIN_CREATED_: "domain-created", + DEPLOYMENT_: "deployment", + INTEGRATION_CONFIGURATION_PERMISSION_UPDATED: "integration-configuration-permission-updated", + INTEGRATION_CONFIGURATION_REMOVED_: "integration-configuration-removed", + INTEGRATION_CONFIGURATION_SCOPE_CHANGE_CONFIRMED_: "integration-configuration-scope-change-confirmed", + MARKETPLACE_INVOICE_CREATED: "marketplace.invoice.created", + MARKETPLACE_INVOICE_PAID: "marketplace.invoice.paid", + MARKETPLACE_INVOICE_NOTPAID: "marketplace.invoice.notpaid", + MARKETPLACE_INVOICE_REFUNDED: "marketplace.invoice.refunded", + OBSERVABILITY_ANOMALY: "observability.anomaly", + TEST_WEBHOOK: "test-webhook", +}; diff --git a/components/vercel_token_auth/sources/common/webhook.mjs b/components/vercel_token_auth/sources/common/webhook.mjs new file mode 100644 index 0000000000000..307faecbf8abe --- /dev/null +++ b/components/vercel_token_auth/sources/common/webhook.mjs @@ -0,0 +1,151 @@ +import { createHmac } from "crypto"; +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../vercel_token_auth.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + props: { + app, + db: "$.service.db", + http: "$.interface.http", + teamId: { + label: "Team ID", + description: "The Team identifier to perform the request on behalf of. Eg. `team_1a2b3c4d5e6f7g8h9i0j1k2l`", + optional: true, + propDefinition: [ + app, + "team", + ], + }, + slug: { + label: "Slug", + description: "The Team slug to perform the request on behalf of. Eg. `my-team-url-slug`", + optional: true, + propDefinition: [ + app, + "team", + () => ({ + mapper: ({ slug }) => slug, + }), + ], + }, + projectIds: { + type: "string[]", + label: "Project IDs", + description: "The Project identifiers to perform the request on behalf of", + optional: true, + propDefinition: [ + app, + "project", + ], + }, + }, + hooks: { + async activate() { + const { + createWebhook, + getEvents, + http: { endpoint: url }, + setWebhookId, + setWebhookSecret, + teamId, + slug, + projectIds, + } = this; + + const response = + await createWebhook({ + params: { + teamId, + slug, + }, + data: { + url, + events: getEvents(), + projectIds, + }, + }); + + setWebhookId(response.id); + setWebhookSecret(response.secret); + }, + async deactivate() { + const { + deleteWebhook, + getWebhookId, + } = this; + + const webhookId = getWebhookId(); + if (webhookId) { + await deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + setWebhookSecret(value) { + this.db.set(constants.WEBHOOK_SECRET, value); + }, + getWebhookSecret() { + return this.db.get(constants.WEBHOOK_SECRET); + }, + getEvents() { + throw new ConfigurationError("getEvents is not implemented"); + }, + isSignatureValid(incommingSignature, bodyRaw) { + const secret = this.getWebhookSecret(); + const rawBodyBuffer = Buffer.from(bodyRaw, "utf-8"); + const expectedSignature = + createHmac("sha1", secret) + .update(rawBodyBuffer) + .digest("hex"); + return expectedSignature === incommingSignature; + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.makeRequest({ + method: "POST", + endpoint: "v1/webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.makeRequest({ + method: "DELETE", + endpoint: `v1/webhooks/${webhookId}`, + ...args, + }); + }, + }, + async run({ + body, bodyRaw, headers, + }) { + const incommingSignature = headers["x-vercel-signature"]; + + if (!incommingSignature) { + throw new ConfigurationError("Missing x-vercel-signature header"); + } + + const isValid = this.isSignatureValid(incommingSignature, bodyRaw); + + if (!isValid) { + throw new ConfigurationError("Invalid x-vercel-signature header"); + } + + this.processResource(body); + }, +}; diff --git a/components/vercel_token_auth/sources/deployment-canceled-instant/deployment-canceled-instant.mjs b/components/vercel_token_auth/sources/deployment-canceled-instant/deployment-canceled-instant.mjs new file mode 100644 index 0000000000000..c4e1432f3cf48 --- /dev/null +++ b/components/vercel_token_auth/sources/deployment-canceled-instant/deployment-canceled-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "vercel_token_auth-deployment-canceled-instant", + name: "Deployment Canceled (Instant)", + description: "Emit new event when a deployment is canceled [See the documentation](https://vercel.com/docs/rest-api/reference/endpoints/webhooks/creates-a-webhook).", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + events.DEPLOYMENT_CANCELED, + ]; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `Deployment Canceled: ${resource.id}`, + ts: resource.createdAt, + }; + }, + }, +}; diff --git a/components/vercel_token_auth/sources/deployment-error-instant/deployment-error-instant.mjs b/components/vercel_token_auth/sources/deployment-error-instant/deployment-error-instant.mjs new file mode 100644 index 0000000000000..fdadc7ec46a25 --- /dev/null +++ b/components/vercel_token_auth/sources/deployment-error-instant/deployment-error-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "vercel_token_auth-deployment-error-instant", + name: "Deployment Error (Instant)", + description: "Emit new event when a deployment encounters an error [See the documentation](https://vercel.com/docs/rest-api/reference/endpoints/webhooks/creates-a-webhook).", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + events.DEPLOYMENT_ERROR, + ]; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `Deployment Error: ${resource.id}`, + ts: resource.createdAt, + }; + }, + }, +}; diff --git a/components/vercel_token_auth/sources/deployment-succeeded-instant/deployment-succeeded-instant.mjs b/components/vercel_token_auth/sources/deployment-succeeded-instant/deployment-succeeded-instant.mjs new file mode 100644 index 0000000000000..4de687ab8da54 --- /dev/null +++ b/components/vercel_token_auth/sources/deployment-succeeded-instant/deployment-succeeded-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "vercel_token_auth-deployment-succeeded-instant", + name: "Deployment Succeeded (Instant)", + description: "Emit new event when a deployment successfully completes [See the documentation](https://vercel.com/docs/rest-api/reference/endpoints/webhooks/creates-a-webhook).", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + events.DEPLOYMENT_SUCCEEDED, + ]; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `Deployment Succeeded: ${resource.id}`, + ts: resource.createdAt, + }; + }, + }, +}; diff --git a/components/vercel_token_auth/sources/new-deployment-instant/new-deployment-instant.mjs b/components/vercel_token_auth/sources/new-deployment-instant/new-deployment-instant.mjs new file mode 100644 index 0000000000000..79a98bd0cc910 --- /dev/null +++ b/components/vercel_token_auth/sources/new-deployment-instant/new-deployment-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import events from "../common/events.mjs"; + +export default { + ...common, + key: "vercel_token_auth-new-deployment-instant", + name: "New Deployment (Instant)", + description: "Emit new event when a deployment is created [See the documentation](https://vercel.com/docs/rest-api/reference/endpoints/webhooks/creates-a-webhook).", + type: "source", + version: "0.0.1", + dedupe: "unique", + methods: { + ...common.methods, + getEvents() { + return [ + events.DEPLOYMENT_CREATED, + ]; + }, + generateMeta(resource) { + return { + id: resource.id, + summary: `New Deployment: ${resource.id}`, + ts: resource.createdAt, + }; + }, + }, +}; diff --git a/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs b/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs index 9474844f7d640..c7cac90c09338 100644 --- a/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs +++ b/components/vercel_token_auth/sources/new-deployment/new-deployment.mjs @@ -5,7 +5,7 @@ export default { key: "vercel_token_auth-new-deployment", name: "New Deployment", description: "Emit new event when a deployment is created", - version: "0.0.4", + version: "0.0.5", type: "source", dedupe: "unique", props: { diff --git a/components/vercel_token_auth/vercel_token_auth.app.mjs b/components/vercel_token_auth/vercel_token_auth.app.mjs index 6da875a429ffb..7f95500b4b3fe 100644 --- a/components/vercel_token_auth/vercel_token_auth.app.mjs +++ b/components/vercel_token_auth/vercel_token_auth.app.mjs @@ -44,13 +44,17 @@ export default { type: "string", label: "Team", description: "The Team identifier or slug to perform the request on behalf of", - async options() { + async options({ + mapper = ({ + slug: label, id: value, + }) => ({ + label, + value, + }), + }) { try { const teams = await this.listTeams(); - return teams?.map((team) => ({ - label: team.slug, - value: team.id, - })) ?? []; + return teams?.map(mapper) ?? []; } catch (e) { throw new Error(e.message); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3b8832f2cc43..785e0779dd27d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13973,6 +13973,9 @@ importers: '@pipedream/platform': specifier: ^3.0.3 version: 3.0.3 + crypto: + specifier: ^1.0.1 + version: 1.0.1 components/verdict_as_a_service: dependencies: @@ -35577,8 +35580,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: