@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.
- package/README.md +114 -134
- package/dist/client/index.d.ts +24 -13
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +14 -3
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- 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 +7 -9
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +74 -18
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +4 -6
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +0 -7
- 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 +4 -4
- 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 +36 -6
- package/src/client/index.ts +73 -7
- package/src/component/_generated/api.ts +2 -2
- 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 +170 -4
- package/src/component/lib.ts +113 -25
- package/src/component/schema.ts +0 -10
- 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
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { createHmac } from "node:crypto";
|
|
4
4
|
import { convexTest } from "convex-test";
|
|
5
5
|
import { describe, expect, test, vi } from "vitest";
|
|
6
|
-
import { api } from "./_generated/api.
|
|
7
|
-
import schema from "./schema.
|
|
8
|
-
import { modules } from "./setup.test.
|
|
6
|
+
import { api } from "./_generated/api.ts";
|
|
7
|
+
import schema from "./schema.ts";
|
|
8
|
+
import { modules } from "./setup.test.ts";
|
|
9
9
|
|
|
10
10
|
process.env.TRANSLOADIT_KEY = "test-key";
|
|
11
11
|
process.env.TRANSLOADIT_SECRET = "test-secret";
|
|
@@ -40,7 +40,6 @@ describe("Transloadit component lib", () => {
|
|
|
40
40
|
payload,
|
|
41
41
|
rawBody,
|
|
42
42
|
signature: `sha1:${signature}`,
|
|
43
|
-
verifySignature: true,
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
expect(result.assemblyId).toBe("asm_123");
|
|
@@ -61,6 +60,173 @@ describe("Transloadit component lib", () => {
|
|
|
61
60
|
expect(results[0]?.stepName).toBe("resized");
|
|
62
61
|
});
|
|
63
62
|
|
|
63
|
+
test("handleWebhook stores url when ssl_url missing", async () => {
|
|
64
|
+
const t = convexTest(schema, modules);
|
|
65
|
+
|
|
66
|
+
const payload = {
|
|
67
|
+
assembly_id: "asm_url",
|
|
68
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
69
|
+
results: {
|
|
70
|
+
stored: [
|
|
71
|
+
{
|
|
72
|
+
id: "file_3",
|
|
73
|
+
url: "https://example.com/file-3.jpg",
|
|
74
|
+
name: "file-3.jpg",
|
|
75
|
+
size: 42,
|
|
76
|
+
mime: "image/jpeg",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const rawBody = JSON.stringify(payload);
|
|
83
|
+
const signature = createHmac("sha1", "test-secret")
|
|
84
|
+
.update(rawBody)
|
|
85
|
+
.digest("hex");
|
|
86
|
+
|
|
87
|
+
await t.action(api.lib.handleWebhook, {
|
|
88
|
+
payload,
|
|
89
|
+
rawBody,
|
|
90
|
+
signature: `sha1:${signature}`,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const results = await t.query(api.lib.listResults, {
|
|
94
|
+
assemblyId: "asm_url",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(results).toHaveLength(1);
|
|
98
|
+
expect(results[0]?.sslUrl).toBe("https://example.com/file-3.jpg");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("listResults exposes expected fields for common robot outputs", async () => {
|
|
102
|
+
const t = convexTest(schema, modules);
|
|
103
|
+
|
|
104
|
+
const payload = {
|
|
105
|
+
assembly_id: "asm_schema",
|
|
106
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
107
|
+
results: {
|
|
108
|
+
images_resized: [
|
|
109
|
+
{
|
|
110
|
+
id: "img_1",
|
|
111
|
+
ssl_url: "https://example.com/img.jpg",
|
|
112
|
+
name: "img.jpg",
|
|
113
|
+
mime: "image/jpeg",
|
|
114
|
+
width: 1600,
|
|
115
|
+
height: 1200,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
videos_encoded: [
|
|
119
|
+
{
|
|
120
|
+
id: "vid_1",
|
|
121
|
+
ssl_url: "https://example.com/vid.mp4",
|
|
122
|
+
name: "vid.mp4",
|
|
123
|
+
mime: "video/mp4",
|
|
124
|
+
duration: 12.5,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
videos_thumbs_output: [
|
|
128
|
+
{
|
|
129
|
+
id: "thumb_1",
|
|
130
|
+
ssl_url: "https://example.com/thumb.jpg",
|
|
131
|
+
name: "thumb.jpg",
|
|
132
|
+
mime: "image/jpeg",
|
|
133
|
+
original_id: "vid_1",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const rawBody = JSON.stringify(payload);
|
|
140
|
+
const signature = createHmac("sha1", "test-secret")
|
|
141
|
+
.update(rawBody)
|
|
142
|
+
.digest("hex");
|
|
143
|
+
|
|
144
|
+
await t.action(api.lib.handleWebhook, {
|
|
145
|
+
payload,
|
|
146
|
+
rawBody,
|
|
147
|
+
signature: `sha1:${signature}`,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const results = await t.query(api.lib.listResults, {
|
|
151
|
+
assemblyId: "asm_schema",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(results).toHaveLength(3);
|
|
155
|
+
|
|
156
|
+
const byStep = new Map(results.map((result) => [result.stepName, result]));
|
|
157
|
+
const image = byStep.get("images_resized");
|
|
158
|
+
const video = byStep.get("videos_encoded");
|
|
159
|
+
const thumb = byStep.get("videos_thumbs_output");
|
|
160
|
+
|
|
161
|
+
expect(image?.sslUrl).toBe("https://example.com/img.jpg");
|
|
162
|
+
expect(image?.mime).toBe("image/jpeg");
|
|
163
|
+
expect(image?.raw?.width).toBe(1600);
|
|
164
|
+
expect(image?.raw?.height).toBe(1200);
|
|
165
|
+
|
|
166
|
+
expect(video?.sslUrl).toBe("https://example.com/vid.mp4");
|
|
167
|
+
expect(video?.mime).toBe("video/mp4");
|
|
168
|
+
expect(video?.raw?.duration).toBe(12.5);
|
|
169
|
+
|
|
170
|
+
expect(thumb?.sslUrl).toBe("https://example.com/thumb.jpg");
|
|
171
|
+
expect(thumb?.raw?.original_id).toBe("vid_1");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("handleWebhook requires rawBody when verifying signature", async () => {
|
|
175
|
+
const t = convexTest(schema, modules);
|
|
176
|
+
const payload = { assembly_id: "asm_missing" };
|
|
177
|
+
const signature = createHmac("sha1", "test-secret")
|
|
178
|
+
.update(JSON.stringify(payload))
|
|
179
|
+
.digest("hex");
|
|
180
|
+
|
|
181
|
+
await expect(
|
|
182
|
+
t.action(api.lib.handleWebhook, {
|
|
183
|
+
payload,
|
|
184
|
+
signature: `sha1:${signature}`,
|
|
185
|
+
}),
|
|
186
|
+
).rejects.toThrow("Missing rawBody for webhook verification");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("handleWebhook can skip verification when configured", async () => {
|
|
190
|
+
const t = convexTest(schema, modules);
|
|
191
|
+
const payload = {
|
|
192
|
+
assembly_id: "asm_skip",
|
|
193
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
194
|
+
results: {
|
|
195
|
+
resized: [
|
|
196
|
+
{
|
|
197
|
+
id: "file_skip",
|
|
198
|
+
ssl_url: "https://example.com/skip.jpg",
|
|
199
|
+
name: "skip.jpg",
|
|
200
|
+
size: 123,
|
|
201
|
+
mime: "image/jpeg",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = await t.action(api.lib.handleWebhook, {
|
|
208
|
+
payload,
|
|
209
|
+
verifySignature: false,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(result.assemblyId).toBe("asm_skip");
|
|
213
|
+
expect(result.resultCount).toBe(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("queueWebhook rejects invalid signature", async () => {
|
|
217
|
+
const t = convexTest(schema, modules);
|
|
218
|
+
const payload = { assembly_id: "asm_bad" };
|
|
219
|
+
const rawBody = JSON.stringify(payload);
|
|
220
|
+
|
|
221
|
+
await expect(
|
|
222
|
+
t.action(api.lib.queueWebhook, {
|
|
223
|
+
payload,
|
|
224
|
+
rawBody,
|
|
225
|
+
signature: "sha1:bad",
|
|
226
|
+
}),
|
|
227
|
+
).rejects.toThrow("Invalid Transloadit webhook signature");
|
|
228
|
+
});
|
|
229
|
+
|
|
64
230
|
test("refreshAssembly fetches status and stores results", async () => {
|
|
65
231
|
const t = convexTest(schema, modules);
|
|
66
232
|
|
package/src/component/lib.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
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 { anyApi, type FunctionReference } from "convex/server";
|
|
4
4
|
import { type Infer, v } from "convex/values";
|
|
5
|
+
import { parseAssemblyStatus } from "../shared/assemblyUrls.ts";
|
|
6
|
+
import { transloaditError } from "../shared/errors.ts";
|
|
7
|
+
import { getResultUrl } from "../shared/resultUtils.ts";
|
|
5
8
|
import {
|
|
6
9
|
action,
|
|
7
10
|
internalAction,
|
|
8
11
|
internalMutation,
|
|
9
12
|
mutation,
|
|
10
13
|
query,
|
|
11
|
-
} from "./_generated/server.
|
|
14
|
+
} from "./_generated/server.ts";
|
|
12
15
|
import {
|
|
13
16
|
buildTransloaditParams,
|
|
14
17
|
flattenResults,
|
|
15
18
|
signTransloaditParams,
|
|
16
19
|
verifyWebhookSignature,
|
|
17
|
-
} from "./apiUtils.
|
|
20
|
+
} from "./apiUtils.ts";
|
|
18
21
|
|
|
19
22
|
const TRANSLOADIT_ASSEMBLY_URL = "https://api2.transloadit.com/assemblies";
|
|
20
23
|
|
|
@@ -56,6 +59,26 @@ const resolveAssemblyId = (payload: AssemblyStatus): string => {
|
|
|
56
59
|
return "";
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
const parseAssemblyPayload = (payload: unknown): AssemblyStatus => {
|
|
63
|
+
const parsed = parseAssemblyStatus(payload);
|
|
64
|
+
if (!parsed) {
|
|
65
|
+
throw transloaditError("payload", "Invalid Transloadit payload");
|
|
66
|
+
}
|
|
67
|
+
return parsed;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const resolveWebhookRawBody = (args: {
|
|
71
|
+
payload: unknown;
|
|
72
|
+
rawBody?: string;
|
|
73
|
+
verifySignature?: boolean;
|
|
74
|
+
}) => {
|
|
75
|
+
if (typeof args.rawBody === "string") return args.rawBody;
|
|
76
|
+
if (args.verifySignature === false) {
|
|
77
|
+
return JSON.stringify(args.payload ?? {});
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
|
|
59
82
|
const buildSignedAssemblyUrl = async (
|
|
60
83
|
assemblyId: string,
|
|
61
84
|
authKey: string,
|
|
@@ -75,12 +98,12 @@ const buildSignedAssemblyUrl = async (
|
|
|
75
98
|
};
|
|
76
99
|
|
|
77
100
|
const applyAssemblyStatus = async (
|
|
78
|
-
ctx: Pick<import("./_generated/server.
|
|
101
|
+
ctx: Pick<import("./_generated/server.ts").FunctionCtx, "runMutation">,
|
|
79
102
|
payload: AssemblyStatus,
|
|
80
103
|
) => {
|
|
81
104
|
const assemblyId = resolveAssemblyId(payload);
|
|
82
105
|
if (!assemblyId) {
|
|
83
|
-
throw
|
|
106
|
+
throw transloaditError("webhook", "Webhook payload missing assembly_id");
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
const results = flattenResults(payload.results ?? undefined);
|
|
@@ -185,6 +208,9 @@ export const upsertAssembly = internalMutation({
|
|
|
185
208
|
},
|
|
186
209
|
returns: v.id("assemblies"),
|
|
187
210
|
handler: async (ctx, args) => {
|
|
211
|
+
// Note: we persist full `raw` + `results` for debugging/fidelity. Large
|
|
212
|
+
// assemblies can hit Convex document size limits; trim or externalize
|
|
213
|
+
// payloads if this becomes an issue for your workload.
|
|
188
214
|
const existing = await ctx.db
|
|
189
215
|
.query("assemblies")
|
|
190
216
|
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", args.assemblyId))
|
|
@@ -244,6 +270,10 @@ export const replaceResultsForAssembly = internalMutation({
|
|
|
244
270
|
},
|
|
245
271
|
returns: v.null(),
|
|
246
272
|
handler: async (ctx, args) => {
|
|
273
|
+
// We store raw result payloads for fidelity. For very large assemblies,
|
|
274
|
+
// consider trimming or externalizing these fields to avoid size limits.
|
|
275
|
+
// This mutation replaces all results in one transaction; extremely large
|
|
276
|
+
// result sets may need batching or external storage to avoid Convex limits.
|
|
247
277
|
const existingResults = await ctx.db
|
|
248
278
|
.query("results")
|
|
249
279
|
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", args.assemblyId))
|
|
@@ -256,11 +286,12 @@ export const replaceResultsForAssembly = internalMutation({
|
|
|
256
286
|
const now = Date.now();
|
|
257
287
|
for (const entry of args.results) {
|
|
258
288
|
const raw = entry.result as Record<string, unknown>;
|
|
289
|
+
const sslUrl = getResultUrl(entry.result);
|
|
259
290
|
await ctx.db.insert("results", {
|
|
260
291
|
assemblyId: args.assemblyId,
|
|
261
292
|
stepName: entry.stepName,
|
|
262
293
|
resultId: typeof raw.id === "string" ? raw.id : undefined,
|
|
263
|
-
sslUrl
|
|
294
|
+
sslUrl,
|
|
264
295
|
name: typeof raw.name === "string" ? raw.name : undefined,
|
|
265
296
|
size: typeof raw.size === "number" ? raw.size : undefined,
|
|
266
297
|
mime: typeof raw.mime === "string" ? raw.mime : undefined,
|
|
@@ -318,8 +349,9 @@ export const createAssembly = action({
|
|
|
318
349
|
|
|
319
350
|
const data = (await response.json()) as Record<string, unknown>;
|
|
320
351
|
if (!response.ok) {
|
|
321
|
-
throw
|
|
322
|
-
|
|
352
|
+
throw transloaditError(
|
|
353
|
+
"createAssembly",
|
|
354
|
+
`HTTP ${response.status}: ${JSON.stringify(data)}`,
|
|
323
355
|
);
|
|
324
356
|
}
|
|
325
357
|
|
|
@@ -331,7 +363,10 @@ export const createAssembly = action({
|
|
|
331
363
|
: "";
|
|
332
364
|
|
|
333
365
|
if (!assemblyId) {
|
|
334
|
-
throw
|
|
366
|
+
throw transloaditError(
|
|
367
|
+
"createAssembly",
|
|
368
|
+
"Transloadit response missing assembly_id",
|
|
369
|
+
);
|
|
335
370
|
}
|
|
336
371
|
|
|
337
372
|
await ctx.runMutation(internal.lib.upsertAssembly, {
|
|
@@ -362,6 +397,13 @@ const vWebhookArgs = {
|
|
|
362
397
|
authSecret: v.optional(v.string()),
|
|
363
398
|
};
|
|
364
399
|
|
|
400
|
+
const vPublicWebhookArgs = {
|
|
401
|
+
payload: v.any(),
|
|
402
|
+
rawBody: v.optional(v.string()),
|
|
403
|
+
signature: v.optional(v.string()),
|
|
404
|
+
verifySignature: v.optional(v.boolean()),
|
|
405
|
+
};
|
|
406
|
+
|
|
365
407
|
export const processWebhook = internalAction({
|
|
366
408
|
args: vWebhookArgs,
|
|
367
409
|
returns: v.object({
|
|
@@ -371,13 +413,22 @@ export const processWebhook = internalAction({
|
|
|
371
413
|
status: v.optional(v.string()),
|
|
372
414
|
}),
|
|
373
415
|
handler: async (ctx, args) => {
|
|
374
|
-
const rawBody =
|
|
416
|
+
const rawBody = resolveWebhookRawBody(args);
|
|
375
417
|
const shouldVerify = args.verifySignature ?? true;
|
|
376
418
|
const authSecret = args.authSecret ?? process.env.TRANSLOADIT_SECRET;
|
|
377
419
|
|
|
378
420
|
if (shouldVerify) {
|
|
421
|
+
if (!rawBody) {
|
|
422
|
+
throw transloaditError(
|
|
423
|
+
"webhook",
|
|
424
|
+
"Missing rawBody for webhook verification",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
379
427
|
if (!authSecret) {
|
|
380
|
-
throw
|
|
428
|
+
throw transloaditError(
|
|
429
|
+
"webhook",
|
|
430
|
+
"Missing TRANSLOADIT_SECRET for webhook validation",
|
|
431
|
+
);
|
|
381
432
|
}
|
|
382
433
|
const verified = await verifyWebhookSignature({
|
|
383
434
|
rawBody,
|
|
@@ -385,17 +436,21 @@ export const processWebhook = internalAction({
|
|
|
385
436
|
authSecret,
|
|
386
437
|
});
|
|
387
438
|
if (!verified) {
|
|
388
|
-
throw
|
|
439
|
+
throw transloaditError(
|
|
440
|
+
"webhook",
|
|
441
|
+
"Invalid Transloadit webhook signature",
|
|
442
|
+
);
|
|
389
443
|
}
|
|
390
444
|
}
|
|
391
445
|
|
|
392
|
-
|
|
446
|
+
const parsed = parseAssemblyPayload(args.payload);
|
|
447
|
+
return applyAssemblyStatus(ctx, parsed);
|
|
393
448
|
},
|
|
394
449
|
});
|
|
395
450
|
|
|
396
451
|
export const handleWebhook = action({
|
|
397
452
|
args: {
|
|
398
|
-
...
|
|
453
|
+
...vPublicWebhookArgs,
|
|
399
454
|
config: v.optional(
|
|
400
455
|
v.object({
|
|
401
456
|
authSecret: v.string(),
|
|
@@ -409,11 +464,12 @@ export const handleWebhook = action({
|
|
|
409
464
|
status: v.optional(v.string()),
|
|
410
465
|
}),
|
|
411
466
|
handler: async (ctx, args) => {
|
|
467
|
+
const verifySignature = args.verifySignature ?? true;
|
|
412
468
|
return ctx.runAction(internal.lib.processWebhook, {
|
|
413
469
|
payload: args.payload,
|
|
414
470
|
rawBody: args.rawBody,
|
|
415
471
|
signature: args.signature,
|
|
416
|
-
verifySignature
|
|
472
|
+
verifySignature,
|
|
417
473
|
authSecret: args.config?.authSecret,
|
|
418
474
|
});
|
|
419
475
|
},
|
|
@@ -421,7 +477,7 @@ export const handleWebhook = action({
|
|
|
421
477
|
|
|
422
478
|
export const queueWebhook = action({
|
|
423
479
|
args: {
|
|
424
|
-
...
|
|
480
|
+
...vPublicWebhookArgs,
|
|
425
481
|
config: v.optional(
|
|
426
482
|
v.object({
|
|
427
483
|
authSecret: v.string(),
|
|
@@ -433,17 +489,48 @@ export const queueWebhook = action({
|
|
|
433
489
|
queued: v.boolean(),
|
|
434
490
|
}),
|
|
435
491
|
handler: async (ctx, args) => {
|
|
436
|
-
const
|
|
437
|
-
const
|
|
492
|
+
const rawBody = resolveWebhookRawBody(args);
|
|
493
|
+
const shouldVerify = args.verifySignature ?? true;
|
|
494
|
+
const authSecret =
|
|
495
|
+
args.config?.authSecret ?? process.env.TRANSLOADIT_SECRET;
|
|
496
|
+
|
|
497
|
+
if (shouldVerify) {
|
|
498
|
+
if (!rawBody) {
|
|
499
|
+
throw transloaditError(
|
|
500
|
+
"webhook",
|
|
501
|
+
"Missing rawBody for webhook verification",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (!authSecret) {
|
|
505
|
+
throw transloaditError(
|
|
506
|
+
"webhook",
|
|
507
|
+
"Missing TRANSLOADIT_SECRET for webhook validation",
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const verified = await verifyWebhookSignature({
|
|
511
|
+
rawBody,
|
|
512
|
+
signatureHeader: args.signature,
|
|
513
|
+
authSecret,
|
|
514
|
+
});
|
|
515
|
+
if (!verified) {
|
|
516
|
+
throw transloaditError(
|
|
517
|
+
"webhook",
|
|
518
|
+
"Invalid Transloadit webhook signature",
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const parsed = parseAssemblyPayload(args.payload);
|
|
524
|
+
const assemblyId = resolveAssemblyId(parsed);
|
|
438
525
|
if (!assemblyId) {
|
|
439
|
-
throw
|
|
526
|
+
throw transloaditError("webhook", "Webhook payload missing assembly_id");
|
|
440
527
|
}
|
|
441
528
|
|
|
442
529
|
await ctx.scheduler.runAfter(0, internal.lib.processWebhook, {
|
|
443
|
-
payload:
|
|
530
|
+
payload: parsed,
|
|
444
531
|
rawBody: args.rawBody,
|
|
445
532
|
signature: args.signature,
|
|
446
|
-
verifySignature:
|
|
533
|
+
verifySignature: true,
|
|
447
534
|
authSecret: args.config?.authSecret,
|
|
448
535
|
});
|
|
449
536
|
|
|
@@ -478,10 +565,11 @@ export const refreshAssembly = action({
|
|
|
478
565
|
: `${TRANSLOADIT_ASSEMBLY_URL}/${assemblyId}`;
|
|
479
566
|
|
|
480
567
|
const response = await fetch(url);
|
|
481
|
-
const payload = (await response.json())
|
|
568
|
+
const payload = parseAssemblyPayload(await response.json());
|
|
482
569
|
if (!response.ok) {
|
|
483
|
-
throw
|
|
484
|
-
|
|
570
|
+
throw transloaditError(
|
|
571
|
+
"status",
|
|
572
|
+
`HTTP ${response.status}: ${JSON.stringify(payload)}`,
|
|
485
573
|
);
|
|
486
574
|
}
|
|
487
575
|
|
package/src/component/schema.ts
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
|
|
4
|
-
export const assemblyStatusValues = [
|
|
5
|
-
"ASSEMBLY_UPLOADING",
|
|
6
|
-
"ASSEMBLY_EXECUTING",
|
|
7
|
-
"ASSEMBLY_COMPLETED",
|
|
8
|
-
"ASSEMBLY_CANCELED",
|
|
9
|
-
"ASSEMBLY_FAILED",
|
|
10
|
-
] as const;
|
|
11
|
-
|
|
12
|
-
export type AssemblyStatus = (typeof assemblyStatusValues)[number];
|
|
13
|
-
|
|
14
4
|
export default defineSchema({
|
|
15
5
|
assemblies: defineTable({
|
|
16
6
|
assemblyId: v.string(),
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
type ConsoleSink = Pick<Console, "log" | "info" | "warn" | "error">;
|
|
2
|
+
|
|
3
|
+
export type DebugLogger = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
log: (message: string, meta?: Record<string, unknown>) => void;
|
|
6
|
+
info: (message: string, meta?: Record<string, unknown>) => void;
|
|
7
|
+
warn: (message: string, meta?: Record<string, unknown>) => void;
|
|
8
|
+
error: (message: string, meta?: Record<string, unknown>) => void;
|
|
9
|
+
event: (name: string, meta?: Record<string, unknown>) => void;
|
|
10
|
+
child: (namespace: string) => DebugLogger;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DebugLoggerOptions = {
|
|
14
|
+
namespace?: string;
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
sink?: ConsoleSink;
|
|
17
|
+
clock?: () => Date;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const resolveEnv = (): Record<string, string | undefined> => {
|
|
21
|
+
if (typeof process !== "undefined" && process.env) {
|
|
22
|
+
return process.env;
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const parseEnabled = (value: string | undefined) => {
|
|
28
|
+
if (!value) return false;
|
|
29
|
+
const normalized = value.trim().toLowerCase();
|
|
30
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const formatLine = (
|
|
34
|
+
timestamp: string,
|
|
35
|
+
prefix: string,
|
|
36
|
+
message: string,
|
|
37
|
+
meta?: Record<string, unknown>,
|
|
38
|
+
) => {
|
|
39
|
+
if (!meta || Object.keys(meta).length === 0) {
|
|
40
|
+
return `${timestamp} ${prefix} ${message}`;
|
|
41
|
+
}
|
|
42
|
+
return `${timestamp} ${prefix} ${message} ${JSON.stringify(meta)}`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const createDebugLogger = (
|
|
46
|
+
options: DebugLoggerOptions = {},
|
|
47
|
+
): DebugLogger => {
|
|
48
|
+
const env = resolveEnv();
|
|
49
|
+
const enabled =
|
|
50
|
+
options.enabled ??
|
|
51
|
+
(parseEnabled(env.TRANSLOADIT_DEBUG) ||
|
|
52
|
+
parseEnabled(env.CONVEX_TRANSLOADIT_DEBUG));
|
|
53
|
+
const namespace = options.namespace ?? "convex";
|
|
54
|
+
const prefix = `[transloadit:${namespace}]`;
|
|
55
|
+
const sink: ConsoleSink = options.sink ?? console;
|
|
56
|
+
const clock = options.clock ?? (() => new Date());
|
|
57
|
+
|
|
58
|
+
const emit = (
|
|
59
|
+
level: "log" | "info" | "warn" | "error",
|
|
60
|
+
message: string,
|
|
61
|
+
meta?: Record<string, unknown>,
|
|
62
|
+
) => {
|
|
63
|
+
if (!enabled) return;
|
|
64
|
+
const line = formatLine(clock().toISOString(), prefix, message, meta);
|
|
65
|
+
sink[level](line);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const logger: DebugLogger = {
|
|
69
|
+
enabled,
|
|
70
|
+
log: (message, meta) => emit("log", message, meta),
|
|
71
|
+
info: (message, meta) => emit("info", message, meta),
|
|
72
|
+
warn: (message, meta) => emit("warn", message, meta),
|
|
73
|
+
error: (message, meta) => emit("error", message, meta),
|
|
74
|
+
event: (name, meta) => emit("info", `event:${name}`, meta),
|
|
75
|
+
child: (childNamespace) =>
|
|
76
|
+
createDebugLogger({
|
|
77
|
+
...options,
|
|
78
|
+
namespace: `${namespace}:${childNamespace}`,
|
|
79
|
+
enabled,
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return logger;
|
|
84
|
+
};
|