@transloadit/convex 0.0.3 → 0.0.4

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.
Files changed (81) hide show
  1. package/README.md +114 -134
  2. package/dist/client/index.d.ts +24 -13
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +14 -3
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -2
  7. package/dist/component/_generated/dataModel.d.ts +1 -1
  8. package/dist/component/_generated/server.d.ts +1 -1
  9. package/dist/component/apiUtils.d.ts +26 -6
  10. package/dist/component/apiUtils.d.ts.map +1 -1
  11. package/dist/component/apiUtils.js +48 -38
  12. package/dist/component/apiUtils.js.map +1 -1
  13. package/dist/component/lib.d.ts +7 -9
  14. package/dist/component/lib.d.ts.map +1 -1
  15. package/dist/component/lib.js +74 -18
  16. package/dist/component/lib.js.map +1 -1
  17. package/dist/component/schema.d.ts +4 -6
  18. package/dist/component/schema.d.ts.map +1 -1
  19. package/dist/component/schema.js +0 -7
  20. package/dist/component/schema.js.map +1 -1
  21. package/dist/debug/index.d.ts +19 -0
  22. package/dist/debug/index.d.ts.map +1 -0
  23. package/dist/debug/index.js +49 -0
  24. package/dist/debug/index.js.map +1 -0
  25. package/dist/react/index.d.ts +201 -3
  26. package/dist/react/index.d.ts.map +1 -1
  27. package/dist/react/index.js +674 -94
  28. package/dist/react/index.js.map +1 -1
  29. package/dist/shared/assemblyUrls.d.ts +10 -0
  30. package/dist/shared/assemblyUrls.d.ts.map +1 -0
  31. package/dist/shared/assemblyUrls.js +26 -0
  32. package/dist/shared/assemblyUrls.js.map +1 -0
  33. package/dist/shared/errors.d.ts +7 -0
  34. package/dist/shared/errors.d.ts.map +1 -0
  35. package/dist/shared/errors.js +10 -0
  36. package/dist/shared/errors.js.map +1 -0
  37. package/dist/shared/pollAssembly.d.ts +12 -0
  38. package/dist/shared/pollAssembly.d.ts.map +1 -0
  39. package/dist/shared/pollAssembly.js +50 -0
  40. package/dist/shared/pollAssembly.js.map +1 -0
  41. package/dist/shared/resultTypes.d.ts +37 -0
  42. package/dist/shared/resultTypes.d.ts.map +1 -0
  43. package/dist/shared/resultTypes.js +2 -0
  44. package/dist/shared/resultTypes.js.map +1 -0
  45. package/dist/shared/resultUtils.d.ts +4 -0
  46. package/dist/shared/resultUtils.d.ts.map +1 -0
  47. package/dist/shared/resultUtils.js +69 -0
  48. package/dist/shared/resultUtils.js.map +1 -0
  49. package/dist/shared/tusUpload.d.ts +13 -0
  50. package/dist/shared/tusUpload.d.ts.map +1 -0
  51. package/dist/shared/tusUpload.js +32 -0
  52. package/dist/shared/tusUpload.js.map +1 -0
  53. package/dist/test/index.d.ts +4 -4
  54. package/dist/test/nodeModules.d.ts +2 -0
  55. package/dist/test/nodeModules.d.ts.map +1 -0
  56. package/dist/test/nodeModules.js +19 -0
  57. package/dist/test/nodeModules.js.map +1 -0
  58. package/package.json +36 -6
  59. package/src/client/index.ts +73 -7
  60. package/src/component/_generated/api.ts +2 -2
  61. package/src/component/_generated/dataModel.ts +1 -1
  62. package/src/component/_generated/server.ts +1 -1
  63. package/src/component/apiUtils.test.ts +166 -2
  64. package/src/component/apiUtils.ts +96 -64
  65. package/src/component/lib.test.ts +170 -4
  66. package/src/component/lib.ts +113 -25
  67. package/src/component/schema.ts +0 -10
  68. package/src/debug/index.ts +84 -0
  69. package/src/react/index.test.tsx +340 -0
  70. package/src/react/index.tsx +1089 -179
  71. package/src/react/uploadWithTus.test.tsx +192 -0
  72. package/src/shared/assemblyUrls.test.ts +71 -0
  73. package/src/shared/assemblyUrls.ts +59 -0
  74. package/src/shared/errors.ts +23 -0
  75. package/src/shared/pollAssembly.ts +65 -0
  76. package/src/shared/resultTypes.ts +44 -0
  77. package/src/shared/resultUtils.test.ts +29 -0
  78. package/src/shared/resultUtils.ts +71 -0
  79. package/src/shared/tusUpload.ts +59 -0
  80. package/src/test/index.ts +1 -1
  81. package/src/test/nodeModules.ts +19 -0
