@pipedream/qstash 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/create-endpoint/create-endpoint.mjs +45 -0
- package/actions/create-topic/create-topic.mjs +24 -0
- package/actions/list-endpoints/list-endpoints.mjs +17 -0
- package/actions/list-topics/list-topics.mjs +17 -0
- package/actions/publish-endpoint-message/publish-endpoint-message.mjs +70 -0
- package/actions/publish-topic-message/publish-topic-message.mjs +76 -0
- package/actions/verify-webhook/verify-webhook.mjs +38 -0
- package/common.mjs +61 -0
- package/package.json +2 -5
- package/qstash.app.mjs +174 -0
- package/sources/new-topic-webhook/new-topic-webhook.mjs +101 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "Create Endpoint",
|
|
5
|
+
version: "0.0.1",
|
|
6
|
+
key: "qstash-create-endpoint",
|
|
7
|
+
description: "Create a new HTTP endpoint on a QStash topic.",
|
|
8
|
+
props: {
|
|
9
|
+
qstash,
|
|
10
|
+
topicId: {
|
|
11
|
+
type: "string",
|
|
12
|
+
label: "QStash Topic",
|
|
13
|
+
description: "The QStash topic to subscribe to.",
|
|
14
|
+
async options() {
|
|
15
|
+
const topics = await this.qstash.listTopics({
|
|
16
|
+
$: this,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return topics.map((topic) => ({
|
|
20
|
+
label: topic.name,
|
|
21
|
+
value: topic.topicId,
|
|
22
|
+
}));
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
endpointUrl: {
|
|
26
|
+
propDefinition: [
|
|
27
|
+
qstash,
|
|
28
|
+
"endpointUrl",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
type: "action",
|
|
33
|
+
methods: {},
|
|
34
|
+
async run({ $ }) {
|
|
35
|
+
const {
|
|
36
|
+
topicId, topicName, endpointUrl,
|
|
37
|
+
} = this;
|
|
38
|
+
return this.qstash.createEndpoint({
|
|
39
|
+
$,
|
|
40
|
+
topicId,
|
|
41
|
+
topicName,
|
|
42
|
+
endpointUrl,
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "Create Topic",
|
|
5
|
+
version: "0.0.1",
|
|
6
|
+
key: "qstash-create-topic",
|
|
7
|
+
description: "Create a new QStash topic that emits to multiple endpoints.",
|
|
8
|
+
props: {
|
|
9
|
+
qstash,
|
|
10
|
+
topicName: {
|
|
11
|
+
propDefinition: [
|
|
12
|
+
qstash,
|
|
13
|
+
"topicName",
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
type: "action",
|
|
18
|
+
async run({ $ }) {
|
|
19
|
+
return this.qstash.createTopic({
|
|
20
|
+
$,
|
|
21
|
+
topicName: this.topicName,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "List Endpoints",
|
|
5
|
+
version: "0.0.1",
|
|
6
|
+
key: "qstash-list-endpoints",
|
|
7
|
+
description: "Lists all your existing QStash endpoints.",
|
|
8
|
+
props: {
|
|
9
|
+
qstash,
|
|
10
|
+
},
|
|
11
|
+
type: "action",
|
|
12
|
+
async run({ $ }) {
|
|
13
|
+
return this.qstash.listEndpoints({
|
|
14
|
+
$,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "List Topics",
|
|
5
|
+
version: "0.0.1",
|
|
6
|
+
key: "qstash-list-topics",
|
|
7
|
+
description: "List all your existing QStash topics.",
|
|
8
|
+
props: {
|
|
9
|
+
qstash,
|
|
10
|
+
},
|
|
11
|
+
type: "action",
|
|
12
|
+
async run({ $ }) {
|
|
13
|
+
return await this.qstash.listTopics({
|
|
14
|
+
$,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "Publish Endpoint Message",
|
|
5
|
+
version: "0.0.1",
|
|
6
|
+
key: "qstash-publish-endpoint-message",
|
|
7
|
+
description: "Publish a message a callback endpoint",
|
|
8
|
+
type: "action",
|
|
9
|
+
props: {
|
|
10
|
+
qstash,
|
|
11
|
+
endpoint: {
|
|
12
|
+
propDefinition: [
|
|
13
|
+
qstash,
|
|
14
|
+
"endpoint",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
body: {
|
|
18
|
+
propDefinition: [
|
|
19
|
+
qstash,
|
|
20
|
+
"body",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
retries: {
|
|
24
|
+
propDefinition: [
|
|
25
|
+
qstash,
|
|
26
|
+
"retries",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
deduplicationId: {
|
|
30
|
+
propDefinition: [
|
|
31
|
+
qstash,
|
|
32
|
+
"deduplicationId",
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
contentBasedDeduplicationEnabled: {
|
|
36
|
+
propDefinition: [
|
|
37
|
+
qstash,
|
|
38
|
+
"contentBasedDeduplicationEnabled",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
delay: {
|
|
42
|
+
propDefinition: [
|
|
43
|
+
qstash,
|
|
44
|
+
"delay",
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
cron: {
|
|
48
|
+
propDefinition: [
|
|
49
|
+
qstash,
|
|
50
|
+
"cron",
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
async run({ $ }) {
|
|
55
|
+
const {
|
|
56
|
+
body, endpoint, deduplicationId, retries, cron, delay, contentBasedDeduplicationEnabled,
|
|
57
|
+
} = this;
|
|
58
|
+
|
|
59
|
+
return this.qstash.publishEndpointMessage({
|
|
60
|
+
$,
|
|
61
|
+
body,
|
|
62
|
+
endpoint,
|
|
63
|
+
deduplicationId,
|
|
64
|
+
contentBasedDeduplicationEnabled,
|
|
65
|
+
retries,
|
|
66
|
+
cron,
|
|
67
|
+
delay,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "Publish Topic Message",
|
|
5
|
+
version: "0.0.1",
|
|
6
|
+
key: "qstash-publish-topic-message",
|
|
7
|
+
description: "Publish a message to a topic",
|
|
8
|
+
type: "action",
|
|
9
|
+
props: {
|
|
10
|
+
qstash,
|
|
11
|
+
topic: {
|
|
12
|
+
type: "string",
|
|
13
|
+
label: "QStash Topic",
|
|
14
|
+
description: "The QStash topic to publish a message to.",
|
|
15
|
+
async options() {
|
|
16
|
+
const topics = await this.qstash.listTopics({
|
|
17
|
+
$: this,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return topics.map((topic) => topic.name);
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
body: {
|
|
24
|
+
propDefinition: [
|
|
25
|
+
qstash,
|
|
26
|
+
"body",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
retries: {
|
|
30
|
+
propDefinition: [
|
|
31
|
+
qstash,
|
|
32
|
+
"retries",
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
deduplicationId: {
|
|
36
|
+
propDefinition: [
|
|
37
|
+
qstash,
|
|
38
|
+
"deduplicationId",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
contentBasedDeduplicationEnabled: {
|
|
42
|
+
propDefinition: [
|
|
43
|
+
qstash,
|
|
44
|
+
"contentBasedDeduplicationEnabled",
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
delay: {
|
|
48
|
+
propDefinition: [
|
|
49
|
+
qstash,
|
|
50
|
+
"delay",
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
cron: {
|
|
54
|
+
propDefinition: [
|
|
55
|
+
qstash,
|
|
56
|
+
"cron",
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
async run({ $ }) {
|
|
61
|
+
const {
|
|
62
|
+
body, topic, delay, retries, deduplicationId, cron, contentBasedDeduplicationEnabled,
|
|
63
|
+
} = this;
|
|
64
|
+
|
|
65
|
+
return this.qstash.publishTopicMessage({
|
|
66
|
+
$,
|
|
67
|
+
body,
|
|
68
|
+
topic,
|
|
69
|
+
delay,
|
|
70
|
+
retries,
|
|
71
|
+
contentBasedDeduplicationEnabled,
|
|
72
|
+
deduplicationId,
|
|
73
|
+
cron,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
import { methods } from "../../common.mjs";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
name: "Verify Webhook",
|
|
6
|
+
version: "0.0.1",
|
|
7
|
+
key: "qstash-verify-webhook",
|
|
8
|
+
description: "Verify an incoming QStash webhook to an endpoint. Only to be used with the **Publish Endpoint** action.",
|
|
9
|
+
props: {
|
|
10
|
+
qstash,
|
|
11
|
+
event: {
|
|
12
|
+
type: "object",
|
|
13
|
+
label: "HTTP Request",
|
|
14
|
+
description: "The incoming QStash HTTP request to be verified. Use `{{ steps.trigger.event }}` as a custom expression and make sure the HTTP triggers **Event Body** option is set to **Raw Body**.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
type: "action",
|
|
18
|
+
methods: {
|
|
19
|
+
...methods,
|
|
20
|
+
},
|
|
21
|
+
async run({ $ }) {
|
|
22
|
+
const signature = this.event.headers[this.event.headers.indexOf("upstash-signature") + 1];
|
|
23
|
+
const keys = await this.qstash.getKeys({
|
|
24
|
+
$,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const currentSigningKey = keys.current;
|
|
28
|
+
const nextSigningKey = keys.next;
|
|
29
|
+
const url = this.event.url.slice(0, -1);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return await this.verify(signature, currentSigningKey, this.event.body, url);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.log(e);
|
|
35
|
+
return await this.verify(signature, nextSigningKey, this.event.body, url);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
package/common.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash, createHmac,
|
|
3
|
+
} from "crypto";
|
|
4
|
+
|
|
5
|
+
export const methods = {
|
|
6
|
+
async verify(
|
|
7
|
+
jwt,
|
|
8
|
+
signingKey,
|
|
9
|
+
body,
|
|
10
|
+
url,
|
|
11
|
+
) {
|
|
12
|
+
const split = jwt.split(".");
|
|
13
|
+
if (split.length != 3) {
|
|
14
|
+
throw new Error("Invalid JWT");
|
|
15
|
+
}
|
|
16
|
+
const [
|
|
17
|
+
header,
|
|
18
|
+
payload,
|
|
19
|
+
signature,
|
|
20
|
+
] = split;
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
signature !=
|
|
24
|
+
createHmac("sha256", signingKey)
|
|
25
|
+
.update(`${header}.${payload}`)
|
|
26
|
+
.digest("base64url")
|
|
27
|
+
) {
|
|
28
|
+
throw new Error("Invalid JWT signature");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Now the jwt is verified and we can start looking at the claims in the payload
|
|
32
|
+
const p = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
33
|
+
|
|
34
|
+
if (p.iss !== "Upstash") {
|
|
35
|
+
throw new Error(`invalid issuer: ${p.iss}, expected "Upstash"`);
|
|
36
|
+
}
|
|
37
|
+
if (p.sub !== url) {
|
|
38
|
+
throw new Error(`invalid subject: ${p.sub}, expected "${url}"`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const now = Math.floor(Date.now() / 1000);
|
|
42
|
+
if (now > p.exp) {
|
|
43
|
+
throw new Error("token has expired");
|
|
44
|
+
}
|
|
45
|
+
if (now < p.nbf) {
|
|
46
|
+
throw new Error("token is not yet valid");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (body != null) {
|
|
50
|
+
if (
|
|
51
|
+
p.body.replace(/=+$/, "") !=
|
|
52
|
+
createHash("sha256").update(body)
|
|
53
|
+
.digest("base64url")
|
|
54
|
+
) {
|
|
55
|
+
throw new Error("body hash does not match");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipedream/qstash",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Pipedream qstash Components",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "qstash.app.mjs",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pipedream",
|
|
8
8
|
"qstash"
|
|
9
9
|
],
|
|
10
|
-
"files": [
|
|
11
|
-
"dist"
|
|
12
|
-
],
|
|
13
10
|
"homepage": "https://pipedream.com/apps/qstash",
|
|
14
11
|
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
|
|
15
12
|
"license": "MIT",
|
package/qstash.app.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { axios } from "@pipedream/platform";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
type: "app",
|
|
5
|
+
app: "qstash",
|
|
6
|
+
propDefinitions: {
|
|
7
|
+
topic: {
|
|
8
|
+
type: "string",
|
|
9
|
+
label: "Topic",
|
|
10
|
+
description: "Topics allow you to publish a single message to more than one API endpoints.",
|
|
11
|
+
},
|
|
12
|
+
topicId: {
|
|
13
|
+
type: "string",
|
|
14
|
+
label: "Topic",
|
|
15
|
+
description: "The ID of th QStash topic.",
|
|
16
|
+
},
|
|
17
|
+
endpoint: {
|
|
18
|
+
type: "string",
|
|
19
|
+
label: "Callback Endpoint",
|
|
20
|
+
description: "The endpoint that will be receiving these events",
|
|
21
|
+
},
|
|
22
|
+
body: {
|
|
23
|
+
type: "object",
|
|
24
|
+
label: "Body",
|
|
25
|
+
description: "The body is forwarded to your endpoints in the HTTP request",
|
|
26
|
+
},
|
|
27
|
+
retries: {
|
|
28
|
+
type: "integer",
|
|
29
|
+
label: "Retries",
|
|
30
|
+
description: "How often should this messasge be retried in case the destination API is not available.",
|
|
31
|
+
default: 3,
|
|
32
|
+
optional: true,
|
|
33
|
+
},
|
|
34
|
+
deduplicationId: {
|
|
35
|
+
type: "string",
|
|
36
|
+
label: "Deduplication ID",
|
|
37
|
+
description: "Provide an ID that will be used to detect duplicate messages. If a duplicate message is detected, the request will be accepted but not enqueued. [Learn more.](https://docs.upstash.com/qstash/howto/publishing#optional-parameters-and-configuration)",
|
|
38
|
+
optional: true,
|
|
39
|
+
},
|
|
40
|
+
contentBasedDeduplicationEnabled: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
label: "Content Based Deduplication Enabled",
|
|
43
|
+
description: "If true, the message content will get hashed and used as deduplication ID. If a duplicate message is detected, the request will be accepted but not enqueued.",
|
|
44
|
+
optional: true,
|
|
45
|
+
},
|
|
46
|
+
delay: {
|
|
47
|
+
type: "string",
|
|
48
|
+
label: "Delay",
|
|
49
|
+
description: "Format for this header is a number followed by duration abbreviation, like `10s`. Available durations are `s` (seconds), `m` (minutes), `h` (hours), `d` (days).",
|
|
50
|
+
optional: true,
|
|
51
|
+
},
|
|
52
|
+
cron: {
|
|
53
|
+
type: "string",
|
|
54
|
+
label: "Cron",
|
|
55
|
+
description: "You can use a tool like [Crontab.guru](https://crontab.guru) to generate cron expressions.",
|
|
56
|
+
optional: true,
|
|
57
|
+
},
|
|
58
|
+
topicName: {
|
|
59
|
+
type: "string",
|
|
60
|
+
label: "Topic Name",
|
|
61
|
+
description: "The name of the topic.",
|
|
62
|
+
},
|
|
63
|
+
endpointUrl: {
|
|
64
|
+
type: "string",
|
|
65
|
+
label: "Endpoint URL",
|
|
66
|
+
description: "User controlled service url where we will send webhooks to.",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
methods: {
|
|
70
|
+
baseUrl() {
|
|
71
|
+
return "https://qstash.upstash.io/v1";
|
|
72
|
+
},
|
|
73
|
+
getHeaders({
|
|
74
|
+
deduplicationId, delay, cron, retries, contentBasedDeduplicationEnabled,
|
|
75
|
+
}) {
|
|
76
|
+
let headers = {
|
|
77
|
+
"Authorization": `Bearer ${this.$auth.qstash_token}`,
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
};
|
|
80
|
+
if (contentBasedDeduplicationEnabled) {
|
|
81
|
+
headers["Upstash-Content-Based-Deduplication"] = contentBasedDeduplicationEnabled;
|
|
82
|
+
}
|
|
83
|
+
if (deduplicationId) {
|
|
84
|
+
headers["Upstash-Deduplication-Id"] = deduplicationId;
|
|
85
|
+
}
|
|
86
|
+
if (delay) {
|
|
87
|
+
headers["Upstash-Delay"] = delay;
|
|
88
|
+
}
|
|
89
|
+
if (cron) {
|
|
90
|
+
headers["Upstash-Cron"] = cron;
|
|
91
|
+
}
|
|
92
|
+
if (retries) {
|
|
93
|
+
headers["Upstash-Retries"] = retries;
|
|
94
|
+
}
|
|
95
|
+
return headers;
|
|
96
|
+
},
|
|
97
|
+
async publishEndpointMessage({
|
|
98
|
+
$, body, endpoint, ...headers
|
|
99
|
+
}) {
|
|
100
|
+
return axios($, {
|
|
101
|
+
url: `${this.baseUrl()}/publish/${endpoint}`,
|
|
102
|
+
method: "POST",
|
|
103
|
+
data: body,
|
|
104
|
+
headers: this.getHeaders(headers),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
async publishTopicMessage({
|
|
108
|
+
$, body, topic, ...headers
|
|
109
|
+
}) {
|
|
110
|
+
return axios($, {
|
|
111
|
+
url: `${this.baseUrl()}/publish/${topic}`,
|
|
112
|
+
method: "POST",
|
|
113
|
+
data: body,
|
|
114
|
+
headers: this.getHeaders(headers),
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
async listEndpoints({ $ }) {
|
|
118
|
+
return axios($ || this, {
|
|
119
|
+
url: `${this.baseUrl()}/endpoints`,
|
|
120
|
+
method: "GET",
|
|
121
|
+
headers: this.getHeaders({}),
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
async listTopics({ $ = this }) {
|
|
125
|
+
return axios($, {
|
|
126
|
+
url: `${this.baseUrl()}/topics`,
|
|
127
|
+
method: "GET",
|
|
128
|
+
headers: this.getHeaders({}),
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
async createTopic({
|
|
132
|
+
$, topicName,
|
|
133
|
+
}) {
|
|
134
|
+
return axios($, {
|
|
135
|
+
url: `${this.baseUrl()}/topics`,
|
|
136
|
+
method: "POST",
|
|
137
|
+
data: {
|
|
138
|
+
name: topicName,
|
|
139
|
+
},
|
|
140
|
+
headers: this.getHeaders({}),
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
async createEndpoint({
|
|
144
|
+
$, topicName, topicId, endpointUrl,
|
|
145
|
+
}) {
|
|
146
|
+
return axios($, {
|
|
147
|
+
url: `${this.baseUrl()}/endpoints`,
|
|
148
|
+
method: "POST",
|
|
149
|
+
data: {
|
|
150
|
+
topicName,
|
|
151
|
+
topicId,
|
|
152
|
+
url: endpointUrl,
|
|
153
|
+
},
|
|
154
|
+
headers: this.getHeaders({}),
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
async deleteEndpoint({
|
|
158
|
+
$ = this, endpointId,
|
|
159
|
+
}) {
|
|
160
|
+
return axios($, {
|
|
161
|
+
url: `${this.baseUrl()}/endpoints/${endpointId}`,
|
|
162
|
+
method: "DELETE",
|
|
163
|
+
headers: this.getHeaders({}),
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
async getKeys({ $ = this }) {
|
|
167
|
+
return axios($, {
|
|
168
|
+
url: `${this.baseUrl()}/keys`,
|
|
169
|
+
method: "GET",
|
|
170
|
+
headers: this.getHeaders({}),
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import qstash from "../../qstash.app.mjs";
|
|
2
|
+
import { methods } from "../../common.mjs";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
name: "New Topic Webhook",
|
|
6
|
+
version: "0.0.1",
|
|
7
|
+
key: "qstash-new-topic-webhook",
|
|
8
|
+
description: "Emit new events on each new QStash topic message",
|
|
9
|
+
props: {
|
|
10
|
+
qstash,
|
|
11
|
+
topicId: {
|
|
12
|
+
type: "string",
|
|
13
|
+
label: "QStash Topic",
|
|
14
|
+
description: "The QStash topic to subscribe to.",
|
|
15
|
+
async options() {
|
|
16
|
+
const topics = await this.qstash.listTopics({
|
|
17
|
+
$: this,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return topics.map((topic) => ({
|
|
21
|
+
label: topic.name,
|
|
22
|
+
value: topic.topicId,
|
|
23
|
+
}));
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
http: {
|
|
27
|
+
type: "$.interface.http",
|
|
28
|
+
customResponse: true, // optional: defaults to false
|
|
29
|
+
},
|
|
30
|
+
db: "$.service.db",
|
|
31
|
+
},
|
|
32
|
+
type: "source",
|
|
33
|
+
methods: {
|
|
34
|
+
...methods,
|
|
35
|
+
},
|
|
36
|
+
hooks: {
|
|
37
|
+
async deploy() {
|
|
38
|
+
console.log("deployment hook started");
|
|
39
|
+
console.log("selected topic ID", this.topicId);
|
|
40
|
+
console.log(`PD generated endpoint URL ${this.http.endpoint}`);
|
|
41
|
+
const endpoint = await this.qstash.createEndpoint({
|
|
42
|
+
$: this,
|
|
43
|
+
topicId: this.topicId,
|
|
44
|
+
endpointUrl: this.http.endpoint,
|
|
45
|
+
});
|
|
46
|
+
console.log("Created endpoint", endpoint);
|
|
47
|
+
this.db.set("endpointId", endpoint.endpointId);
|
|
48
|
+
},
|
|
49
|
+
async deactivate() {
|
|
50
|
+
console.log("deactivate hook started");
|
|
51
|
+
const endpointId = this.db.get("endpointId");
|
|
52
|
+
await this.qstash.deleteEndpoint({
|
|
53
|
+
$: this,
|
|
54
|
+
endpointId,
|
|
55
|
+
});
|
|
56
|
+
console.log(`Deleted QStash endpoint ${endpointId}`);
|
|
57
|
+
},
|
|
58
|
+
async activate() {
|
|
59
|
+
console.log("activate hook started");
|
|
60
|
+
|
|
61
|
+
const keys = await this.qstash.getKeys({
|
|
62
|
+
$: this,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const currentSigningKey = keys.current;
|
|
66
|
+
const nextSigningKey = keys.next;
|
|
67
|
+
|
|
68
|
+
this.db.set("currentSigningKey", currentSigningKey);
|
|
69
|
+
this.db.set("nextSigningKey", nextSigningKey);
|
|
70
|
+
console.log("Set QStash signing keys");
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
async run(event) {
|
|
74
|
+
const signature = event.headers["upstash-signature"];
|
|
75
|
+
const currentSigningKey = this.db.get("currentSigningKey");
|
|
76
|
+
const nextSigningKey = this.db.get("nextSigningKey");
|
|
77
|
+
const url = event.url.slice(0, -1);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await this.verify(signature, currentSigningKey, event.rawBody, url);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.log(e);
|
|
83
|
+
await this.verify(signature, nextSigningKey, event.rawBody, url);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await this.$emit(
|
|
87
|
+
{
|
|
88
|
+
event,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: event.headers["upstash-signature"],
|
|
92
|
+
summary: "New webhook received from QStash",
|
|
93
|
+
ts: Date.now(),
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await this.http.respond({
|
|
98
|
+
status: 200,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|