@transloadit/convex 0.0.3 → 0.0.5

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 (85) hide show
  1. package/README.md +154 -122
  2. package/dist/client/index.d.ts +54 -13
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +48 -5
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -2
  7. package/dist/component/_generated/component.d.ts +11 -0
  8. package/dist/component/_generated/component.d.ts.map +1 -1
  9. package/dist/component/_generated/dataModel.d.ts +1 -1
  10. package/dist/component/_generated/server.d.ts +1 -1
  11. package/dist/component/apiUtils.d.ts +26 -6
  12. package/dist/component/apiUtils.d.ts.map +1 -1
  13. package/dist/component/apiUtils.js +48 -38
  14. package/dist/component/apiUtils.js.map +1 -1
  15. package/dist/component/lib.d.ts +37 -8
  16. package/dist/component/lib.d.ts.map +1 -1
  17. package/dist/component/lib.js +145 -18
  18. package/dist/component/lib.js.map +1 -1
  19. package/dist/component/schema.d.ts +9 -6
  20. package/dist/component/schema.d.ts.map +1 -1
  21. package/dist/component/schema.js +4 -8
  22. package/dist/component/schema.js.map +1 -1
  23. package/dist/debug/index.d.ts +19 -0
  24. package/dist/debug/index.d.ts.map +1 -0
  25. package/dist/debug/index.js +49 -0
  26. package/dist/debug/index.js.map +1 -0
  27. package/dist/react/index.d.ts +201 -3
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/react/index.js +674 -94
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/shared/assemblyUrls.d.ts +10 -0
  32. package/dist/shared/assemblyUrls.d.ts.map +1 -0
  33. package/dist/shared/assemblyUrls.js +26 -0
  34. package/dist/shared/assemblyUrls.js.map +1 -0
  35. package/dist/shared/errors.d.ts +7 -0
  36. package/dist/shared/errors.d.ts.map +1 -0
  37. package/dist/shared/errors.js +10 -0
  38. package/dist/shared/errors.js.map +1 -0
  39. package/dist/shared/pollAssembly.d.ts +12 -0
  40. package/dist/shared/pollAssembly.d.ts.map +1 -0
  41. package/dist/shared/pollAssembly.js +50 -0
  42. package/dist/shared/pollAssembly.js.map +1 -0
  43. package/dist/shared/resultTypes.d.ts +37 -0
  44. package/dist/shared/resultTypes.d.ts.map +1 -0
  45. package/dist/shared/resultTypes.js +2 -0
  46. package/dist/shared/resultTypes.js.map +1 -0
  47. package/dist/shared/resultUtils.d.ts +4 -0
  48. package/dist/shared/resultUtils.d.ts.map +1 -0
  49. package/dist/shared/resultUtils.js +69 -0
  50. package/dist/shared/resultUtils.js.map +1 -0
  51. package/dist/shared/tusUpload.d.ts +13 -0
  52. package/dist/shared/tusUpload.d.ts.map +1 -0
  53. package/dist/shared/tusUpload.js +32 -0
  54. package/dist/shared/tusUpload.js.map +1 -0
  55. package/dist/test/index.d.ts +9 -4
  56. package/dist/test/index.d.ts.map +1 -1
  57. package/dist/test/nodeModules.d.ts +2 -0
  58. package/dist/test/nodeModules.d.ts.map +1 -0
  59. package/dist/test/nodeModules.js +19 -0
  60. package/dist/test/nodeModules.js.map +1 -0
  61. package/package.json +40 -7
  62. package/src/client/index.ts +111 -9
  63. package/src/component/_generated/api.ts +2 -2
  64. package/src/component/_generated/component.ts +14 -0
  65. package/src/component/_generated/dataModel.ts +1 -1
  66. package/src/component/_generated/server.ts +1 -1
  67. package/src/component/apiUtils.test.ts +166 -2
  68. package/src/component/apiUtils.ts +96 -64
  69. package/src/component/lib.test.ts +213 -4
  70. package/src/component/lib.ts +192 -25
  71. package/src/component/schema.ts +4 -11
  72. package/src/debug/index.ts +84 -0
  73. package/src/react/index.test.tsx +340 -0
  74. package/src/react/index.tsx +1089 -179
  75. package/src/react/uploadWithTus.test.tsx +192 -0
  76. package/src/shared/assemblyUrls.test.ts +71 -0
  77. package/src/shared/assemblyUrls.ts +59 -0
  78. package/src/shared/errors.ts +23 -0
  79. package/src/shared/pollAssembly.ts +65 -0
  80. package/src/shared/resultTypes.ts +44 -0
  81. package/src/shared/resultUtils.test.ts +29 -0
  82. package/src/shared/resultUtils.ts +71 -0
  83. package/src/shared/tusUpload.ts +59 -0
  84. package/src/test/index.ts +1 -1
  85. 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 {
@@ -51,6 +111,8 @@ export const vAssemblyResultResponse = v.object({
51
111
  _id: v.string(),
52
112
  _creationTime: v.number(),
53
113
  assemblyId: v.string(),
114
+ album: v.optional(v.string()),
115
+ userId: v.optional(v.string()),
54
116
  stepName: v.string(),
55
117
  resultId: v.optional(v.string()),
56
118
  sslUrl: v.optional(v.string()),
@@ -74,6 +136,9 @@ export const vCreateAssemblyArgs = v.object({
74
136
  userId: v.optional(v.string()),
75
137
  });
76
138
 
139
+ /**
140
+ * @deprecated Prefer `makeTransloaditAPI` or `Transloadit` for new code.
141
+ */
77
142
  export class TransloaditClient {
78
143
  declare component: TransloaditComponent;
79
144
  declare config: TransloaditConfig;
@@ -158,6 +223,13 @@ export class TransloaditClient {
158
223
  return ctx.runQuery(this.component.lib.listResults, args);
159
224
  }
160
225
 
226
+ async listAlbumResults(
227
+ ctx: RunQueryCtx,
228
+ args: { album: string; limit?: number },
229
+ ) {
230
+ return ctx.runQuery(this.component.lib.listAlbumResults, args);
231
+ }
232
+
161
233
  async storeAssemblyMetadata(
162
234
  ctx: RunMutationCtx,
163
235
  args: { assemblyId: string; userId?: string; fields?: unknown },
@@ -172,6 +244,9 @@ export class TransloaditClient {
172
244
 
173
245
  export class Transloadit extends TransloaditClient {}
174
246
 
247
+ /**
248
+ * @deprecated Prefer `new Transloadit(...)` or `makeTransloaditAPI(...)`.
249
+ */
175
250
  export function createTransloadit(
176
251
  component: TransloaditComponent,
177
252
  config?: Partial<TransloaditConfig>,
@@ -183,10 +258,10 @@ export function makeTransloaditAPI(
183
258
  component: TransloaditComponent,
184
259
  config?: Partial<TransloaditConfig>,
185
260
  ) {
186
- const resolvedConfig: TransloaditConfig = {
261
+ const resolveConfig = (): TransloaditConfig => ({
187
262
  authKey: config?.authKey ?? requireEnv(["TRANSLOADIT_KEY"]),
188
263
  authSecret: config?.authSecret ?? requireEnv(["TRANSLOADIT_SECRET"]),
189
- };
264
+ });
190
265
 
191
266
  return {
192
267
  createAssembly: actionGeneric({
@@ -196,6 +271,7 @@ export function makeTransloaditAPI(
196
271
  data: v.any(),
197
272
  }),
198
273
  handler: async (ctx, args) => {
274
+ const resolvedConfig = resolveConfig();
199
275
  return ctx.runAction(component.lib.createAssembly, {
200
276
  ...args,
201
277
  config: resolvedConfig,
@@ -207,13 +283,15 @@ export function makeTransloaditAPI(
207
283
  payload: v.any(),
208
284
  rawBody: v.optional(v.string()),
209
285
  signature: v.optional(v.string()),
210
- verifySignature: v.optional(v.boolean()),
211
286
  },
212
287
  returns: v.object({
213
288
  assemblyId: v.string(),
214
289
  resultCount: v.number(),
290
+ ok: v.optional(v.string()),
291
+ status: v.optional(v.string()),
215
292
  }),
216
293
  handler: async (ctx, args) => {
294
+ const resolvedConfig = resolveConfig();
217
295
  return ctx.runAction(component.lib.handleWebhook, {
218
296
  ...args,
219
297
  config: { authSecret: resolvedConfig.authSecret },
@@ -225,13 +303,13 @@ export function makeTransloaditAPI(
225
303
  payload: v.any(),
226
304
  rawBody: v.optional(v.string()),
227
305
  signature: v.optional(v.string()),
228
- verifySignature: v.optional(v.boolean()),
229
306
  },
230
307
  returns: v.object({
231
308
  assemblyId: v.string(),
232
309
  queued: v.boolean(),
233
310
  }),
234
311
  handler: async (ctx, args) => {
312
+ const resolvedConfig = resolveConfig();
235
313
  return ctx.runAction(component.lib.queueWebhook, {
236
314
  ...args,
237
315
  config: { authSecret: resolvedConfig.authSecret },
@@ -247,6 +325,7 @@ export function makeTransloaditAPI(
247
325
  status: v.optional(v.string()),
248
326
  }),
249
327
  handler: async (ctx, args) => {
328
+ const resolvedConfig = resolveConfig();
250
329
  return ctx.runAction(component.lib.refreshAssembly, {
251
330
  ...args,
252
331
  config: resolvedConfig,
@@ -282,6 +361,29 @@ export function makeTransloaditAPI(
282
361
  return ctx.runQuery(component.lib.listResults, args);
283
362
  },
284
363
  }),
364
+ listAlbumResults: queryGeneric({
365
+ args: {
366
+ album: v.string(),
367
+ limit: v.optional(v.number()),
368
+ },
369
+ returns: v.array(vAssemblyResultResponse),
370
+ handler: async (ctx, args) => {
371
+ return ctx.runQuery(component.lib.listAlbumResults, args);
372
+ },
373
+ }),
374
+ purgeAlbum: mutationGeneric({
375
+ args: {
376
+ album: v.string(),
377
+ deleteAssemblies: v.optional(v.boolean()),
378
+ },
379
+ returns: v.object({
380
+ deletedResults: v.number(),
381
+ deletedAssemblies: v.number(),
382
+ }),
383
+ handler: async (ctx, args) => {
384
+ return ctx.runMutation(component.lib.purgeAlbum, args);
385
+ },
386
+ }),
285
387
  storeAssemblyMetadata: mutationGeneric({
286
388
  args: {
287
389
  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";
@@ -142,6 +142,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
142
142
  Array<any>,
143
143
  Name
144
144
  >;
145
+ listAlbumResults: FunctionReference<
146
+ "query",
147
+ "internal",
148
+ { album: string; limit?: number },
149
+ Array<any>,
150
+ Name
151
+ >;
152
+ purgeAlbum: FunctionReference<
153
+ "mutation",
154
+ "internal",
155
+ { album: string; deleteAssemblies?: boolean },
156
+ { deletedResults: number; deletedAssemblies: number },
157
+ Name
158
+ >;
145
159
  storeAssemblyMetadata: FunctionReference<
146
160
  "mutation",
147
161
  "internal",
@@ -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 = {