@@ -1,11 +1,71 @@
1
- import type { AssemblyStatus } from "@transloadit/types/assemblyStatus";
2
- import type { AssemblyInstructionsInput } from "@transloadit/types/template";
1
+ import type { AssemblyStatus } from "@transloadit/zod/v3/assemblyStatus";
2
+ import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template";
3
3
  import { actionGeneric, mutationGeneric, queryGeneric } from "convex/server";
4
4
  import { type Infer, v } from "convex/values";
5
- import type { ComponentApi } from "../component/_generated/component.js";
6
- import type { RunActionCtx, RunMutationCtx, RunQueryCtx } from "./types.js";
5
+ import type { ComponentApi } from "../component/_generated/component.ts";
6
+ import type { RunActionCtx, RunMutationCtx, RunQueryCtx } from "./types.ts";
7
7
 
8
- export { parseTransloaditWebhook } from "../component/apiUtils.js";
8
+ export {
9
+ assemblyStatusErrCodeSchema,
10
+ assemblyStatusOkCodeSchema,
11
+ assemblyStatusResultsSchema,
12
+ assemblyStatusSchema,
13
+ isAssemblyBusy,
14
+ isAssemblyBusyStatus,
15
+ isAssemblyErrorStatus,
16
+ isAssemblyOkStatus,
17
+ isAssemblySysError,
18
+ isAssemblyTerminal,
19
+ isAssemblyTerminalError,
20
+ isAssemblyTerminalOk,
21
+ isAssemblyTerminalOkStatus,
22
+ } from "@transloadit/zod/v3/assemblyStatus";
23
+ export type {
24
+ ParsedWebhookRequest,
25
+ VerifiedWebhookRequest,
26
+ WebhookActionArgs,
27
+ } from "../component/apiUtils.ts";
28
+ export {
29
+ buildWebhookQueueArgs,
30
+ handleWebhookRequest,
31
+ parseAndVerifyTransloaditWebhook,
32
+ parseTransloaditWebhook,
33
+ } from "../component/apiUtils.ts";
34
+ export type {
35
+ NormalizedAssemblyUrls,
36
+ TransloaditAssembly,
37
+ } from "../shared/assemblyUrls.ts";
38
+ export {
39
+ ASSEMBLY_STATUS_COMPLETED,
40
+ ASSEMBLY_STATUS_UPLOADING,
41
+ getAssemblyStage,
42
+ isAssemblyCompletedStatus,
43
+ isAssemblyUploadingStatus,
44
+ normalizeAssemblyUploadUrls,
45
+ parseAssemblyFields,
46
+ parseAssemblyResults,
47
+ parseAssemblyStatus,
48
+ parseAssemblyUrls,
49
+ } from "../shared/assemblyUrls.ts";
50
+ export { pollAssembly } from "../shared/pollAssembly.ts";
51
+ export type {
52
+ ImageResizeResult,
53
+ ResultByRobot,
54
+ ResultForRobot,
55
+ StoreResult,
56
+ TransloaditResult,
57
+ VideoEncodeResult,
58
+ VideoThumbsResult,
59
+ } from "../shared/resultTypes.ts";
60
+ export {
61
+ getResultOriginalKey,
62
+ getResultUrl,
63
+ } from "../shared/resultUtils.ts";
64
+ export type {
65
+ TusMetadataOptions,
66
+ TusUploadConfig,
67
+ } from "../shared/tusUpload.ts";
68
+ export { buildTusUploadConfig } from "../shared/tusUpload.ts";
9
69
  export type { AssemblyStatus, AssemblyInstructionsInput };
10
70
 
