@transloadit/convex 0.0.1 → 0.0.3
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 +162 -79
- package/dist/client/index.d.ts +92 -63
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +57 -30
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +35 -15
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/apiUtils.d.ts +13 -4
- package/dist/component/apiUtils.d.ts.map +1 -1
- package/dist/component/apiUtils.js +22 -12
- package/dist/component/apiUtils.js.map +1 -1
- package/dist/component/lib.d.ts +69 -45
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +154 -77
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +9 -9
- package/dist/component/schema.js +3 -3
- package/dist/component/schema.js.map +1 -1
- package/dist/react/index.d.ts +23 -25
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +129 -88
- package/dist/react/index.js.map +1 -1
- 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/package.json +27 -15
- package/src/client/index.ts +72 -35
- package/src/client/types.ts +12 -11
- package/src/component/_generated/component.ts +44 -13
- package/src/component/apiUtils.test.ts +29 -0
- package/src/component/apiUtils.ts +52 -26
- package/src/component/lib.test.ts +73 -3
- package/src/component/lib.ts +220 -97
- package/src/component/schema.ts +3 -3
- package/src/react/index.tsx +193 -150
- package/src/test/index.ts +10 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transloadit/convex",
|
|
3
3
|
"description": "Transloadit component for Convex",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.5.0",
|
|
7
7
|
"repository": {
|
|
@@ -25,16 +25,18 @@
|
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"test": "vitest run --typecheck",
|
|
27
27
|
"test:watch": "vitest --typecheck",
|
|
28
|
+
"test:browser": "dotenv -q -- node scripts/run-browser.ts",
|
|
28
29
|
"lint": "biome check .",
|
|
29
30
|
"format": "biome format . --write",
|
|
31
|
+
"check": "yarn format && yarn lint && yarn typecheck && yarn test",
|
|
30
32
|
"prepack": "yarn build",
|
|
31
|
-
"smoke": "dotenv -q -- node scripts/smoke-test.ts",
|
|
32
33
|
"template:ensure": "dotenv -q -- node scripts/ensure-template.ts",
|
|
33
|
-
"tunnel": "node scripts/start-webhook-tunnel.ts"
|
|
34
|
-
"qa:full": "dotenv -q -- node scripts/qa-full.ts",
|
|
35
|
-
"qa:full:verbose": "dotenv -q -- node scripts/qa-full.ts --verbose"
|
|
34
|
+
"tunnel": "node scripts/start-webhook-tunnel.ts"
|
|
36
35
|
},
|
|
37
|
-
"files": [
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
38
40
|
"exports": {
|
|
39
41
|
"./package.json": "./package.json",
|
|
40
42
|
".": {
|
|
@@ -52,6 +54,10 @@
|
|
|
52
54
|
"types": "./dist/component/lib.d.ts",
|
|
53
55
|
"default": "./dist/component/lib.js"
|
|
54
56
|
},
|
|
57
|
+
"./test": {
|
|
58
|
+
"types": "./dist/test/index.d.ts",
|
|
59
|
+
"default": "./dist/test/index.js"
|
|
60
|
+
},
|
|
55
61
|
"./convex.config": {
|
|
56
62
|
"@convex-dev/component-source": "./src/component/convex.config.ts",
|
|
57
63
|
"types": "./dist/component/convex.config.d.ts",
|
|
@@ -68,19 +74,25 @@
|
|
|
68
74
|
}
|
|
69
75
|
},
|
|
70
76
|
"dependencies": {
|
|
71
|
-
"
|
|
77
|
+
"@transloadit/types": "^4.1.3",
|
|
78
|
+
"tus-js-client": "^4.3.1"
|
|
72
79
|
},
|
|
73
80
|
"devDependencies": {
|
|
74
|
-
"@biomejs/biome": "^
|
|
81
|
+
"@biomejs/biome": "^2.3.11",
|
|
75
82
|
"@edge-runtime/vm": "^5.0.0",
|
|
76
|
-
"@
|
|
77
|
-
"@types/
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
83
|
+
"@playwright/test": "^1.57.0",
|
|
84
|
+
"@types/node": "^25.0.8",
|
|
85
|
+
"@types/react": "^19.2.8",
|
|
86
|
+
"@types/react-dom": "^19",
|
|
87
|
+
"convex": "^1.31.4",
|
|
88
|
+
"convex-test": "^0.0.41",
|
|
89
|
+
"dotenv-cli": "^11.0.0",
|
|
90
|
+
"esbuild": "^0.27.2",
|
|
91
|
+
"react": "^19.2.3",
|
|
92
|
+
"react-dom": "^19.2.3",
|
|
81
93
|
"typescript": "^5.9.3",
|
|
82
|
-
"vitest": "
|
|
94
|
+
"vitest": "^4.0.17"
|
|
83
95
|
},
|
|
84
96
|
"types": "./dist/client/index.d.ts",
|
|
85
97
|
"module": "./dist/client/index.js"
|
|
86
|
-
}
|
|
98
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import type { AssemblyStatus } from "@transloadit/types/assemblyStatus";
|
|
2
|
+
import type { AssemblyInstructionsInput } from "@transloadit/types/template";
|
|
1
3
|
import { actionGeneric, mutationGeneric, queryGeneric } from "convex/server";
|
|
2
4
|
import { type Infer, v } from "convex/values";
|
|
3
5
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
4
6
|
import type { RunActionCtx, RunMutationCtx, RunQueryCtx } from "./types.js";
|
|
5
7
|
|
|
8
|
+
export { parseTransloaditWebhook } from "../component/apiUtils.js";
|
|
9
|
+
export type { AssemblyStatus, AssemblyInstructionsInput };
|
|
10
|
+
|
|
6
11
|
export interface TransloaditConfig {
|
|
7
12
|
authKey: string;
|
|
8
13
|
authSecret: string;
|
|
@@ -30,9 +35,9 @@ export const vAssemblyResponse = v.object({
|
|
|
30
35
|
templateId: v.optional(v.string()),
|
|
31
36
|
notifyUrl: v.optional(v.string()),
|
|
32
37
|
numExpectedUploadFiles: v.optional(v.number()),
|
|
33
|
-
fields: v.optional(v.any()),
|
|
34
|
-
uploads: v.optional(v.any()),
|
|
35
|
-
results: v.optional(v.any()),
|
|
38
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
39
|
+
uploads: v.optional(v.array(v.any())),
|
|
40
|
+
results: v.optional(v.record(v.string(), v.array(v.any()))),
|
|
36
41
|
error: v.optional(v.any()),
|
|
37
42
|
raw: v.optional(v.any()),
|
|
38
43
|
createdAt: v.number(),
|
|
@@ -60,12 +65,12 @@ export type AssemblyResultResponse = Infer<typeof vAssemblyResultResponse>;
|
|
|
60
65
|
|
|
61
66
|
export const vCreateAssemblyArgs = v.object({
|
|
62
67
|
templateId: v.optional(v.string()),
|
|
63
|
-
steps: v.optional(v.any()),
|
|
64
|
-
fields: v.optional(v.any()),
|
|
68
|
+
steps: v.optional(v.record(v.string(), v.any())),
|
|
69
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
65
70
|
notifyUrl: v.optional(v.string()),
|
|
66
71
|
numExpectedUploadFiles: v.optional(v.number()),
|
|
67
72
|
expires: v.optional(v.string()),
|
|
68
|
-
additionalParams: v.optional(v.any()),
|
|
73
|
+
additionalParams: v.optional(v.record(v.string(), v.any())),
|
|
69
74
|
userId: v.optional(v.string()),
|
|
70
75
|
});
|
|
71
76
|
|
|
@@ -79,12 +84,8 @@ export class TransloaditClient {
|
|
|
79
84
|
) {
|
|
80
85
|
this.component = component;
|
|
81
86
|
this.config = {
|
|
82
|
-
authKey:
|
|
83
|
-
|
|
84
|
-
requireEnv(["TRANSLOADIT_AUTH_KEY", "TRANSLOADIT_KEY"]),
|
|
85
|
-
authSecret:
|
|
86
|
-
config?.authSecret ??
|
|
87
|
-
requireEnv(["TRANSLOADIT_AUTH_SECRET", "TRANSLOADIT_SECRET"]),
|
|
87
|
+
authKey: config?.authKey ?? requireEnv(["TRANSLOADIT_KEY"]),
|
|
88
|
+
authSecret: config?.authSecret ?? requireEnv(["TRANSLOADIT_SECRET"]),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -102,17 +103,22 @@ export class TransloaditClient {
|
|
|
102
103
|
});
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
async
|
|
106
|
+
async handleWebhook(
|
|
106
107
|
ctx: RunActionCtx,
|
|
107
|
-
args:
|
|
108
|
+
args: {
|
|
109
|
+
payload: unknown;
|
|
110
|
+
rawBody?: string;
|
|
111
|
+
signature?: string;
|
|
112
|
+
verifySignature?: boolean;
|
|
113
|
+
},
|
|
108
114
|
) {
|
|
109
|
-
return ctx.runAction(this.component.lib.
|
|
115
|
+
return ctx.runAction(this.component.lib.handleWebhook, {
|
|
110
116
|
...args,
|
|
111
|
-
config: this.config,
|
|
117
|
+
config: { authSecret: this.config.authSecret },
|
|
112
118
|
});
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
async
|
|
121
|
+
async queueWebhook(
|
|
116
122
|
ctx: RunActionCtx,
|
|
117
123
|
args: {
|
|
118
124
|
payload: unknown;
|
|
@@ -121,12 +127,19 @@ export class TransloaditClient {
|
|
|
121
127
|
verifySignature?: boolean;
|
|
122
128
|
},
|
|
123
129
|
) {
|
|
124
|
-
return ctx.runAction(this.component.lib.
|
|
130
|
+
return ctx.runAction(this.component.lib.queueWebhook, {
|
|
125
131
|
...args,
|
|
126
132
|
config: { authSecret: this.config.authSecret },
|
|
127
133
|
});
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
async refreshAssembly(ctx: RunActionCtx, assemblyId: string) {
|
|
137
|
+
return ctx.runAction(this.component.lib.refreshAssembly, {
|
|
138
|
+
assemblyId,
|
|
139
|
+
config: this.config,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
async getAssemblyStatus(ctx: RunQueryCtx, assemblyId: string) {
|
|
131
144
|
return ctx.runQuery(this.component.lib.getAssemblyStatus, { assemblyId });
|
|
132
145
|
}
|
|
@@ -157,17 +170,22 @@ export class TransloaditClient {
|
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
172
|
|
|
173
|
+
export class Transloadit extends TransloaditClient {}
|
|
174
|
+
|
|
175
|
+
export function createTransloadit(
|
|
176
|
+
component: TransloaditComponent,
|
|
177
|
+
config?: Partial<TransloaditConfig>,
|
|
178
|
+
) {
|
|
179
|
+
return new Transloadit(component, config);
|
|
180
|
+
}
|
|
181
|
+
|
|
160
182
|
export function makeTransloaditAPI(
|
|
161
183
|
component: TransloaditComponent,
|
|
162
184
|
config?: Partial<TransloaditConfig>,
|
|
163
185
|
) {
|
|
164
186
|
const resolvedConfig: TransloaditConfig = {
|
|
165
|
-
authKey:
|
|
166
|
-
|
|
167
|
-
requireEnv(["TRANSLOADIT_AUTH_KEY", "TRANSLOADIT_KEY"]),
|
|
168
|
-
authSecret:
|
|
169
|
-
config?.authSecret ??
|
|
170
|
-
requireEnv(["TRANSLOADIT_AUTH_SECRET", "TRANSLOADIT_SECRET"]),
|
|
187
|
+
authKey: config?.authKey ?? requireEnv(["TRANSLOADIT_KEY"]),
|
|
188
|
+
authSecret: config?.authSecret ?? requireEnv(["TRANSLOADIT_SECRET"]),
|
|
171
189
|
};
|
|
172
190
|
|
|
173
191
|
return {
|
|
@@ -184,21 +202,25 @@ export function makeTransloaditAPI(
|
|
|
184
202
|
});
|
|
185
203
|
},
|
|
186
204
|
}),
|
|
187
|
-
|
|
188
|
-
args:
|
|
205
|
+
handleWebhook: actionGeneric({
|
|
206
|
+
args: {
|
|
207
|
+
payload: v.any(),
|
|
208
|
+
rawBody: v.optional(v.string()),
|
|
209
|
+
signature: v.optional(v.string()),
|
|
210
|
+
verifySignature: v.optional(v.boolean()),
|
|
211
|
+
},
|
|
189
212
|
returns: v.object({
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
url: v.string(),
|
|
213
|
+
assemblyId: v.string(),
|
|
214
|
+
resultCount: v.number(),
|
|
193
215
|
}),
|
|
194
216
|
handler: async (ctx, args) => {
|
|
195
|
-
return ctx.runAction(component.lib.
|
|
217
|
+
return ctx.runAction(component.lib.handleWebhook, {
|
|
196
218
|
...args,
|
|
197
|
-
config: resolvedConfig,
|
|
219
|
+
config: { authSecret: resolvedConfig.authSecret },
|
|
198
220
|
});
|
|
199
221
|
},
|
|
200
222
|
}),
|
|
201
|
-
|
|
223
|
+
queueWebhook: actionGeneric({
|
|
202
224
|
args: {
|
|
203
225
|
payload: v.any(),
|
|
204
226
|
rawBody: v.optional(v.string()),
|
|
@@ -207,15 +229,30 @@ export function makeTransloaditAPI(
|
|
|
207
229
|
},
|
|
208
230
|
returns: v.object({
|
|
209
231
|
assemblyId: v.string(),
|
|
210
|
-
|
|
232
|
+
queued: v.boolean(),
|
|
211
233
|
}),
|
|
212
234
|
handler: async (ctx, args) => {
|
|
213
|
-
return ctx.runAction(component.lib.
|
|
235
|
+
return ctx.runAction(component.lib.queueWebhook, {
|
|
214
236
|
...args,
|
|
215
237
|
config: { authSecret: resolvedConfig.authSecret },
|
|
216
238
|
});
|
|
217
239
|
},
|
|
218
240
|
}),
|
|
241
|
+
refreshAssembly: actionGeneric({
|
|
242
|
+
args: { assemblyId: v.string() },
|
|
243
|
+
returns: v.object({
|
|
244
|
+
assemblyId: v.string(),
|
|
245
|
+
resultCount: v.number(),
|
|
246
|
+
ok: v.optional(v.string()),
|
|
247
|
+
status: v.optional(v.string()),
|
|
248
|
+
}),
|
|
249
|
+
handler: async (ctx, args) => {
|
|
250
|
+
return ctx.runAction(component.lib.refreshAssembly, {
|
|
251
|
+
...args,
|
|
252
|
+
config: resolvedConfig,
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
219
256
|
getAssemblyStatus: queryGeneric({
|
|
220
257
|
args: { assemblyId: v.string() },
|
|
221
258
|
returns: v.union(vAssemblyResponse, v.null()),
|
|
@@ -249,7 +286,7 @@ export function makeTransloaditAPI(
|
|
|
249
286
|
args: {
|
|
250
287
|
assemblyId: v.string(),
|
|
251
288
|
userId: v.optional(v.string()),
|
|
252
|
-
fields: v.optional(v.any()),
|
|
289
|
+
fields: v.optional(v.record(v.string(), v.any())),
|
|
253
290
|
},
|
|
254
291
|
returns: v.union(vAssemblyResponse, v.null()),
|
|
255
292
|
handler: async (ctx, args) => {
|
package/src/client/types.ts
CHANGED
|
@@ -33,17 +33,18 @@ export type QueryCtx = RunQueryCtx & {
|
|
|
33
33
|
storage: StorageReader;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
export type OpaqueIds<T> =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
export type OpaqueIds<T> =
|
|
37
|
+
T extends GenericId<infer _T>
|
|
38
|
+
? string
|
|
39
|
+
: T extends (infer U)[]
|
|
40
|
+
? OpaqueIds<U>[]
|
|
41
|
+
: T extends ArrayBuffer
|
|
42
|
+
? ArrayBuffer
|
|
43
|
+
: T extends object
|
|
44
|
+
? {
|
|
45
|
+
[K in keyof T]: OpaqueIds<T[K]>;
|
|
46
|
+
}
|
|
47
|
+
: T;
|
|
47
48
|
|
|
48
49
|
export type UseApi<API> = Expand<{
|
|
49
50
|
[mod in keyof API]: API[mod] extends FunctionReference<
|
|
@@ -60,24 +60,43 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
60
60
|
{ assemblyId: string; data: any },
|
|
61
61
|
Name
|
|
62
62
|
>;
|
|
63
|
-
|
|
63
|
+
handleWebhook: FunctionReference<
|
|
64
64
|
"action",
|
|
65
65
|
"internal",
|
|
66
66
|
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
payload: any;
|
|
68
|
+
rawBody?: string;
|
|
69
|
+
signature?: string;
|
|
70
|
+
verifySignature?: boolean;
|
|
71
|
+
config?: { authSecret: string };
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
assemblyId: string;
|
|
75
|
+
resultCount: number;
|
|
76
|
+
ok?: string;
|
|
77
|
+
status?: string;
|
|
76
78
|
},
|
|
77
|
-
{ params: string; signature: string; url: string },
|
|
78
79
|
Name
|
|
79
80
|
>;
|
|
80
|
-
|
|
81
|
+
processWebhook: FunctionReference<
|
|
82
|
+
"action",
|
|
83
|
+
"internal",
|
|
84
|
+
{
|
|
85
|
+
payload: any;
|
|
86
|
+
rawBody?: string;
|
|
87
|
+
signature?: string;
|
|
88
|
+
verifySignature?: boolean;
|
|
89
|
+
authSecret?: string;
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
assemblyId: string;
|
|
93
|
+
resultCount: number;
|
|
94
|
+
ok?: string;
|
|
95
|
+
status?: string;
|
|
96
|
+
},
|
|
97
|
+
Name
|
|
98
|
+
>;
|
|
99
|
+
queueWebhook: FunctionReference<
|
|
81
100
|
"action",
|
|
82
101
|
"internal",
|
|
83
102
|
{
|
|
@@ -87,7 +106,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
87
106
|
verifySignature?: boolean;
|
|
88
107
|
config?: { authSecret: string };
|
|
89
108
|
},
|
|
90
|
-
{ assemblyId: string;
|
|
109
|
+
{ assemblyId: string; queued: boolean },
|
|
110
|
+
Name
|
|
111
|
+
>;
|
|
112
|
+
refreshAssembly: FunctionReference<
|
|
113
|
+
"action",
|
|
114
|
+
"internal",
|
|
115
|
+
{ assemblyId: string; config?: { authKey: string; authSecret: string } },
|
|
116
|
+
{
|
|
117
|
+
assemblyId: string;
|
|
118
|
+
resultCount: number;
|
|
119
|
+
ok?: string;
|
|
120
|
+
status?: string;
|
|
121
|
+
},
|
|
91
122
|
Name
|
|
92
123
|
>;
|
|
93
124
|
getAssemblyStatus: FunctionReference<
|
|
@@ -2,6 +2,7 @@ import { createHmac } from "node:crypto";
|
|
|
2
2
|
import { describe, expect, test } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
buildTransloaditParams,
|
|
5
|
+
parseTransloaditWebhook,
|
|
5
6
|
signTransloaditParams,
|
|
6
7
|
verifyWebhookSignature,
|
|
7
8
|
} from "./apiUtils.js";
|
|
@@ -45,4 +46,32 @@ describe("apiUtils", () => {
|
|
|
45
46
|
|
|
46
47
|
expect(verified).toBe(true);
|
|
47
48
|
});
|
|
49
|
+
|
|
50
|
+
test("parseTransloaditWebhook returns payload and signature", async () => {
|
|
51
|
+
const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
|
|
52
|
+
const formData = new FormData();
|
|
53
|
+
formData.append("transloadit", JSON.stringify(payload));
|
|
54
|
+
formData.append("signature", "sha384:abc");
|
|
55
|
+
|
|
56
|
+
const request = new Request("http://localhost", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
body: formData,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await parseTransloaditWebhook(request);
|
|
62
|
+
expect(result.payload).toEqual(payload);
|
|
63
|
+
expect(result.rawBody).toBe(JSON.stringify(payload));
|
|
64
|
+
expect(result.signature).toBe("sha384:abc");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("parseTransloaditWebhook throws on missing payload", async () => {
|
|
68
|
+
const request = new Request("http://localhost", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: new FormData(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await expect(parseTransloaditWebhook(request)).rejects.toThrow(
|
|
74
|
+
"Missing transloadit payload",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
48
77
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { AssemblyStatusResults } from "@transloadit/types/assemblyStatus";
|
|
2
|
+
import type { AssemblyInstructionsInput } from "@transloadit/types/template";
|
|
3
|
+
|
|
1
4
|
export interface TransloaditAuthConfig {
|
|
2
5
|
authKey: string;
|
|
3
6
|
authSecret: string;
|
|
@@ -6,8 +9,8 @@ export interface TransloaditAuthConfig {
|
|
|
6
9
|
export interface BuildParamsOptions {
|
|
7
10
|
authKey: string;
|
|
8
11
|
templateId?: string;
|
|
9
|
-
steps?:
|
|
10
|
-
fields?:
|
|
12
|
+
steps?: AssemblyInstructionsInput["steps"];
|
|
13
|
+
fields?: AssemblyInstructionsInput["fields"];
|
|
11
14
|
notifyUrl?: string;
|
|
12
15
|
numExpectedUploadFiles?: number;
|
|
13
16
|
expires?: string;
|
|
@@ -64,30 +67,27 @@ async function hmacHex(
|
|
|
64
67
|
key: string,
|
|
65
68
|
data: string,
|
|
66
69
|
): Promise<string> {
|
|
67
|
-
if (globalThis.crypto?.subtle) {
|
|
68
|
-
|
|
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("");
|
|
70
|
+
if (!globalThis.crypto?.subtle) {
|
|
71
|
+
throw new Error("Web Crypto is required to sign Transloadit payloads");
|
|
85
72
|
}
|
|
86
73
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
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
91
|
}
|
|
92
92
|
|
|
93
93
|
export async function signTransloaditParams(
|
|
@@ -98,6 +98,30 @@ export async function signTransloaditParams(
|
|
|
98
98
|
return `sha384:${signature}`;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export type ParsedWebhookRequest = {
|
|
102
|
+
payload: unknown;
|
|
103
|
+
rawBody: string;
|
|
104
|
+
signature?: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export async function parseTransloaditWebhook(
|
|
108
|
+
request: Request,
|
|
109
|
+
): Promise<ParsedWebhookRequest> {
|
|
110
|
+
const formData = await request.formData();
|
|
111
|
+
const rawPayload = formData.get("transloadit");
|
|
112
|
+
const signature = formData.get("signature");
|
|
113
|
+
|
|
114
|
+
if (typeof rawPayload !== "string") {
|
|
115
|
+
throw new Error("Missing transloadit payload");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
payload: JSON.parse(rawPayload),
|
|
120
|
+
rawBody: rawPayload,
|
|
121
|
+
signature: typeof signature === "string" ? signature : undefined,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
function safeCompare(a: string, b: string): boolean {
|
|
102
126
|
if (a.length !== b.length) return false;
|
|
103
127
|
let mismatch = 0;
|
|
@@ -136,13 +160,15 @@ export async function verifyWebhookSignature(options: {
|
|
|
136
160
|
return safeCompare(expected, sig);
|
|
137
161
|
}
|
|
138
162
|
|
|
163
|
+
export type AssemblyResult = AssemblyStatusResults[string][number];
|
|
164
|
+
|
|
139
165
|
export type AssemblyResultRecord = {
|
|
140
166
|
stepName: string;
|
|
141
|
-
result:
|
|
167
|
+
result: AssemblyResult;
|
|
142
168
|
};
|
|
143
169
|
|
|
144
170
|
export function flattenResults(
|
|
145
|
-
results:
|
|
171
|
+
results: AssemblyStatusResults | undefined,
|
|
146
172
|
): AssemblyResultRecord[] {
|
|
147
173
|
if (!results) return [];
|
|
148
174
|
const output: AssemblyResultRecord[] = [];
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { createHmac } from "node:crypto";
|
|
4
4
|
import { convexTest } from "convex-test";
|
|
5
|
-
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { describe, expect, test, vi } from "vitest";
|
|
6
6
|
import { api } from "./_generated/api.js";
|
|
7
7
|
import schema from "./schema.js";
|
|
8
8
|
import { modules } from "./setup.test.js";
|
|
9
9
|
|
|
10
|
-
process.env.
|
|
11
|
-
process.env.
|
|
10
|
+
process.env.TRANSLOADIT_KEY = "test-key";
|
|
11
|
+
process.env.TRANSLOADIT_SECRET = "test-secret";
|
|
12
12
|
|
|
13
13
|
describe("Transloadit component lib", () => {
|
|
14
14
|
test("handleWebhook stores assembly and results", async () => {
|
|
@@ -60,4 +60,74 @@ describe("Transloadit component lib", () => {
|
|
|
60
60
|
expect(results).toHaveLength(1);
|
|
61
61
|
expect(results[0]?.stepName).toBe("resized");
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
test("refreshAssembly fetches status and stores results", async () => {
|
|
65
|
+
const t = convexTest(schema, modules);
|
|
66
|
+
|
|
67
|
+
const payload = {
|
|
68
|
+
assembly_id: "asm_456",
|
|
69
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
70
|
+
message: "Assembly complete",
|
|
71
|
+
results: {
|
|
72
|
+
resized: [
|
|
73
|
+
{
|
|
74
|
+
id: "file_2",
|
|
75
|
+
ssl_url: "https://example.com/file-2.jpg",
|
|
76
|
+
name: "file-2.jpg",
|
|
77
|
+
size: 54321,
|
|
78
|
+
mime: "image/jpeg",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const fetchMock = vi.fn<typeof fetch>(async () => {
|
|
85
|
+
return new Response(JSON.stringify(payload), {
|
|
86
|
+
status: 200,
|
|
87
|
+
headers: { "content-type": "application/json" },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = await t.action(api.lib.refreshAssembly, {
|
|
95
|
+
assemblyId: "asm_456",
|
|
96
|
+
config: { authKey: "test-key", authSecret: "test-secret" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result.assemblyId).toBe("asm_456");
|
|
100
|
+
expect(result.ok).toBe("ASSEMBLY_COMPLETED");
|
|
101
|
+
|
|
102
|
+
const requestInfo = fetchMock.mock.calls[0]?.[0];
|
|
103
|
+
const requestUrl =
|
|
104
|
+
typeof requestInfo === "string"
|
|
105
|
+
? requestInfo
|
|
106
|
+
: requestInfo instanceof URL
|
|
107
|
+
? requestInfo.toString()
|
|
108
|
+
: requestInfo instanceof Request
|
|
109
|
+
? requestInfo.url
|
|
110
|
+
: "";
|
|
111
|
+
if (!requestUrl) {
|
|
112
|
+
throw new Error("Expected fetch to be called with a URL string");
|
|
113
|
+
}
|
|
114
|
+
const url = new URL(requestUrl);
|
|
115
|
+
expect(url.origin).toBe("https://api2.transloadit.com");
|
|
116
|
+
expect(url.searchParams.get("signature")).toBeTruthy();
|
|
117
|
+
expect(url.searchParams.get("params")).toBeTruthy();
|
|
118
|
+
|
|
119
|
+
const assembly = await t.query(api.lib.getAssemblyStatus, {
|
|
120
|
+
assemblyId: "asm_456",
|
|
121
|
+
});
|
|
122
|
+
expect(assembly?.ok).toBe("ASSEMBLY_COMPLETED");
|
|
123
|
+
|
|
124
|
+
const results = await t.query(api.lib.listResults, {
|
|
125
|
+
assemblyId: "asm_456",
|
|
126
|
+
});
|
|
127
|
+
expect(results).toHaveLength(1);
|
|
128
|
+
expect(results[0]?.stepName).toBe("resized");
|
|
129
|
+
} finally {
|
|
130
|
+
vi.unstubAllGlobals();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
63
133
|
});
|