@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.
Files changed (87) hide show
  1. package/README.md +184 -121
  2. package/dist/client/index.d.ts +100 -60
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +69 -31
  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 +35 -15
  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 +36 -7
  12. package/dist/component/apiUtils.d.ts.map +1 -1
  13. package/dist/component/apiUtils.js +60 -40
  14. package/dist/component/apiUtils.js.map +1 -1
  15. package/dist/component/lib.d.ts +71 -49
  16. package/dist/component/lib.d.ts.map +1 -1
  17. package/dist/component/lib.js +206 -73
  18. package/dist/component/lib.js.map +1 -1
  19. package/dist/component/schema.d.ts +11 -13
  20. package/dist/component/schema.d.ts.map +1 -1
  21. package/dist/component/schema.js +3 -10
  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 +213 -17
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/react/index.js +726 -105
  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 +65 -0
  56. package/dist/test/index.d.ts.map +1 -0
  57. package/dist/test/index.js +8 -0
  58. package/dist/test/index.js.map +1 -0
  59. package/dist/test/nodeModules.d.ts +2 -0
  60. package/dist/test/nodeModules.d.ts.map +1 -0
  61. package/dist/test/nodeModules.js +19 -0
  62. package/dist/test/nodeModules.js.map +1 -0
  63. package/package.json +53 -15
  64. package/src/client/index.ts +141 -38
  65. package/src/component/_generated/api.ts +2 -2
  66. package/src/component/_generated/component.ts +44 -13
  67. package/src/component/_generated/dataModel.ts +1 -1
  68. package/src/component/_generated/server.ts +1 -1
  69. package/src/component/apiUtils.test.ts +195 -2
  70. package/src/component/apiUtils.ts +124 -66
  71. package/src/component/lib.test.ts +243 -7
  72. package/src/component/lib.ts +302 -90
  73. package/src/component/schema.ts +3 -13
  74. package/src/debug/index.ts +84 -0
  75. package/src/react/index.test.tsx +340 -0
  76. package/src/react/index.tsx +1105 -152
  77. package/src/react/uploadWithTus.test.tsx +192 -0
  78. package/src/shared/assemblyUrls.test.ts +71 -0
  79. package/src/shared/assemblyUrls.ts +59 -0
  80. package/src/shared/errors.ts +23 -0
  81. package/src/shared/pollAssembly.ts +65 -0
  82. package/src/shared/resultTypes.ts +44 -0
  83. package/src/shared/resultUtils.test.ts +29 -0
  84. package/src/shared/resultUtils.ts +71 -0
  85. package/src/shared/tusUpload.ts +59 -0
  86. package/src/test/index.ts +10 -0
  87. package/src/test/nodeModules.ts +19 -0
@@ -1,10 +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,
8
+ parseTransloaditWebhook,
5
9
  signTransloaditParams,
6
10
  verifyWebhookSignature,
7
- } from "./apiUtils.js";
11
+ } from "./apiUtils.ts";
8
12
 
