@stepflowjs/trigger-webhook 0.0.1

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.
@@ -0,0 +1,146 @@
1
+ import { Trigger, TriggerHandler } from '@stepflowjs/core';
2
+
3
+ type SignatureAlgorithm = "sha256" | "sha1" | "sha512";
4
+ interface WebhookTriggerConfig {
5
+ /** The webhook endpoint path */
6
+ path: string;
7
+ /** Secret for signature verification */
8
+ secret: string;
9
+ /** Header name containing the signature (default: 'x-webhook-signature') */
10
+ signatureHeader?: string;
11
+ /** Signing algorithm (default: 'sha256') */
12
+ algorithm?: SignatureAlgorithm;
13
+ /** Signature prefix like 'sha256=' (optional) */
14
+ signaturePrefix?: string;
15
+ /** Header name for timestamp (for replay attack prevention) */
16
+ timestampHeader?: string;
17
+ /** Maximum age in seconds for timestamp validation (default: 300) */
18
+ timestampTolerance?: number;
19
+ }
20
+ interface WebhookTriggerResponse {
21
+ success: boolean;
22
+ eventId: string;
23
+ }
24
+ /**
25
+ * Webhook trigger for Stepflow workflows with signature verification
26
+ *
27
+ * Supports multiple webhook provider patterns:
28
+ * - GitHub style: `sha256=<hex>`
29
+ * - Stripe style: `t=timestamp,v1=signature`
30
+ * - Generic HMAC
31
+ *
32
+ * Includes replay attack prevention via timestamp validation.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // GitHub webhook
37
+ * const githubTrigger = new WebhookTrigger({
38
+ * path: '/webhooks/github',
39
+ * secret: process.env.GITHUB_WEBHOOK_SECRET,
40
+ * signatureHeader: 'x-hub-signature-256',
41
+ * algorithm: 'sha256',
42
+ * signaturePrefix: 'sha256=',
43
+ * });
44
+ *
45
+ * // Stripe webhook
46
+ * const stripeTrigger = new WebhookTrigger({
47
+ * path: '/webhooks/stripe',
48
+ * secret: process.env.STRIPE_WEBHOOK_SECRET,
49
+ * signatureHeader: 'stripe-signature',
50
+ * algorithm: 'sha256',
51
+ * timestampHeader: 'stripe-signature',
52
+ * timestampTolerance: 300,
53
+ * });
54
+ *
55
+ * await githubTrigger.start(async (event) => {
56
+ * await stepflow.trigger('github-webhook', event.data);
57
+ * });
58
+ *
59
+ * // In your framework adapter:
60
+ * app.post('/webhooks/github', async (req) => {
61
+ * const response = await githubTrigger.handleRequest(req);
62
+ * return response;
63
+ * });
64
+ * ```
65
+ */
66
+ declare class WebhookTrigger implements Trigger<WebhookTriggerConfig> {
67
+ readonly config: WebhookTriggerConfig;
68
+ readonly type = "webhook";
69
+ private handler?;
70
+ constructor(config: WebhookTriggerConfig);
71
+ /**
72
+ * Start the trigger with a handler function
73
+ * @param handler Function to call when webhook requests are received
74
+ */
75
+ start(handler: TriggerHandler): Promise<void>;
76
+ /**
77
+ * Stop the trigger
78
+ */
79
+ stop(): Promise<void>;
80
+ /**
81
+ * Health check - returns true if handler is registered
82
+ */
83
+ healthCheck(): Promise<boolean>;
84
+ /**
85
+ * Handle an incoming webhook request
86
+ *
87
+ * This method should be called by framework adapters to process webhook requests.
88
+ * It validates the signature, checks for replay attacks, creates a trigger event,
89
+ * and invokes the registered handler.
90
+ *
91
+ * @param request Web standard Request object
92
+ * @returns Web standard Response object
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // Hono adapter
97
+ * app.post('/webhook', async (c) => {
98
+ * const response = await trigger.handleRequest(c.req.raw);
99
+ * return response;
100
+ * });
101
+ *
102
+ * // Express adapter
103
+ * app.post('/webhook', async (req, res) => {
104
+ * const request = new Request(`http://localhost${req.url}`, {
105
+ * method: req.method,
106
+ * headers: req.headers,
107
+ * body: JSON.stringify(req.body),
108
+ * });
109
+ * const response = await trigger.handleRequest(request);
110
+ * res.status(response.status).json(await response.json());
111
+ * });
112
+ * ```
113
+ */
114
+ handleRequest(request: Request): Promise<Response>;
115
+ /**
116
+ * Validate webhook signature using Web Crypto API
117
+ * Supports multiple signature formats:
118
+ * - GitHub: sha256=<hex>
119
+ * - Stripe: t=timestamp,v1=signature
120
+ * - Generic: <hex>
121
+ * @private
122
+ */
123
+ private validateSignature;
124
+ /**
125
+ * Verify HMAC signature using Web Crypto API
126
+ * @private
127
+ */
128
+ private verifyHmac;
129
+ /**
130
+ * Map algorithm name to Web Crypto API hash algorithm
131
+ * @private
132
+ */
133
+ private getHashAlgorithm;
134
+ /**
135
+ * Constant-time string comparison to prevent timing attacks
136
+ * @private
137
+ */
138
+ private constantTimeCompare;
139
+ /**
140
+ * Validate timestamp to prevent replay attacks
141
+ * @private
142
+ */
143
+ private validateTimestamp;
144
+ }
145
+
146
+ export { type SignatureAlgorithm, WebhookTrigger, type WebhookTriggerConfig, type WebhookTriggerResponse };
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ // src/index.ts
2
+ var WebhookTrigger = class {
3
+ constructor(config) {
4
+ this.config = config;
5
+ this.config.signatureHeader = config.signatureHeader || "x-webhook-signature";
6
+ this.config.algorithm = config.algorithm || "sha256";
7
+ this.config.timestampTolerance = config.timestampTolerance ?? 300;
8
+ }
9
+ type = "webhook";
10
+ handler;
11
+ /**
12
+ * Start the trigger with a handler function
13
+ * @param handler Function to call when webhook requests are received
14
+ */
15
+ async start(handler) {
16
+ this.handler = handler;
17
+ }
18
+ /**
19
+ * Stop the trigger
20
+ */
21
+ async stop() {
22
+ this.handler = void 0;
23
+ }
24
+ /**
25
+ * Health check - returns true if handler is registered
26
+ */
27
+ async healthCheck() {
28
+ return this.handler !== void 0;
29
+ }
30
+ /**
31
+ * Handle an incoming webhook request
32
+ *
33
+ * This method should be called by framework adapters to process webhook requests.
34
+ * It validates the signature, checks for replay attacks, creates a trigger event,
35
+ * and invokes the registered handler.
36
+ *
37
+ * @param request Web standard Request object
38
+ * @returns Web standard Response object
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // Hono adapter
43
+ * app.post('/webhook', async (c) => {
44
+ * const response = await trigger.handleRequest(c.req.raw);
45
+ * return response;
46
+ * });
47
+ *
48
+ * // Express adapter
49
+ * app.post('/webhook', async (req, res) => {
50
+ * const request = new Request(`http://localhost${req.url}`, {
51
+ * method: req.method,
52
+ * headers: req.headers,
53
+ * body: JSON.stringify(req.body),
54
+ * });
55
+ * const response = await trigger.handleRequest(request);
56
+ * res.status(response.status).json(await response.json());
57
+ * });
58
+ * ```
59
+ */
60
+ async handleRequest(request) {
61
+ if (!this.handler) {
62
+ return new Response(JSON.stringify({ error: "Trigger not started" }), {
63
+ status: 503,
64
+ headers: { "Content-Type": "application/json" }
65
+ });
66
+ }
67
+ try {
68
+ const bodyText = await request.text();
69
+ let data;
70
+ const contentType = request.headers.get("content-type") || "";
71
+ if (contentType.includes("application/json")) {
72
+ data = JSON.parse(bodyText);
73
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
74
+ const params = new URLSearchParams(bodyText);
75
+ data = Object.fromEntries(params.entries());
76
+ } else {
77
+ data = bodyText;
78
+ }
79
+ const isValid = await this.validateSignature(request, bodyText);
80
+ if (!isValid) {
81
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
82
+ status: 401,
83
+ headers: { "Content-Type": "application/json" }
84
+ });
85
+ }
86
+ if (this.config.timestampHeader) {
87
+ const isTimestampValid = this.validateTimestamp(request);
88
+ if (!isTimestampValid) {
89
+ return new Response(
90
+ JSON.stringify({ error: "Request timestamp too old or invalid" }),
91
+ {
92
+ status: 401,
93
+ headers: { "Content-Type": "application/json" }
94
+ }
95
+ );
96
+ }
97
+ }
98
+ const eventId = crypto.randomUUID();
99
+ const event = {
100
+ id: eventId,
101
+ type: this.type,
102
+ source: `webhook ${this.config.path}`,
103
+ data,
104
+ metadata: {
105
+ path: this.config.path,
106
+ headers: Object.fromEntries(request.headers.entries())
107
+ },
108
+ timestamp: /* @__PURE__ */ new Date()
109
+ };
110
+ await this.handler(event);
111
+ const response = {
112
+ success: true,
113
+ eventId
114
+ };
115
+ return new Response(JSON.stringify(response), {
116
+ status: 200,
117
+ headers: { "Content-Type": "application/json" }
118
+ });
119
+ } catch (error) {
120
+ return new Response(
121
+ JSON.stringify({
122
+ error: "Internal server error",
123
+ message: error.message
124
+ }),
125
+ {
126
+ status: 500,
127
+ headers: { "Content-Type": "application/json" }
128
+ }
129
+ );
130
+ }
131
+ }
132
+ /**
133
+ * Validate webhook signature using Web Crypto API
134
+ * Supports multiple signature formats:
135
+ * - GitHub: sha256=<hex>
136
+ * - Stripe: t=timestamp,v1=signature
137
+ * - Generic: <hex>
138
+ * @private
139
+ */
140
+ async validateSignature(request, payload) {
141
+ const signatureHeader = request.headers.get(this.config.signatureHeader);
142
+ if (!signatureHeader) {
143
+ return false;
144
+ }
145
+ try {
146
+ let signature;
147
+ let timestamp;
148
+ if (signatureHeader.includes("t=") && signatureHeader.includes("v1=")) {
149
+ const parts = signatureHeader.split(",");
150
+ const tPart = parts.find((p) => p.startsWith("t="));
151
+ const v1Part = parts.find((p) => p.startsWith("v1="));
152
+ if (!tPart || !v1Part) {
153
+ return false;
154
+ }
155
+ timestamp = parseInt(tPart.split("=")[1], 10);
156
+ signature = v1Part.split("=")[1];
157
+ const signedPayload = `${timestamp}.${payload}`;
158
+ return await this.verifyHmac(signedPayload, signature);
159
+ }
160
+ if (this.config.signaturePrefix && signatureHeader.startsWith(this.config.signaturePrefix)) {
161
+ signature = signatureHeader.substring(
162
+ this.config.signaturePrefix.length
163
+ );
164
+ } else if (signatureHeader.includes("=")) {
165
+ signature = signatureHeader.split("=")[1];
166
+ } else {
167
+ signature = signatureHeader;
168
+ }
169
+ return await this.verifyHmac(payload, signature);
170
+ } catch (error) {
171
+ console.error("Signature validation error:", error);
172
+ return false;
173
+ }
174
+ }
175
+ /**
176
+ * Verify HMAC signature using Web Crypto API
177
+ * @private
178
+ */
179
+ async verifyHmac(payload, expectedSignature) {
180
+ const encoder = new TextEncoder();
181
+ const keyData = encoder.encode(this.config.secret);
182
+ const messageData = encoder.encode(payload);
183
+ const hashAlgorithm = this.getHashAlgorithm(this.config.algorithm);
184
+ const cryptoKey = await crypto.subtle.importKey(
185
+ "raw",
186
+ keyData,
187
+ {
188
+ name: "HMAC",
189
+ hash: hashAlgorithm
190
+ },
191
+ false,
192
+ ["sign"]
193
+ );
194
+ const signatureBuffer = await crypto.subtle.sign(
195
+ "HMAC",
196
+ cryptoKey,
197
+ messageData
198
+ );
199
+ const hashArray = Array.from(new Uint8Array(signatureBuffer));
200
+ const computedSignature = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
201
+ return this.constantTimeCompare(computedSignature, expectedSignature);
202
+ }
203
+ /**
204
+ * Map algorithm name to Web Crypto API hash algorithm
205
+ * @private
206
+ */
207
+ getHashAlgorithm(algorithm) {
208
+ switch (algorithm) {
209
+ case "sha256":
210
+ return "SHA-256";
211
+ case "sha1":
212
+ return "SHA-1";
213
+ case "sha512":
214
+ return "SHA-512";
215
+ default:
216
+ return "SHA-256";
217
+ }
218
+ }
219
+ /**
220
+ * Constant-time string comparison to prevent timing attacks
221
+ * @private
222
+ */
223
+ constantTimeCompare(a, b) {
224
+ if (a.length !== b.length) {
225
+ return false;
226
+ }
227
+ let result = 0;
228
+ for (let i = 0; i < a.length; i++) {
229
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
230
+ }
231
+ return result === 0;
232
+ }
233
+ /**
234
+ * Validate timestamp to prevent replay attacks
235
+ * @private
236
+ */
237
+ validateTimestamp(request) {
238
+ if (!this.config.timestampHeader) {
239
+ return true;
240
+ }
241
+ const timestampHeader = request.headers.get(this.config.timestampHeader);
242
+ if (!timestampHeader) {
243
+ return false;
244
+ }
245
+ try {
246
+ let timestamp;
247
+ if (timestampHeader.includes("t=")) {
248
+ const tPart = timestampHeader.split(",").find((p) => p.startsWith("t="));
249
+ if (!tPart) {
250
+ return false;
251
+ }
252
+ timestamp = parseInt(tPart.split("=")[1], 10);
253
+ } else {
254
+ timestamp = parseInt(timestampHeader, 10);
255
+ }
256
+ if (isNaN(timestamp)) {
257
+ return false;
258
+ }
259
+ const now = Math.floor(Date.now() / 1e3);
260
+ const age = now - timestamp;
261
+ return age >= 0 && age <= this.config.timestampTolerance;
262
+ } catch (error) {
263
+ console.error("Timestamp validation error:", error);
264
+ return false;
265
+ }
266
+ }
267
+ };
268
+ export {
269
+ WebhookTrigger
270
+ };
271
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Trigger, TriggerHandler, TriggerEvent } from \"@stepflowjs/core\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type SignatureAlgorithm = \"sha256\" | \"sha1\" | \"sha512\";\n\nexport interface WebhookTriggerConfig {\n /** The webhook endpoint path */\n path: string;\n /** Secret for signature verification */\n secret: string;\n /** Header name containing the signature (default: 'x-webhook-signature') */\n signatureHeader?: string;\n /** Signing algorithm (default: 'sha256') */\n algorithm?: SignatureAlgorithm;\n /** Signature prefix like 'sha256=' (optional) */\n signaturePrefix?: string;\n /** Header name for timestamp (for replay attack prevention) */\n timestampHeader?: string;\n /** Maximum age in seconds for timestamp validation (default: 300) */\n timestampTolerance?: number;\n}\n\nexport interface WebhookTriggerResponse {\n success: boolean;\n eventId: string;\n}\n\n// ============================================================================\n// WebhookTrigger Implementation\n// ============================================================================\n\n/**\n * Webhook trigger for Stepflow workflows with signature verification\n *\n * Supports multiple webhook provider patterns:\n * - GitHub style: `sha256=<hex>`\n * - Stripe style: `t=timestamp,v1=signature`\n * - Generic HMAC\n *\n * Includes replay attack prevention via timestamp validation.\n *\n * @example\n * ```typescript\n * // GitHub webhook\n * const githubTrigger = new WebhookTrigger({\n * path: '/webhooks/github',\n * secret: process.env.GITHUB_WEBHOOK_SECRET,\n * signatureHeader: 'x-hub-signature-256',\n * algorithm: 'sha256',\n * signaturePrefix: 'sha256=',\n * });\n *\n * // Stripe webhook\n * const stripeTrigger = new WebhookTrigger({\n * path: '/webhooks/stripe',\n * secret: process.env.STRIPE_WEBHOOK_SECRET,\n * signatureHeader: 'stripe-signature',\n * algorithm: 'sha256',\n * timestampHeader: 'stripe-signature',\n * timestampTolerance: 300,\n * });\n *\n * await githubTrigger.start(async (event) => {\n * await stepflow.trigger('github-webhook', event.data);\n * });\n *\n * // In your framework adapter:\n * app.post('/webhooks/github', async (req) => {\n * const response = await githubTrigger.handleRequest(req);\n * return response;\n * });\n * ```\n */\nexport class WebhookTrigger implements Trigger<WebhookTriggerConfig> {\n readonly type = \"webhook\";\n private handler?: TriggerHandler;\n\n constructor(readonly config: WebhookTriggerConfig) {\n // Set defaults\n this.config.signatureHeader =\n config.signatureHeader || \"x-webhook-signature\";\n this.config.algorithm = config.algorithm || \"sha256\";\n this.config.timestampTolerance = config.timestampTolerance ?? 300;\n }\n\n /**\n * Start the trigger with a handler function\n * @param handler Function to call when webhook requests are received\n */\n async start(handler: TriggerHandler): Promise<void> {\n this.handler = handler;\n }\n\n /**\n * Stop the trigger\n */\n async stop(): Promise<void> {\n this.handler = undefined;\n }\n\n /**\n * Health check - returns true if handler is registered\n */\n async healthCheck(): Promise<boolean> {\n return this.handler !== undefined;\n }\n\n /**\n * Handle an incoming webhook request\n *\n * This method should be called by framework adapters to process webhook requests.\n * It validates the signature, checks for replay attacks, creates a trigger event,\n * and invokes the registered handler.\n *\n * @param request Web standard Request object\n * @returns Web standard Response object\n *\n * @example\n * ```typescript\n * // Hono adapter\n * app.post('/webhook', async (c) => {\n * const response = await trigger.handleRequest(c.req.raw);\n * return response;\n * });\n *\n * // Express adapter\n * app.post('/webhook', async (req, res) => {\n * const request = new Request(`http://localhost${req.url}`, {\n * method: req.method,\n * headers: req.headers,\n * body: JSON.stringify(req.body),\n * });\n * const response = await trigger.handleRequest(request);\n * res.status(response.status).json(await response.json());\n * });\n * ```\n */\n async handleRequest(request: Request): Promise<Response> {\n if (!this.handler) {\n return new Response(JSON.stringify({ error: \"Trigger not started\" }), {\n status: 503,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n try {\n // Read request body as text for signature verification\n const bodyText = await request.text();\n let data: unknown;\n\n // Parse body based on content type\n const contentType = request.headers.get(\"content-type\") || \"\";\n if (contentType.includes(\"application/json\")) {\n data = JSON.parse(bodyText);\n } else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n const params = new URLSearchParams(bodyText);\n data = Object.fromEntries(params.entries());\n } else {\n data = bodyText;\n }\n\n // Validate signature\n const isValid = await this.validateSignature(request, bodyText);\n if (!isValid) {\n return new Response(JSON.stringify({ error: \"Invalid signature\" }), {\n status: 401,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n // Validate timestamp (replay attack prevention)\n if (this.config.timestampHeader) {\n const isTimestampValid = this.validateTimestamp(request);\n if (!isTimestampValid) {\n return new Response(\n JSON.stringify({ error: \"Request timestamp too old or invalid\" }),\n {\n status: 401,\n headers: { \"Content-Type\": \"application/json\" },\n },\n );\n }\n }\n\n // Create trigger event\n const eventId = crypto.randomUUID();\n const event: TriggerEvent = {\n id: eventId,\n type: this.type,\n source: `webhook ${this.config.path}`,\n data,\n metadata: {\n path: this.config.path,\n headers: Object.fromEntries(request.headers.entries()),\n },\n timestamp: new Date(),\n };\n\n // Invoke handler\n await this.handler(event);\n\n // Return success response\n const response: WebhookTriggerResponse = {\n success: true,\n eventId,\n };\n\n return new Response(JSON.stringify(response), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: \"Internal server error\",\n message: (error as Error).message,\n }),\n {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n },\n );\n }\n }\n\n /**\n * Validate webhook signature using Web Crypto API\n * Supports multiple signature formats:\n * - GitHub: sha256=<hex>\n * - Stripe: t=timestamp,v1=signature\n * - Generic: <hex>\n * @private\n */\n private async validateSignature(\n request: Request,\n payload: string,\n ): Promise<boolean> {\n const signatureHeader = request.headers.get(this.config.signatureHeader!);\n\n if (!signatureHeader) {\n return false;\n }\n\n try {\n // Parse signature based on format\n let signature: string;\n let timestamp: number | undefined;\n\n // Stripe format: t=timestamp,v1=signature\n if (signatureHeader.includes(\"t=\") && signatureHeader.includes(\"v1=\")) {\n const parts = signatureHeader.split(\",\");\n const tPart = parts.find((p) => p.startsWith(\"t=\"));\n const v1Part = parts.find((p) => p.startsWith(\"v1=\"));\n\n if (!tPart || !v1Part) {\n return false;\n }\n\n timestamp = parseInt(tPart.split(\"=\")[1], 10);\n signature = v1Part.split(\"=\")[1];\n\n // For Stripe, payload includes timestamp\n const signedPayload = `${timestamp}.${payload}`;\n return await this.verifyHmac(signedPayload, signature);\n }\n\n // GitHub format: sha256=<hex> or algorithm prefix\n if (\n this.config.signaturePrefix &&\n signatureHeader.startsWith(this.config.signaturePrefix)\n ) {\n signature = signatureHeader.substring(\n this.config.signaturePrefix.length,\n );\n } else if (signatureHeader.includes(\"=\")) {\n // Generic prefix format\n signature = signatureHeader.split(\"=\")[1];\n } else {\n // No prefix\n signature = signatureHeader;\n }\n\n return await this.verifyHmac(payload, signature);\n } catch (error) {\n console.error(\"Signature validation error:\", error);\n return false;\n }\n }\n\n /**\n * Verify HMAC signature using Web Crypto API\n * @private\n */\n private async verifyHmac(\n payload: string,\n expectedSignature: string,\n ): Promise<boolean> {\n const encoder = new TextEncoder();\n const keyData = encoder.encode(this.config.secret);\n const messageData = encoder.encode(payload);\n\n // Map algorithm to Web Crypto API hash name\n const hashAlgorithm = this.getHashAlgorithm(this.config.algorithm!);\n\n // Import key for HMAC\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyData,\n {\n name: \"HMAC\",\n hash: hashAlgorithm,\n },\n false,\n [\"sign\"],\n );\n\n // Generate signature\n const signatureBuffer = await crypto.subtle.sign(\n \"HMAC\",\n cryptoKey,\n messageData,\n );\n\n // Convert to hex string\n const hashArray = Array.from(new Uint8Array(signatureBuffer));\n const computedSignature = hashArray\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n // Constant-time comparison\n return this.constantTimeCompare(computedSignature, expectedSignature);\n }\n\n /**\n * Map algorithm name to Web Crypto API hash algorithm\n * @private\n */\n private getHashAlgorithm(algorithm: SignatureAlgorithm): string {\n switch (algorithm) {\n case \"sha256\":\n return \"SHA-256\";\n case \"sha1\":\n return \"SHA-1\";\n case \"sha512\":\n return \"SHA-512\";\n default:\n return \"SHA-256\";\n }\n }\n\n /**\n * Constant-time string comparison to prevent timing attacks\n * @private\n */\n private constantTimeCompare(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return result === 0;\n }\n\n /**\n * Validate timestamp to prevent replay attacks\n * @private\n */\n private validateTimestamp(request: Request): boolean {\n if (!this.config.timestampHeader) {\n return true;\n }\n\n const timestampHeader = request.headers.get(this.config.timestampHeader);\n\n if (!timestampHeader) {\n return false;\n }\n\n try {\n let timestamp: number;\n\n // Stripe format: t=timestamp,v1=signature\n if (timestampHeader.includes(\"t=\")) {\n const tPart = timestampHeader\n .split(\",\")\n .find((p) => p.startsWith(\"t=\"));\n if (!tPart) {\n return false;\n }\n timestamp = parseInt(tPart.split(\"=\")[1], 10);\n } else {\n // Unix timestamp\n timestamp = parseInt(timestampHeader, 10);\n }\n\n if (isNaN(timestamp)) {\n return false;\n }\n\n const now = Math.floor(Date.now() / 1000);\n const age = now - timestamp;\n\n // Check if timestamp is within tolerance\n return age >= 0 && age <= this.config.timestampTolerance!;\n } catch (error) {\n console.error(\"Timestamp validation error:\", error);\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@stepflowjs/trigger-webhook",
3
+ "version": "0.0.1",
4
+ "description": "Webhook trigger for Stepflow with signature verification",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@stepflowjs/core": "0.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "tsup": "^8.5.1",
23
+ "vitest": "^4.0.17"
24
+ },
25
+ "peerDependencies": {
26
+ "typescript": "^5.0.0"
27
+ },
28
+ "license": "MIT",
29
+ "author": "Stepflow Contributors",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://stepflow-production.up.railway.app",
33
+ "directory": "packages/triggers/webhook"
34
+ },
35
+ "homepage": "https://stepflow-production.up.railway.app",
36
+ "bugs": {
37
+ "url": "https://stepflow-production.up.railway.app"
38
+ },
39
+ "keywords": [
40
+ "stepflow",
41
+ "trigger",
42
+ "webhook",
43
+ "signature",
44
+ "verification",
45
+ "workflow",
46
+ "orchestration"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest",
56
+ "clean": "rm -rf dist"
57
+ }
58
+ }