@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.
- package/README.md +154 -122
- package/dist/client/index.d.ts +54 -13
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +48 -5
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/component.d.ts +11 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/dataModel.d.ts +1 -1
- package/dist/component/_generated/server.d.ts +1 -1
- package/dist/component/apiUtils.d.ts +26 -6
- package/dist/component/apiUtils.d.ts.map +1 -1
- package/dist/component/apiUtils.js +48 -38
- package/dist/component/apiUtils.js.map +1 -1
- package/dist/component/lib.d.ts +37 -8
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +145 -18
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +9 -6
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +4 -8
- package/dist/component/schema.js.map +1 -1
- package/dist/debug/index.d.ts +19 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/index.js +49 -0
- package/dist/debug/index.js.map +1 -0
- package/dist/react/index.d.ts +201 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +674 -94
- package/dist/react/index.js.map +1 -1
- package/dist/shared/assemblyUrls.d.ts +10 -0
- package/dist/shared/assemblyUrls.d.ts.map +1 -0
- package/dist/shared/assemblyUrls.js +26 -0
- package/dist/shared/assemblyUrls.js.map +1 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +10 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/pollAssembly.d.ts +12 -0
- package/dist/shared/pollAssembly.d.ts.map +1 -0
- package/dist/shared/pollAssembly.js +50 -0
- package/dist/shared/pollAssembly.js.map +1 -0
- package/dist/shared/resultTypes.d.ts +37 -0
- package/dist/shared/resultTypes.d.ts.map +1 -0
- package/dist/shared/resultTypes.js +2 -0
- package/dist/shared/resultTypes.js.map +1 -0
- package/dist/shared/resultUtils.d.ts +4 -0
- package/dist/shared/resultUtils.d.ts.map +1 -0
- package/dist/shared/resultUtils.js +69 -0
- package/dist/shared/resultUtils.js.map +1 -0
- package/dist/shared/tusUpload.d.ts +13 -0
- package/dist/shared/tusUpload.d.ts.map +1 -0
- package/dist/shared/tusUpload.js +32 -0
- package/dist/shared/tusUpload.js.map +1 -0
- package/dist/test/index.d.ts +9 -4
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/nodeModules.d.ts +2 -0
- package/dist/test/nodeModules.d.ts.map +1 -0
- package/dist/test/nodeModules.js +19 -0
- package/dist/test/nodeModules.js.map +1 -0
- package/package.json +40 -7
- package/src/client/index.ts +111 -9
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +14 -0
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +1 -1
- package/src/component/apiUtils.test.ts +166 -2
- package/src/component/apiUtils.ts +96 -64
- package/src/component/lib.test.ts +213 -4
- package/src/component/lib.ts +192 -25
- package/src/component/schema.ts +4 -11
- package/src/debug/index.ts +84 -0
- package/src/react/index.test.tsx +340 -0
- package/src/react/index.tsx +1089 -179
- package/src/react/uploadWithTus.test.tsx +192 -0
- package/src/shared/assemblyUrls.test.ts +71 -0
- package/src/shared/assemblyUrls.ts +59 -0
- package/src/shared/errors.ts +23 -0
- package/src/shared/pollAssembly.ts +65 -0
- package/src/shared/resultTypes.ts +44 -0
- package/src/shared/resultUtils.test.ts +29 -0
- package/src/shared/resultUtils.ts +71 -0
- package/src/shared/tusUpload.ts +59 -0
- package/src/test/index.ts +1 -1
- package/src/test/nodeModules.ts +19 -0
package/src/client/index.ts
CHANGED
|
@@ -1,11 +1,71 @@
|
|
|
1
|
-
import type { AssemblyStatus } from "@transloadit/
|
|
2
|
-
import type { AssemblyInstructionsInput } from "@transloadit/
|
|
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.
|
|
6
|
-
import type { RunActionCtx, RunMutationCtx, RunQueryCtx } from "./types.
|
|
5
|
+
import type { ComponentApi } from "../component/_generated/component.ts";
|
|
6
|
+
import type { RunActionCtx, RunMutationCtx, RunQueryCtx } from "./types.ts";
|
|
7
7
|
|
|
8
|
-
export {
|
|
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
|
|
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.
|
|
12
|
-
import type * as lib from "../lib.
|
|
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",
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
internalMutationGeneric,
|
|
29
29
|
internalQueryGeneric,
|
|
30
30
|
} from "convex/server";
|
|
31
|
-
import type { DataModel } from "./dataModel.
|
|
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.
|
|
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
|
|
2
|
-
import type {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
142
|
-
|
|
135
|
+
return { ...parsed, verified };
|
|
136
|
+
}
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
export type WebhookActionArgs = {
|
|
158
|
+
payload: unknown;
|
|
159
|
+
rawBody?: string;
|
|
160
|
+
signature?: string;
|
|
161
|
+
};
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 = {
|