11
71
  export interface TransloaditConfig {
@@ -74,6 +134,9 @@ export const vCreateAssemblyArgs = v.object({
74
134
  userId: v.optional(v.string()),
75
135
  });
76
136
 
137
+ /**
138
+ * @deprecated Prefer `makeTransloaditAPI` or `Transloadit` for new code.
139
+ */
77
140
  export class TransloaditClient {
78
141
  declare component: TransloaditComponent;
79
142
  declare config: TransloaditConfig;
@@ -172,6 +235,9 @@ export class TransloaditClient {
172
235
 
173
236
  export class Transloadit extends TransloaditClient {}
174
237
 
238
+ /**
239
+ * @deprecated Prefer `new Transloadit(...)` or `makeTransloaditAPI(...)`.
240
+ */
175
241
  export function createTransloadit(
176
242
  component: TransloaditComponent,
177
243
  config?: Partial<TransloaditConfig>,
@@ -207,11 +273,12 @@ export function makeTransloaditAPI(
207
273
  payload: v.any(),
208
274
  rawBody: v.optional(v.string()),
209
275
  signature: v.optional(v.string()),
210
- verifySignature: v.optional(v.boolean()),
211
276
  },
212
277
  returns: v.object({
213
278
  assemblyId: v.string(),
214
279
  resultCount: v.number(),
280
+ ok: v.optional(v.string()),
281
+ status: v.optional(v.string()),
215
282
  }),
216
283
  handler: async (ctx, args) => {
217
284
  return ctx.runAction(component.lib.handleWebhook, {
@@ -225,7 +292,6 @@ export function makeTransloaditAPI(
225
292
  payload: v.any(),
226
293
  rawBody: v.optional(v.string()),
227
294
  signature: v.optional(v.string()),
228
- verifySignature: v.optional(v.boolean()),
229
295
  },
230
296
  returns: v.object({
231
297
  assemblyId: v.string(),
@@ -8,8 +8,8 @@
8
8
  * @module
9
9
  */
10
10
 
11
- import type * as apiUtils from "../apiUtils.js";
12
- import type * as lib from "../lib.js";
11
+ import type * as apiUtils from "../apiUtils.ts";
12
+ import type * as lib from "../lib.ts";
13
13
 
14
14
  import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server";
15
15
  import { anyApi, componentsGeneric } from "convex/server";
@@ -15,7 +15,7 @@ import type {
15
15
  SystemTableNames,
16
16
  } from "convex/server";
17
17
  import type { GenericId } from "convex/values";
18
- import schema from "../schema.js";
18
+ import schema from "../schema.ts";
19
19
 
20
20
  export type TableNames = TableNamesInDataModel<DataModel>;
21
21
 
@@ -28,7 +28,7 @@ import {
28
28
  internalMutationGeneric,
29
29
  internalQueryGeneric,
30
30
  } from "convex/server";
31
- import type { DataModel } from "./dataModel.js";
31
+ import type { DataModel } from "./dataModel.ts";
32
32
 
33
33
  export const query: QueryBuilder<DataModel, "public"> = queryGeneric;
34
34
 
@@ -1,11 +1,14 @@
1
1
  import { createHmac } from "node:crypto";
2
- import { describe, expect, test } from "vitest";
2
+ import { describe, expect, test, vi } from "vitest";
3
3
  import {
4
4
  buildTransloaditParams,
5
+ buildWebhookQueueArgs,
6
+ handleWebhookRequest,
7
+ parseAndVerifyTransloaditWebhook,
5
8
  parseTransloaditWebhook,
6
9
  signTransloaditParams,
7
10
  verifyWebhookSignature,
8
- } from "./apiUtils.js";
11
+ } from "./apiUtils.ts";
9
12
 
10
13
  describe("apiUtils", () => {
11
14
  test("buildTransloaditParams requires templateId or steps", () => {
@@ -74,4 +77,165 @@ describe("apiUtils", () => {
74
77
  "Missing transloadit payload",
75
78
  );
76
79
  });
80
+
81
+ test("parseAndVerifyTransloaditWebhook verifies signature", async () => {
82
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
83
+ const rawBody = JSON.stringify(payload);
84
+ const secret = "webhook-secret";
85
+ const digest = createHmac("sha384", secret).update(rawBody).digest("hex");
86
+ const formData = new FormData();
87
+ formData.append("transloadit", rawBody);
88
+ formData.append("signature", `sha384:${digest}`);
89
+
90
+ const request = new Request("http://localhost", {
91
+ method: "POST",
92
+ body: formData,
93
+ });
94
+
95
+ const parsed = await parseAndVerifyTransloaditWebhook(request, {
96
+ authSecret: secret,
97
+ });
98
+
99
+ expect(parsed.payload).toEqual(payload);
100
+ expect(parsed.verified).toBe(true);
101
+ });
102
+
103
+ test("parseAndVerifyTransloaditWebhook rejects invalid signature", async () => {
104
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
105
+ const formData = new FormData();
106
+ formData.append("transloadit", JSON.stringify(payload));
107
+ formData.append("signature", "sha384:bad");
108
+
109
+ const request = new Request("http://localhost", {
110
+ method: "POST",
111
+ body: formData,
112
+ });
113
+
114
+ await expect(
115
+ parseAndVerifyTransloaditWebhook(request, {
116
+ authSecret: "secret",
117
+ }),
118
+ ).rejects.toThrow("Invalid Transloadit webhook signature");
119
+ });
120
+
121
+ test("buildWebhookQueueArgs returns webhook payload args", async () => {
122
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
123
+ const rawBody = JSON.stringify(payload);
124
+ const secret = "webhook-secret";
125
+ const digest = createHmac("sha384", secret).update(rawBody).digest("hex");
126
+ const formData = new FormData();
127
+ formData.append("transloadit", rawBody);
128
+ formData.append("signature", `sha384:${digest}`);
129
+
130
+ const request = new Request("http://localhost", {
131
+ method: "POST",
132
+ body: formData,
133
+ });
134
+
135
+ const args = await buildWebhookQueueArgs(request, { authSecret: secret });
136
+ expect(args.payload).toEqual(payload);
137
+ expect(args.rawBody).toBe(rawBody);
138
+ expect(args.signature).toBe(`sha384:${digest}`);
139
+ });
140
+
141
+ test("buildWebhookQueueArgs can skip verification", async () => {
142
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
143
+ const rawBody = JSON.stringify(payload);
144
+ const formData = new FormData();
145
+ formData.append("transloadit", rawBody);
146
+
147
+ const request = new Request("http://localhost", {
148
+ method: "POST",
149
+ body: formData,
150
+ });
151
+
152
+ const args = await buildWebhookQueueArgs(request, {
153
+ authSecret: "secret",
154
+ requireSignature: false,
155
+ });
156
+ expect(args.payload).toEqual(payload);
157
+ expect(args.rawBody).toBe(rawBody);
158
+ expect(args.signature).toBeUndefined();
159
+ });
160
+
161
+ test("handleWebhookRequest queues webhook by default", async () => {
162
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
163
+ const rawBody = JSON.stringify(payload);
164
+ const formData = new FormData();
165
+ formData.append("transloadit", rawBody);
166
+ formData.append("signature", "sha384:abc");
167
+
168
+ const request = new Request("http://localhost", {
169
+ method: "POST",
170
+ body: formData,
171
+ });
172
+ const runAction = vi.fn().mockResolvedValue(null);
173
+
174
+ const response = await handleWebhookRequest(request, {
175
+ runAction,
176
+ });
177
+
178
+ expect(runAction).toHaveBeenCalledWith({
179
+ payload,
180
+ rawBody,
181
+ signature: "sha384:abc",
182
+ });
183
+ expect(response.status).toBe(202);
184
+ });
185
+
186
+ test("handleWebhookRequest supports sync mode with verification", async () => {
187
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
188
+ const rawBody = JSON.stringify(payload);
189
+ const secret = "webhook-secret";
190
+ const digest = createHmac("sha384", secret).update(rawBody).digest("hex");
191
+ const formData = new FormData();
192
+ formData.append("transloadit", rawBody);
193
+ formData.append("signature", `sha384:${digest}`);
194
+
195
+ const request = new Request("http://localhost", {
196
+ method: "POST",
197
+ body: formData,
198
+ });
199
+ const runAction = vi.fn().mockResolvedValue(null);
200
+
201
+ const response = await handleWebhookRequest(request, {
202
+ mode: "sync",
203
+ runAction,
204
+ requireSignature: true,
205
+ authSecret: secret,
206
+ });
207
+
208
+ expect(runAction).toHaveBeenCalledWith({
209
+ payload,
210
+ rawBody,
211
+ signature: `sha384:${digest}`,
212
+ });
213
+ expect(response.status).toBe(204);
214
+ });
215
+
216
+ test("handleWebhookRequest honors a custom response status", async () => {
217
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
218
+ const rawBody = JSON.stringify(payload);
219
+ const formData = new FormData();
220
+ formData.append("transloadit", rawBody);
221
+ formData.append("signature", "sha384:abc");
222
+
223
+ const request = new Request("http://localhost", {
224
+ method: "POST",
225
+ body: formData,
226
+ });
227
+ const runAction = vi.fn().mockResolvedValue(null);
228
+
229
+ const response = await handleWebhookRequest(request, {
230
+ runAction,
231
+ responseStatus: 299,
232
+ });
233
+
234
+ expect(runAction).toHaveBeenCalledWith({
235
+ payload,
236
+ rawBody,
237
+ signature: "sha384:abc",
238
+ });
239
+ expect(response.status).toBe(299);
240
+ });
77
241
  });
@@ -1,5 +1,7 @@
1
- import type { AssemblyStatusResults } from "@transloadit/types/assemblyStatus";
2
- import type { AssemblyInstructionsInput } from "@transloadit/types/template";
1
+ import { signParams, verifyWebhookSignature } from "@transloadit/utils";
2
+ import type { AssemblyStatusResults } from "@transloadit/zod/v3/assemblyStatus";
3
+ import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template";
4
+ import { transloaditError } from "../shared/errors.ts";
3
5
 
4
6
  export interface TransloaditAuthConfig {
5
7
  authKey: string;
@@ -26,7 +28,10 @@ export function buildTransloaditParams(
26
28
  options: BuildParamsOptions,
27
29
  ): BuildParamsResult {
28
30
  if (!options.templateId && !options.steps) {
29
- throw new Error("Provide either templateId or steps to create an Assembly");
31
+ throw transloaditError(
32
+ "createAssembly",
33
+ "Provide either templateId or steps to create an Assembly",
34
+ );
30
35
  }
31
36
 
32
37
  const auth: Record<string, string> = {
@@ -62,40 +67,11 @@ export function buildTransloaditParams(
62
67
  };
63
68
  }
64
69
 
65
- async function hmacHex(
66
- algorithm: "SHA-384" | "SHA-1",
67
- key: string,
68
- data: string,
69
- ): Promise<string> {
70
- if (!globalThis.crypto?.subtle) {
71
- throw new Error("Web Crypto is required to sign Transloadit payloads");
72
- }
73
-
74
- const encoder = new TextEncoder();
75
- const cryptoKey = await globalThis.crypto.subtle.importKey(
76
- "raw",
77
- encoder.encode(key),
78
- { name: "HMAC", hash: { name: algorithm } },
79
- false,
80
- ["sign"],
81
- );
82
- const signature = await globalThis.crypto.subtle.sign(
83
- "HMAC",
84
- cryptoKey,
85
- encoder.encode(data),
86
- );
87
- const bytes = new Uint8Array(signature);
88
- return Array.from(bytes)
89
- .map((byte) => byte.toString(16).padStart(2, "0"))
90
- .join("");
91
- }
92
-
93
70
  export async function signTransloaditParams(
94
71
  paramsString: string,
95
72
  authSecret: string,
96
73
  ): Promise<string> {
97
- const signature = await hmacHex("SHA-384", authSecret, paramsString);
98
- return `sha384:${signature}`;
74
+ return signParams(paramsString, authSecret, "sha384");
99
75
  }
100
76
 
101
77
  export type ParsedWebhookRequest = {
@@ -104,6 +80,10 @@ export type ParsedWebhookRequest = {
104
80
  signature?: string;
105
81
  };
106
82
 
83
+ export type VerifiedWebhookRequest = ParsedWebhookRequest & {
84
+ verified: boolean;
85
+ };
86
+
107
87
  export async function parseTransloaditWebhook(
108
88
  request: Request,
109
89
  ): Promise<ParsedWebhookRequest> {
@@ -112,7 +92,7 @@ export async function parseTransloaditWebhook(
112
92
  const signature = formData.get("signature");
113
93
 
114
94
  if (typeof rawPayload !== "string") {
115
- throw new Error("Missing transloadit payload");
95
+ throw transloaditError("webhook", "Missing transloadit payload");
116
96
  }
117
97
 
118
98
  return {
@@ -122,44 +102,96 @@ export async function parseTransloaditWebhook(
122
102
  };
123
103
  }
124
104
 
125
- function safeCompare(a: string, b: string): boolean {
126
- if (a.length !== b.length) return false;
127
- let mismatch = 0;
128
- for (let i = 0; i < a.length; i += 1) {
129
- mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
105
+ export async function parseAndVerifyTransloaditWebhook(
106
+ request: Request,
107
+ options: {
108
+ authSecret: string;
109
+ requireSignature?: boolean;
110
+ },
111
+ ): Promise<VerifiedWebhookRequest> {
112
+ const parsed = await parseTransloaditWebhook(request);
113
+ const authSecret = options.authSecret;
114
+ if (!authSecret) {
115
+ throw transloaditError(
116
+ "webhook",
117
+ "Missing authSecret for webhook verification",
118
+ );
119
+ }
120
+ const verified = await verifyWebhookSignature({
121
+ rawBody: parsed.rawBody,
122
+ signatureHeader: parsed.signature,
123
+ authSecret,
124
+ });
125
+
126
+ if (options.requireSignature ?? true) {
127
+ if (!verified) {
128
+ throw transloaditError(
129
+ "webhook",
130
+ "Invalid Transloadit webhook signature",
131
+ );
132
+ }
130
133
  }
131
- return mismatch === 0;
132
- }
133
-
134
- export async function verifyWebhookSignature(options: {
135
- rawBody: string;
136
- signatureHeader?: string;
137
- authSecret: string;
138
- }): Promise<boolean> {
139
- if (!options.signatureHeader) return false;
140
134
 
141
- const signatureHeader = options.signatureHeader.trim();
142
- if (!signatureHeader) return false;
135
+ return { ...parsed, verified };
136
+ }
143
137
 
144
- const [prefix, sig] = signatureHeader.includes(":")
145
- ? (signatureHeader.split(":") as [string, string])
146
- : ["sha1", signatureHeader];
138
+ export async function buildWebhookQueueArgs(
139
+ request: Request,
140
+ options: {
141
+ authSecret: string;
142
+ requireSignature?: boolean;
143
+ },
144
+ ): Promise<ParsedWebhookRequest> {
145
+ if (options.requireSignature === false) {
146
+ return parseTransloaditWebhook(request);
147
+ }
147
148
 
148
- const normalized = prefix.toLowerCase();
149
- const algorithm = normalized === "sha384" ? "SHA-384" : "SHA-1";
149
+ const parsed = await parseAndVerifyTransloaditWebhook(request, options);
150
+ return {
151
+ payload: parsed.payload,
152
+ rawBody: parsed.rawBody,
153
+ signature: parsed.signature,
154
+ };
155
+ }
150
156
 
151
- if (normalized !== "sha384" && normalized !== "sha1") {
152
- return false;
153
- }
157
+ export type WebhookActionArgs = {
158
+ payload: unknown;
159
+ rawBody?: string;
160
+ signature?: string;
161
+ };
154
162
 
155
- const expected = await hmacHex(
156
- algorithm,
157
- options.authSecret,
158
- options.rawBody,
159
- );
160
- return safeCompare(expected, sig);
163
+ export async function handleWebhookRequest(
164
+ request: Request,
165
+ options: {
166
+ mode?: "queue" | "sync";
167
+ runAction: (args: WebhookActionArgs) => Promise<unknown>;
168
+ requireSignature?: boolean;
169
+ authSecret?: string;
170
+ responseStatus?: number;
171
+ },
172
+ ): Promise<Response> {
173
+ const mode = options.mode ?? "queue";
174
+ const requireSignature = options.requireSignature ?? false;
175
+
176
+ const parsed = requireSignature
177
+ ? await parseAndVerifyTransloaditWebhook(request, {
178
+ authSecret: options.authSecret ?? "",
179
+ requireSignature: true,
180
+ })
181
+ : await parseTransloaditWebhook(request);
182
+
183
+ await options.runAction({
184
+ payload: parsed.payload,
185
+ rawBody: parsed.rawBody,
186
+ signature: parsed.signature,
187
+ });
188
+
189
+ const status = options.responseStatus ?? (mode === "sync" ? 204 : 202);
190
+ return new Response(null, { status });
161
191
  }
162
192
 
193
+ export { verifyWebhookSignature };
194
+
163
195
  export type AssemblyResult = AssemblyStatusResults[string][number];
164
196
 
165
197
  export type AssemblyResultRecord = {