@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.
Files changed (81) hide show
  1. package/README.md +114 -134
  2. package/dist/client/index.d.ts +24 -13
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +14 -3
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -2
  7. package/dist/component/_generated/dataModel.d.ts +1 -1
  8. package/dist/component/_generated/server.d.ts +1 -1
  9. package/dist/component/apiUtils.d.ts +26 -6
  10. package/dist/component/apiUtils.d.ts.map +1 -1
  11. package/dist/component/apiUtils.js +48 -38
  12. package/dist/component/apiUtils.js.map +1 -1
  13. package/dist/component/lib.d.ts +7 -9
  14. package/dist/component/lib.d.ts.map +1 -1
  15. package/dist/component/lib.js +74 -18
  16. package/dist/component/lib.js.map +1 -1
  17. package/dist/component/schema.d.ts +4 -6
  18. package/dist/component/schema.d.ts.map +1 -1
  19. package/dist/component/schema.js +0 -7
  20. package/dist/component/schema.js.map +1 -1
  21. package/dist/debug/index.d.ts +19 -0
  22. package/dist/debug/index.d.ts.map +1 -0
  23. package/dist/debug/index.js +49 -0
  24. package/dist/debug/index.js.map +1 -0
  25. package/dist/react/index.d.ts +201 -3
  26. package/dist/react/index.d.ts.map +1 -1
  27. package/dist/react/index.js +674 -94
  28. package/dist/react/index.js.map +1 -1
  29. package/dist/shared/assemblyUrls.d.ts +10 -0
  30. package/dist/shared/assemblyUrls.d.ts.map +1 -0
  31. package/dist/shared/assemblyUrls.js +26 -0
  32. package/dist/shared/assemblyUrls.js.map +1 -0
  33. package/dist/shared/errors.d.ts +7 -0
  34. package/dist/shared/errors.d.ts.map +1 -0
  35. package/dist/shared/errors.js +10 -0
  36. package/dist/shared/errors.js.map +1 -0
  37. package/dist/shared/pollAssembly.d.ts +12 -0
  38. package/dist/shared/pollAssembly.d.ts.map +1 -0
  39. package/dist/shared/pollAssembly.js +50 -0
  40. package/dist/shared/pollAssembly.js.map +1 -0
  41. package/dist/shared/resultTypes.d.ts +37 -0
  42. package/dist/shared/resultTypes.d.ts.map +1 -0
  43. package/dist/shared/resultTypes.js +2 -0
  44. package/dist/shared/resultTypes.js.map +1 -0
  45. package/dist/shared/resultUtils.d.ts +4 -0
  46. package/dist/shared/resultUtils.d.ts.map +1 -0
  47. package/dist/shared/resultUtils.js +69 -0
  48. package/dist/shared/resultUtils.js.map +1 -0
  49. package/dist/shared/tusUpload.d.ts +13 -0
  50. package/dist/shared/tusUpload.d.ts.map +1 -0
  51. package/dist/shared/tusUpload.js +32 -0
  52. package/dist/shared/tusUpload.js.map +1 -0
  53. package/dist/test/index.d.ts +4 -4
  54. package/dist/test/nodeModules.d.ts +2 -0
  55. package/dist/test/nodeModules.d.ts.map +1 -0
  56. package/dist/test/nodeModules.js +19 -0
  57. package/dist/test/nodeModules.js.map +1 -0
  58. package/package.json +36 -6
  59. package/src/client/index.ts +73 -7
  60. package/src/component/_generated/api.ts +2 -2
  61. package/src/component/_generated/dataModel.ts +1 -1
  62. package/src/component/_generated/server.ts +1 -1
  63. package/src/component/apiUtils.test.ts +166 -2
  64. package/src/component/apiUtils.ts +96 -64
  65. package/src/component/lib.test.ts +170 -4
  66. package/src/component/lib.ts +113 -25
  67. package/src/component/schema.ts +0 -10
  68. package/src/debug/index.ts +84 -0
  69. package/src/react/index.test.tsx +340 -0
  70. package/src/react/index.tsx +1089 -179
  71. package/src/react/uploadWithTus.test.tsx +192 -0
  72. package/src/shared/assemblyUrls.test.ts +71 -0
  73. package/src/shared/assemblyUrls.ts +59 -0
  74. package/src/shared/errors.ts +23 -0
  75. package/src/shared/pollAssembly.ts +65 -0
  76. package/src/shared/resultTypes.ts +44 -0
  77. package/src/shared/resultUtils.test.ts +29 -0
  78. package/src/shared/resultUtils.ts +71 -0
  79. package/src/shared/tusUpload.ts +59 -0
  80. package/src/test/index.ts +1 -1
  81. 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.js";
7
- import schema from "./schema.js";
8
- import { modules } from "./setup.test.js";
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
 
@@ -1,20 +1,23 @@
1
- import type { AssemblyStatus } from "@transloadit/types/assemblyStatus";
2
- import type { AssemblyInstructionsInput } from "@transloadit/types/template";
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.js";
14
+ } from "./_generated/server.ts";
12
15
  import {
13
16
  buildTransloaditParams,
14
17
  flattenResults,
15
18
  signTransloaditParams,
16
19
  verifyWebhookSignature,
17
- } from "./apiUtils.js";
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.js").FunctionCtx, "runMutation">,
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 new Error("Webhook payload missing assembly_id");
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: typeof raw.ssl_url === "string" ? raw.ssl_url : undefined,
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 new Error(
322
- `Transloadit error ${response.status}: ${JSON.stringify(data)}`,
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 new Error("Transloadit response missing assembly_id");
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 = args.rawBody ?? JSON.stringify(args.payload ?? {});
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 new Error("Missing TRANSLOADIT_SECRET for webhook validation");
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 new Error("Invalid Transloadit webhook signature");
439
+ throw transloaditError(
440
+ "webhook",
441
+ "Invalid Transloadit webhook signature",
442
+ );
389
443
  }
390
444
  }
391
445
 
392
- return applyAssemblyStatus(ctx, args.payload as AssemblyStatus);
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
- ...vWebhookArgs,
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: args.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
- ...vWebhookArgs,
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 payload = args.payload as AssemblyStatus;
437
- const assemblyId = resolveAssemblyId(payload);
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 new Error("Webhook payload missing assembly_id");
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: args.payload,
530
+ payload: parsed,
444
531
  rawBody: args.rawBody,
445
532
  signature: args.signature,
446
- verifySignature: args.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()) as AssemblyStatus;
568
+ const payload = parseAssemblyPayload(await response.json());
482
569
  if (!response.ok) {
483
- throw new Error(
484
- `Transloadit status error ${response.status}: ${JSON.stringify(payload)}`,
570
+ throw transloaditError(
571
+ "status",
572
+ `HTTP ${response.status}: ${JSON.stringify(payload)}`,
485
573
  );
486
574
  }
487
575
 
@@ -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
+ };