@transloadit/convex 0.0.2 → 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 +184 -121
- package/dist/client/index.d.ts +100 -60
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +69 -31
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/component.d.ts +35 -15
- 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 +36 -7
- package/dist/component/apiUtils.d.ts.map +1 -1
- package/dist/component/apiUtils.js +60 -40
- package/dist/component/apiUtils.js.map +1 -1
- package/dist/component/lib.d.ts +71 -49
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +206 -73
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +11 -13
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +3 -10
- 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 +213 -17
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +726 -105
- 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 +65 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +8 -0
- package/dist/test/index.js.map +1 -0
- 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 +53 -15
- package/src/client/index.ts +141 -38
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +44 -13
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +1 -1
- package/src/component/apiUtils.test.ts +195 -2
- package/src/component/apiUtils.ts +124 -66
- package/src/component/lib.test.ts +243 -7
- package/src/component/lib.ts +302 -90
- package/src/component/schema.ts +3 -13
- package/src/debug/index.ts +84 -0
- package/src/react/index.test.tsx +340 -0
- package/src/react/index.tsx +1105 -152
- 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 +10 -0
- package/src/test/nodeModules.ts +19 -0
package/src/component/lib.ts
CHANGED
|
@@ -1,20 +1,141 @@
|
|
|
1
|
+
import type { AssemblyStatus } from "@transloadit/zod/v3/assemblyStatus";
|
|
2
|
+
import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template";
|
|
3
|
+
import { anyApi, type FunctionReference } from "convex/server";
|
|
1
4
|
import { type Infer, v } from "convex/values";
|
|
2
|
-
import {
|
|
5
|
+
import { parseAssemblyStatus } from "../shared/assemblyUrls.ts";
|
|
6
|
+
import { transloaditError } from "../shared/errors.ts";
|
|
7
|
+
import { getResultUrl } from "../shared/resultUtils.ts";
|
|
3
8
|
import {
|
|
4
9
|
action,
|
|
10
|
+
internalAction,
|
|
5
11
|
internalMutation,
|
|
6
12
|
mutation,
|
|
7
13
|
query,
|
|
8
|
-
} from "./_generated/server.
|
|
14
|
+
} from "./_generated/server.ts";
|
|
9
15
|
import {
|
|
10
16
|
buildTransloaditParams,
|
|
11
17
|
flattenResults,
|
|
12
18
|
signTransloaditParams,
|
|
13
19
|
verifyWebhookSignature,
|
|
14
|
-
} from "./apiUtils.
|
|
20
|
+
} from "./apiUtils.ts";
|
|
15
21
|
|
|
16
22
|
const TRANSLOADIT_ASSEMBLY_URL = "https://api2.transloadit.com/assemblies";
|
|
17
23
|
|
|
24
|
+
type ProcessWebhookResult = {
|
|
25
|
+
assemblyId: string;
|
|
26
|
+
resultCount: number;
|
|
27
|
+
ok?: string;
|
|
28
|
+
status?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type InternalApi = {
|
|
32
|
+
lib: {
|
|
33
|
+
upsertAssembly: FunctionReference<
|
|
34
|
+
"mutation",
|
|
35
|
+
"internal",
|
|
36
|
+
Record<string, unknown>,
|
|
37
|
+
unknown
|
|
38
|
+
>;
|
|
39
|
+
replaceResultsForAssembly: FunctionReference<
|
|
40
|
+
"mutation",
|
|
41
|
+
"internal",
|
|
42
|
+
Record<string, unknown>,
|
|
43
|
+
unknown
|
|
44
|
+
>;
|
|
45
|
+
processWebhook: FunctionReference<
|
|
46
|
+
"action",
|
|
47
|
+
"internal",
|
|
48
|
+
Record<string, unknown>,
|
|
49
|
+
ProcessWebhookResult
|
|
50
|
+
>;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const internal = anyApi as unknown as InternalApi;
|
|
55
|
+
|
|
56
|
+
const resolveAssemblyId = (payload: AssemblyStatus): string => {
|
|
57
|
+
if (typeof payload.assembly_id === "string") return payload.assembly_id;
|
|
58
|
+
if (typeof payload.assemblyId === "string") return payload.assemblyId;
|
|
59
|
+
return "";
|
|
60
|
+
};
|
|
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
|
+
|
|
82
|
+
const buildSignedAssemblyUrl = async (
|
|
83
|
+
assemblyId: string,
|
|
84
|
+
authKey: string,
|
|
85
|
+
authSecret: string,
|
|
86
|
+
): Promise<string> => {
|
|
87
|
+
const params = JSON.stringify({
|
|
88
|
+
auth: {
|
|
89
|
+
key: authKey,
|
|
90
|
+
expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const signature = await signTransloaditParams(params, authSecret);
|
|
94
|
+
const url = new URL(`${TRANSLOADIT_ASSEMBLY_URL}/${assemblyId}`);
|
|
95
|
+
url.searchParams.set("signature", signature);
|
|
96
|
+
url.searchParams.set("params", params);
|
|
97
|
+
return url.toString();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const applyAssemblyStatus = async (
|
|
101
|
+
ctx: Pick<import("./_generated/server.ts").FunctionCtx, "runMutation">,
|
|
102
|
+
payload: AssemblyStatus,
|
|
103
|
+
) => {
|
|
104
|
+
const assemblyId = resolveAssemblyId(payload);
|
|
105
|
+
if (!assemblyId) {
|
|
106
|
+
throw transloaditError("webhook", "Webhook payload missing assembly_id");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const results = flattenResults(payload.results ?? undefined);
|
|
110
|
+
|
|
111
|
+
await ctx.runMutation(internal.lib.upsertAssembly, {
|
|
112
|
+
assemblyId,
|
|
113
|
+
status: typeof payload.ok === "string" ? payload.ok : undefined,
|
|
114
|
+
ok: typeof payload.ok === "string" ? payload.ok : undefined,
|
|
115
|
+
message: typeof payload.message === "string" ? payload.message : undefined,
|
|
116
|
+
templateId:
|
|
117
|
+
typeof payload.template_id === "string" ? payload.template_id : undefined,
|
|
118
|
+
notifyUrl:
|
|
119
|
+
typeof payload.notify_url === "string" ? payload.notify_url : undefined,
|
|
120
|
+
uploads: payload.uploads,
|
|
121
|
+
results: payload.results,
|
|
122
|
+
error: payload.error,
|
|
123
|
+
raw: payload,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await ctx.runMutation(internal.lib.replaceResultsForAssembly, {
|
|
127
|
+
assemblyId,
|
|
128
|
+
results,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
assemblyId,
|
|
133
|
+
resultCount: results.length,
|
|
134
|
+
ok: typeof payload.ok === "string" ? payload.ok : undefined,
|
|
135
|
+
status: typeof payload.ok === "string" ? payload.ok : undefined,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
18
139
|
export const vAssembly = v.object({
|
|
19
140
|
_id: v.id("assemblies"),
|
|
20
141
|
_creationTime: v.number(),
|
|
@@ -25,9 +146,9 @@ export const vAssembly = v.object({
|
|
|
25
146
|
templateId: v.optional(v.string()),
|
|
26
147
|
notifyUrl: v.optional(v.string()),
|
|
27
148
|
numExpectedUploadFiles: v.optional(v.number()),
|
|
28
|
-
fields: v.optional(v.any()),
|
|
29
|
-
uploads: v.optional(v.any()),
|
|
30
|
-
results: v.optional(v.any()),
|
|
149
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
150
|
+
uploads: v.optional(v.array(v.any())),
|
|
151
|
+
results: v.optional(v.record(v.string(), v.array(v.any()))),
|
|
31
152
|
error: v.optional(v.any()),
|
|
32
153
|
raw: v.optional(v.any()),
|
|
33
154
|
createdAt: v.number(),
|
|
@@ -60,12 +181,12 @@ export const vTransloaditConfig = v.object({
|
|
|
60
181
|
|
|
61
182
|
const vAssemblyBaseArgs = {
|
|
62
183
|
templateId: v.optional(v.string()),
|
|
63
|
-
steps: v.optional(v.any()),
|
|
64
|
-
fields: v.optional(v.any()),
|
|
184
|
+
steps: v.optional(v.record(v.string(), v.any())),
|
|
185
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
65
186
|
notifyUrl: v.optional(v.string()),
|
|
66
187
|
numExpectedUploadFiles: v.optional(v.number()),
|
|
67
188
|
expires: v.optional(v.string()),
|
|
68
|
-
additionalParams: v.optional(v.any()),
|
|
189
|
+
additionalParams: v.optional(v.record(v.string(), v.any())),
|
|
69
190
|
userId: v.optional(v.string()),
|
|
70
191
|
};
|
|
71
192
|
|
|
@@ -78,15 +199,18 @@ export const upsertAssembly = internalMutation({
|
|
|
78
199
|
templateId: v.optional(v.string()),
|
|
79
200
|
notifyUrl: v.optional(v.string()),
|
|
80
201
|
numExpectedUploadFiles: v.optional(v.number()),
|
|
81
|
-
fields: v.optional(v.any()),
|
|
82
|
-
uploads: v.optional(v.any()),
|
|
83
|
-
results: v.optional(v.any()),
|
|
202
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
203
|
+
uploads: v.optional(v.array(v.any())),
|
|
204
|
+
results: v.optional(v.record(v.string(), v.array(v.any()))),
|
|
84
205
|
error: v.optional(v.any()),
|
|
85
206
|
raw: v.optional(v.any()),
|
|
86
207
|
userId: v.optional(v.string()),
|
|
87
208
|
},
|
|
88
209
|
returns: v.id("assemblies"),
|
|
89
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.
|
|
90
214
|
const existing = await ctx.db
|
|
91
215
|
.query("assemblies")
|
|
92
216
|
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", args.assemblyId))
|
|
@@ -146,6 +270,10 @@ export const replaceResultsForAssembly = internalMutation({
|
|
|
146
270
|
},
|
|
147
271
|
returns: v.null(),
|
|
148
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.
|
|
149
277
|
const existingResults = await ctx.db
|
|
150
278
|
.query("results")
|
|
151
279
|
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", args.assemblyId))
|
|
@@ -158,11 +286,12 @@ export const replaceResultsForAssembly = internalMutation({
|
|
|
158
286
|
const now = Date.now();
|
|
159
287
|
for (const entry of args.results) {
|
|
160
288
|
const raw = entry.result as Record<string, unknown>;
|
|
289
|
+
const sslUrl = getResultUrl(entry.result);
|
|
161
290
|
await ctx.db.insert("results", {
|
|
162
291
|
assemblyId: args.assemblyId,
|
|
163
292
|
stepName: entry.stepName,
|
|
164
293
|
resultId: typeof raw.id === "string" ? raw.id : undefined,
|
|
165
|
-
sslUrl
|
|
294
|
+
sslUrl,
|
|
166
295
|
name: typeof raw.name === "string" ? raw.name : undefined,
|
|
167
296
|
size: typeof raw.size === "number" ? raw.size : undefined,
|
|
168
297
|
mime: typeof raw.mime === "string" ? raw.mime : undefined,
|
|
@@ -188,10 +317,10 @@ export const createAssembly = action({
|
|
|
188
317
|
const { paramsString, params } = buildTransloaditParams({
|
|
189
318
|
authKey: args.config.authKey,
|
|
190
319
|
templateId: args.templateId,
|
|
191
|
-
steps: args.steps as
|
|
192
|
-
fields: args.fields as
|
|
320
|
+
steps: args.steps as AssemblyInstructionsInput["steps"],
|
|
321
|
+
fields: args.fields as AssemblyInstructionsInput["fields"],
|
|
193
322
|
notifyUrl: args.notifyUrl,
|
|
194
|
-
numExpectedUploadFiles:
|
|
323
|
+
numExpectedUploadFiles: undefined,
|
|
195
324
|
expires: args.expires,
|
|
196
325
|
additionalParams: args.additionalParams as
|
|
197
326
|
| Record<string, unknown>
|
|
@@ -206,6 +335,12 @@ export const createAssembly = action({
|
|
|
206
335
|
const formData = new FormData();
|
|
207
336
|
formData.append("params", paramsString);
|
|
208
337
|
formData.append("signature", signature);
|
|
338
|
+
if (typeof args.numExpectedUploadFiles === "number") {
|
|
339
|
+
formData.append(
|
|
340
|
+
"tus_num_expected_upload_files",
|
|
341
|
+
String(args.numExpectedUploadFiles),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
209
344
|
|
|
210
345
|
const response = await fetch(TRANSLOADIT_ASSEMBLY_URL, {
|
|
211
346
|
method: "POST",
|
|
@@ -214,8 +349,9 @@ export const createAssembly = action({
|
|
|
214
349
|
|
|
215
350
|
const data = (await response.json()) as Record<string, unknown>;
|
|
216
351
|
if (!response.ok) {
|
|
217
|
-
throw
|
|
218
|
-
|
|
352
|
+
throw transloaditError(
|
|
353
|
+
"createAssembly",
|
|
354
|
+
`HTTP ${response.status}: ${JSON.stringify(data)}`,
|
|
219
355
|
);
|
|
220
356
|
}
|
|
221
357
|
|
|
@@ -227,7 +363,10 @@ export const createAssembly = action({
|
|
|
227
363
|
: "";
|
|
228
364
|
|
|
229
365
|
if (!assemblyId) {
|
|
230
|
-
throw
|
|
366
|
+
throw transloaditError(
|
|
367
|
+
"createAssembly",
|
|
368
|
+
"Transloadit response missing assembly_id",
|
|
369
|
+
);
|
|
231
370
|
}
|
|
232
371
|
|
|
233
372
|
await ctx.runMutation(internal.lib.upsertAssembly, {
|
|
@@ -250,49 +389,68 @@ export const createAssembly = action({
|
|
|
250
389
|
},
|
|
251
390
|
});
|
|
252
391
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
392
|
+
const vWebhookArgs = {
|
|
393
|
+
payload: v.any(),
|
|
394
|
+
rawBody: v.optional(v.string()),
|
|
395
|
+
signature: v.optional(v.string()),
|
|
396
|
+
verifySignature: v.optional(v.boolean()),
|
|
397
|
+
authSecret: v.optional(v.string()),
|
|
398
|
+
};
|
|
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
|
+
|
|
407
|
+
export const processWebhook = internalAction({
|
|
408
|
+
args: vWebhookArgs,
|
|
258
409
|
returns: v.object({
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
410
|
+
assemblyId: v.string(),
|
|
411
|
+
resultCount: v.number(),
|
|
412
|
+
ok: v.optional(v.string()),
|
|
413
|
+
status: v.optional(v.string()),
|
|
262
414
|
}),
|
|
263
|
-
handler: async (
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
steps: args.steps as Record<string, unknown> | undefined,
|
|
268
|
-
fields: args.fields as Record<string, unknown> | undefined,
|
|
269
|
-
notifyUrl: args.notifyUrl,
|
|
270
|
-
numExpectedUploadFiles: args.numExpectedUploadFiles,
|
|
271
|
-
expires: args.expires,
|
|
272
|
-
additionalParams: args.additionalParams as
|
|
273
|
-
| Record<string, unknown>
|
|
274
|
-
| undefined,
|
|
275
|
-
});
|
|
415
|
+
handler: async (ctx, args) => {
|
|
416
|
+
const rawBody = resolveWebhookRawBody(args);
|
|
417
|
+
const shouldVerify = args.verifySignature ?? true;
|
|
418
|
+
const authSecret = args.authSecret ?? process.env.TRANSLOADIT_SECRET;
|
|
276
419
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
420
|
+
if (shouldVerify) {
|
|
421
|
+
if (!rawBody) {
|
|
422
|
+
throw transloaditError(
|
|
423
|
+
"webhook",
|
|
424
|
+
"Missing rawBody for webhook verification",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (!authSecret) {
|
|
428
|
+
throw transloaditError(
|
|
429
|
+
"webhook",
|
|
430
|
+
"Missing TRANSLOADIT_SECRET for webhook validation",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const verified = await verifyWebhookSignature({
|
|
434
|
+
rawBody,
|
|
435
|
+
signatureHeader: args.signature,
|
|
436
|
+
authSecret,
|
|
437
|
+
});
|
|
438
|
+
if (!verified) {
|
|
439
|
+
throw transloaditError(
|
|
440
|
+
"webhook",
|
|
441
|
+
"Invalid Transloadit webhook signature",
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
281
445
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
signature,
|
|
285
|
-
url: TRANSLOADIT_ASSEMBLY_URL,
|
|
286
|
-
};
|
|
446
|
+
const parsed = parseAssemblyPayload(args.payload);
|
|
447
|
+
return applyAssemblyStatus(ctx, parsed);
|
|
287
448
|
},
|
|
288
449
|
});
|
|
289
450
|
|
|
290
451
|
export const handleWebhook = action({
|
|
291
452
|
args: {
|
|
292
|
-
|
|
293
|
-
rawBody: v.optional(v.string()),
|
|
294
|
-
signature: v.optional(v.string()),
|
|
295
|
-
verifySignature: v.optional(v.boolean()),
|
|
453
|
+
...vPublicWebhookArgs,
|
|
296
454
|
config: v.optional(
|
|
297
455
|
v.object({
|
|
298
456
|
authSecret: v.string(),
|
|
@@ -302,19 +460,51 @@ export const handleWebhook = action({
|
|
|
302
460
|
returns: v.object({
|
|
303
461
|
assemblyId: v.string(),
|
|
304
462
|
resultCount: v.number(),
|
|
463
|
+
ok: v.optional(v.string()),
|
|
464
|
+
status: v.optional(v.string()),
|
|
305
465
|
}),
|
|
306
466
|
handler: async (ctx, args) => {
|
|
307
|
-
const
|
|
467
|
+
const verifySignature = args.verifySignature ?? true;
|
|
468
|
+
return ctx.runAction(internal.lib.processWebhook, {
|
|
469
|
+
payload: args.payload,
|
|
470
|
+
rawBody: args.rawBody,
|
|
471
|
+
signature: args.signature,
|
|
472
|
+
verifySignature,
|
|
473
|
+
authSecret: args.config?.authSecret,
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
export const queueWebhook = action({
|
|
479
|
+
args: {
|
|
480
|
+
...vPublicWebhookArgs,
|
|
481
|
+
config: v.optional(
|
|
482
|
+
v.object({
|
|
483
|
+
authSecret: v.string(),
|
|
484
|
+
}),
|
|
485
|
+
),
|
|
486
|
+
},
|
|
487
|
+
returns: v.object({
|
|
488
|
+
assemblyId: v.string(),
|
|
489
|
+
queued: v.boolean(),
|
|
490
|
+
}),
|
|
491
|
+
handler: async (ctx, args) => {
|
|
492
|
+
const rawBody = resolveWebhookRawBody(args);
|
|
308
493
|
const shouldVerify = args.verifySignature ?? true;
|
|
309
494
|
const authSecret =
|
|
310
|
-
args.config?.authSecret ??
|
|
311
|
-
process.env.TRANSLOADIT_AUTH_SECRET ??
|
|
312
|
-
process.env.TRANSLOADIT_SECRET;
|
|
495
|
+
args.config?.authSecret ?? process.env.TRANSLOADIT_SECRET;
|
|
313
496
|
|
|
314
497
|
if (shouldVerify) {
|
|
498
|
+
if (!rawBody) {
|
|
499
|
+
throw transloaditError(
|
|
500
|
+
"webhook",
|
|
501
|
+
"Missing rawBody for webhook verification",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
315
504
|
if (!authSecret) {
|
|
316
|
-
throw
|
|
317
|
-
"
|
|
505
|
+
throw transloaditError(
|
|
506
|
+
"webhook",
|
|
507
|
+
"Missing TRANSLOADIT_SECRET for webhook validation",
|
|
318
508
|
);
|
|
319
509
|
}
|
|
320
510
|
const verified = await verifyWebhookSignature({
|
|
@@ -323,45 +513,67 @@ export const handleWebhook = action({
|
|
|
323
513
|
authSecret,
|
|
324
514
|
});
|
|
325
515
|
if (!verified) {
|
|
326
|
-
throw
|
|
516
|
+
throw transloaditError(
|
|
517
|
+
"webhook",
|
|
518
|
+
"Invalid Transloadit webhook signature",
|
|
519
|
+
);
|
|
327
520
|
}
|
|
328
521
|
}
|
|
329
522
|
|
|
330
|
-
const
|
|
331
|
-
const assemblyId =
|
|
332
|
-
typeof payload.assembly_id === "string"
|
|
333
|
-
? payload.assembly_id
|
|
334
|
-
: typeof payload.assemblyId === "string"
|
|
335
|
-
? payload.assemblyId
|
|
336
|
-
: "";
|
|
337
|
-
|
|
523
|
+
const parsed = parseAssemblyPayload(args.payload);
|
|
524
|
+
const assemblyId = resolveAssemblyId(parsed);
|
|
338
525
|
if (!assemblyId) {
|
|
339
|
-
throw
|
|
526
|
+
throw transloaditError("webhook", "Webhook payload missing assembly_id");
|
|
340
527
|
}
|
|
341
528
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
assemblyId,
|
|
349
|
-
status: typeof payload.ok === "string" ? payload.ok : undefined,
|
|
350
|
-
ok: typeof payload.ok === "string" ? payload.ok : undefined,
|
|
351
|
-
message:
|
|
352
|
-
typeof payload.message === "string" ? payload.message : undefined,
|
|
353
|
-
uploads: payload.uploads,
|
|
354
|
-
results: payload.results,
|
|
355
|
-
error: payload.error,
|
|
356
|
-
raw: payload,
|
|
529
|
+
await ctx.scheduler.runAfter(0, internal.lib.processWebhook, {
|
|
530
|
+
payload: parsed,
|
|
531
|
+
rawBody: args.rawBody,
|
|
532
|
+
signature: args.signature,
|
|
533
|
+
verifySignature: true,
|
|
534
|
+
authSecret: args.config?.authSecret,
|
|
357
535
|
});
|
|
358
536
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
537
|
+
return { assemblyId, queued: true };
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
export const refreshAssembly = action({
|
|
542
|
+
args: {
|
|
543
|
+
assemblyId: v.string(),
|
|
544
|
+
config: v.optional(
|
|
545
|
+
v.object({
|
|
546
|
+
authKey: v.string(),
|
|
547
|
+
authSecret: v.string(),
|
|
548
|
+
}),
|
|
549
|
+
),
|
|
550
|
+
},
|
|
551
|
+
returns: v.object({
|
|
552
|
+
assemblyId: v.string(),
|
|
553
|
+
resultCount: v.number(),
|
|
554
|
+
ok: v.optional(v.string()),
|
|
555
|
+
status: v.optional(v.string()),
|
|
556
|
+
}),
|
|
557
|
+
handler: async (ctx, args) => {
|
|
558
|
+
const { assemblyId } = args;
|
|
559
|
+
const authKey = args.config?.authKey ?? process.env.TRANSLOADIT_KEY;
|
|
560
|
+
const authSecret =
|
|
561
|
+
args.config?.authSecret ?? process.env.TRANSLOADIT_SECRET;
|
|
562
|
+
const url =
|
|
563
|
+
authKey && authSecret
|
|
564
|
+
? await buildSignedAssemblyUrl(assemblyId, authKey, authSecret)
|
|
565
|
+
: `${TRANSLOADIT_ASSEMBLY_URL}/${assemblyId}`;
|
|
566
|
+
|
|
567
|
+
const response = await fetch(url);
|
|
568
|
+
const payload = parseAssemblyPayload(await response.json());
|
|
569
|
+
if (!response.ok) {
|
|
570
|
+
throw transloaditError(
|
|
571
|
+
"status",
|
|
572
|
+
`HTTP ${response.status}: ${JSON.stringify(payload)}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
363
575
|
|
|
364
|
-
return
|
|
576
|
+
return applyAssemblyStatus(ctx, payload);
|
|
365
577
|
},
|
|
366
578
|
});
|
|
367
579
|
|
|
@@ -437,7 +649,7 @@ export const storeAssemblyMetadata = mutation({
|
|
|
437
649
|
args: {
|
|
438
650
|
assemblyId: v.string(),
|
|
439
651
|
userId: v.optional(v.string()),
|
|
440
|
-
fields: v.optional(v.any()),
|
|
652
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
441
653
|
},
|
|
442
654
|
returns: v.union(vAssembly, v.null()),
|
|
443
655
|
handler: async (ctx, args) => {
|
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(),
|
|
@@ -20,9 +10,9 @@ export default defineSchema({
|
|
|
20
10
|
templateId: v.optional(v.string()),
|
|
21
11
|
notifyUrl: v.optional(v.string()),
|
|
22
12
|
numExpectedUploadFiles: v.optional(v.number()),
|
|
23
|
-
fields: v.optional(v.any()),
|
|
24
|
-
uploads: v.optional(v.any()),
|
|
25
|
-
results: v.optional(v.any()),
|
|
13
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
14
|
+
uploads: v.optional(v.array(v.any())),
|
|
15
|
+
results: v.optional(v.record(v.string(), v.array(v.any()))),
|
|
26
16
|
error: v.optional(v.any()),
|
|
27
17
|
raw: v.optional(v.any()),
|
|
28
18
|
createdAt: v.number(),
|
|
@@ -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
|
+
};
|