9
13
  describe("apiUtils", () => {
10
14
  test("buildTransloaditParams requires templateId or steps", () => {
@@ -45,4 +49,193 @@ describe("apiUtils", () => {
45
49
 
46
50
  expect(verified).toBe(true);
47
51
  });
52
+
53
+ test("parseTransloaditWebhook returns payload and signature", async () => {
54
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
55
+ const formData = new FormData();
56
+ formData.append("transloadit", JSON.stringify(payload));
57
+ formData.append("signature", "sha384:abc");
58
+
59
+ const request = new Request("http://localhost", {
60
+ method: "POST",
61
+ body: formData,
62
+ });
63
+
64
+ const result = await parseTransloaditWebhook(request);
65
+ expect(result.payload).toEqual(payload);
66
+ expect(result.rawBody).toBe(JSON.stringify(payload));
67
+ expect(result.signature).toBe("sha384:abc");
68
+ });
69
+
70
+ test("parseTransloaditWebhook throws on missing payload", async () => {
71
+ const request = new Request("http://localhost", {
72
+ method: "POST",
73
+ body: new FormData(),
74
+ });
75
+
76
+ await expect(parseTransloaditWebhook(request)).rejects.toThrow(
77
+ "Missing transloadit payload",
78
+ );
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
+ });
48
241
  });
@@ -1,3 +1,8 @@
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";
5
+
1
6
  export interface TransloaditAuthConfig {
2
7
  authKey: string;
3
8
  authSecret: string;
@@ -6,8 +11,8 @@ export interface TransloaditAuthConfig {
6
11
  export interface BuildParamsOptions {
7
12
  authKey: string;
8
13
  templateId?: string;
9
- steps?: Record<string, unknown>;
10
- fields?: Record<string, unknown>;
14
+ steps?: AssemblyInstructionsInput["steps"];
15
+ fields?: AssemblyInstructionsInput["fields"];
11
16
  notifyUrl?: string;
12
17
  numExpectedUploadFiles?: number;
13
18
  expires?: string;
@@ -23,7 +28,10 @@ export function buildTransloaditParams(
23
28
  options: BuildParamsOptions,
24
29
  ): BuildParamsResult {
25
30
  if (!options.templateId && !options.steps) {
26
- 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
+ );
27
35
  }
28
36
 
29
37
  const auth: Record<string, string> = {
@@ -59,90 +67,140 @@ export function buildTransloaditParams(
59
67
  };
60
68
  }
61
69
 
62
- async function hmacHex(
63
- algorithm: "SHA-384" | "SHA-1",
64
- key: string,
65
- data: string,
66
- ): Promise<string> {
67
- if (globalThis.crypto?.subtle) {
68
- const encoder = new TextEncoder();
69
- const cryptoKey = await globalThis.crypto.subtle.importKey(
70
- "raw",
71
- encoder.encode(key),
72
- { name: "HMAC", hash: { name: algorithm } },
73
- false,
74
- ["sign"],
75
- );
76
- const signature = await globalThis.crypto.subtle.sign(
77
- "HMAC",
78
- cryptoKey,
79
- encoder.encode(data),
80
- );
81
- const bytes = new Uint8Array(signature);
82
- return Array.from(bytes)
83
- .map((byte) => byte.toString(16).padStart(2, "0"))
84
- .join("");
85
- }
86
-
87
- const { createHmac } = await import("node:crypto");
88
- return createHmac(algorithm.replace("-", "").toLowerCase(), key)
89
- .update(data)
90
- .digest("hex");
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
- function safeCompare(a: string, b: string): boolean {
102
- if (a.length !== b.length) return false;
103
- let mismatch = 0;
104
- for (let i = 0; i < a.length; i += 1) {
105
- mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
77
+ export type ParsedWebhookRequest = {
78
+ payload: unknown;
79
+ rawBody: string;
80
+ signature?: string;
81
+ };
82
+
83
+ export type VerifiedWebhookRequest = ParsedWebhookRequest & {
84
+ verified: boolean;
85
+ };
86
+
87
+ export async function parseTransloaditWebhook(
88
+ request: Request,
89
+ ): Promise<ParsedWebhookRequest> {
90
+ const formData = await request.formData();
91
+ const rawPayload = formData.get("transloadit");
92
+ const signature = formData.get("signature");
93
+
94
+ if (typeof rawPayload !== "string") {
95
+ throw transloaditError("webhook", "Missing transloadit payload");
106
96
  }
107
- return mismatch === 0;
97
+
98
+ return {
99
+ payload: JSON.parse(rawPayload),
100
+ rawBody: rawPayload,
101
+ signature: typeof signature === "string" ? signature : undefined,
102
+ };
108
103
  }
109
104
 
110
- export async function verifyWebhookSignature(options: {
111
- rawBody: string;
112
- signatureHeader?: string;
113
- authSecret: string;
114
- }): Promise<boolean> {
115
- if (!options.signatureHeader) return false;
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
+ }
133
+ }
116
134
 
117
- const signatureHeader = options.signatureHeader.trim();
118
- if (!signatureHeader) return false;
135
+ return { ...parsed, verified };
136
+ }
119
137
 
120
- const [prefix, sig] = signatureHeader.includes(":")
121
- ? (signatureHeader.split(":") as [string, string])
122
- : ["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
+ }
123
148
 
124
- const normalized = prefix.toLowerCase();
125
- 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
+ }
126
156
 
127
- if (normalized !== "sha384" && normalized !== "sha1") {
128
- return false;
129
- }
157
+ export type WebhookActionArgs = {
158
+ payload: unknown;
159
+ rawBody?: string;
160
+ signature?: string;
161
+ };
130
162
 
131
- const expected = await hmacHex(
132
- algorithm,
133
- options.authSecret,
134
- options.rawBody,
135
- );
136
- 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 });
137
191
  }
138
192
 
193
+ export { verifyWebhookSignature };
194
+
195
+ export type AssemblyResult = AssemblyStatusResults[string][number];
196
+
139
197
  export type AssemblyResultRecord = {
140
198
  stepName: string;
141
- result: Record<string, unknown>;
199
+ result: AssemblyResult;
142
200
  };
143
201
 
144
202
  export function flattenResults(
145
- results: Record<string, Array<Record<string, unknown>>> | undefined,
203
+ results: AssemblyStatusResults | undefined,
146
204
  ): AssemblyResultRecord[] {
147
205
  if (!results) return [];
148
206
  const output: AssemblyResultRecord[] = [];