@kodelyth/tlon 2026.5.42 → 2026.6.1

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 (63) hide show
  1. package/klaw.plugin.json +203 -3
  2. package/package.json +17 -4
  3. package/api.ts +0 -16
  4. package/channel-plugin-api.ts +0 -1
  5. package/doctor-contract-api.ts +0 -1
  6. package/index.ts +0 -16
  7. package/runtime-api.ts +0 -17
  8. package/setup-api.ts +0 -2
  9. package/setup-entry.ts +0 -9
  10. package/src/account-fields.ts +0 -31
  11. package/src/channel.message-adapter.test.ts +0 -145
  12. package/src/channel.runtime.ts +0 -259
  13. package/src/channel.ts +0 -192
  14. package/src/config-schema.ts +0 -54
  15. package/src/core.test.ts +0 -298
  16. package/src/doctor-contract.ts +0 -9
  17. package/src/doctor.test.ts +0 -46
  18. package/src/doctor.ts +0 -10
  19. package/src/logger-runtime.ts +0 -1
  20. package/src/monitor/approval-runtime.ts +0 -363
  21. package/src/monitor/approval.test.ts +0 -33
  22. package/src/monitor/approval.ts +0 -283
  23. package/src/monitor/authorization.ts +0 -30
  24. package/src/monitor/cites.ts +0 -54
  25. package/src/monitor/discovery.ts +0 -68
  26. package/src/monitor/history.ts +0 -226
  27. package/src/monitor/index.ts +0 -1523
  28. package/src/monitor/media.test.ts +0 -80
  29. package/src/monitor/media.ts +0 -156
  30. package/src/monitor/processed-messages.test.ts +0 -58
  31. package/src/monitor/processed-messages.ts +0 -89
  32. package/src/monitor/settings-helpers.test.ts +0 -113
  33. package/src/monitor/settings-helpers.ts +0 -158
  34. package/src/monitor/utils.ts +0 -402
  35. package/src/runtime.ts +0 -9
  36. package/src/security.test.ts +0 -658
  37. package/src/session-route.ts +0 -40
  38. package/src/settings.ts +0 -391
  39. package/src/setup-core.ts +0 -231
  40. package/src/setup-surface.ts +0 -99
  41. package/src/targets.ts +0 -102
  42. package/src/tlon-api.test.ts +0 -572
  43. package/src/tlon-api.ts +0 -389
  44. package/src/types.ts +0 -160
  45. package/src/urbit/auth.ssrf.test.ts +0 -45
  46. package/src/urbit/auth.ts +0 -48
  47. package/src/urbit/base-url.test.ts +0 -48
  48. package/src/urbit/base-url.ts +0 -61
  49. package/src/urbit/channel-ops.test.ts +0 -36
  50. package/src/urbit/channel-ops.ts +0 -149
  51. package/src/urbit/context.ts +0 -50
  52. package/src/urbit/errors.ts +0 -51
  53. package/src/urbit/fetch.ts +0 -38
  54. package/src/urbit/foreigns.ts +0 -49
  55. package/src/urbit/send.test.ts +0 -83
  56. package/src/urbit/send.ts +0 -228
  57. package/src/urbit/sse-client.test.ts +0 -234
  58. package/src/urbit/sse-client.ts +0 -492
  59. package/src/urbit/story.ts +0 -332
  60. package/src/urbit/upload.test.ts +0 -155
  61. package/src/urbit/upload.ts +0 -60
  62. package/test-api.ts +0 -1
  63. package/tsconfig.json +0 -16
@@ -1,572 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { authenticate } from "./urbit/auth.js";
3
- import { scryUrbitPath } from "./urbit/channel-ops.js";
4
-
5
- const { mockFetchGuard, mockRelease, mockGetSignedUrl } = vi.hoisted(() => ({
6
- mockFetchGuard: vi.fn(),
7
- mockRelease: vi.fn(async () => {}),
8
- mockGetSignedUrl: vi.fn(),
9
- }));
10
-
11
- vi.mock("klaw/plugin-sdk/ssrf-runtime", async () => {
12
- const original = (await vi.importActual("klaw/plugin-sdk/ssrf-runtime")) as Record<
13
- string,
14
- unknown
15
- >;
16
- return {
17
- ...original,
18
- fetchWithSsrFGuard: mockFetchGuard,
19
- };
20
- });
21
-
22
- vi.mock("@aws-sdk/s3-request-presigner", () => ({
23
- getSignedUrl: mockGetSignedUrl,
24
- }));
25
-
26
- vi.mock("./urbit/auth.js", () => ({
27
- authenticate: vi.fn(),
28
- }));
29
-
30
- vi.mock("./urbit/channel-ops.js", () => ({
31
- scryUrbitPath: vi.fn(),
32
- }));
33
-
34
- import { fetchWithSsrFGuard } from "klaw/plugin-sdk/ssrf-runtime";
35
- import { configureClient, uploadFile } from "./tlon-api.js";
36
-
37
- const mockAuthenticate = vi.mocked(authenticate);
38
- const mockScryUrbitPath = vi.mocked(scryUrbitPath);
39
- const mockGuardedFetch = vi.mocked(fetchWithSsrFGuard);
40
-
41
- function createMemexResponse(
42
- uploadUrl: string,
43
- filePath = "https://memex.tlon.network/files/uploaded.png",
44
- ): Response {
45
- return new Response(
46
- JSON.stringify({
47
- url: uploadUrl,
48
- filePath,
49
- }),
50
- {
51
- status: 200,
52
- headers: { "content-type": "application/json" },
53
- },
54
- );
55
- }
56
-
57
- function createGuardedResult(response: Response, finalUrl: string) {
58
- return {
59
- response,
60
- finalUrl,
61
- release: mockRelease,
62
- };
63
- }
64
-
65
- function guardedFetchCall(index: number): Parameters<typeof fetchWithSsrFGuard>[0] {
66
- const call = mockGuardedFetch.mock.calls[index]?.at(0);
67
- if (call === undefined) {
68
- throw new Error(`expected guarded fetch call ${index}`);
69
- }
70
- return call;
71
- }
72
-
73
- describe("uploadFile memex upload hardening", () => {
74
- beforeEach(() => {
75
- vi.clearAllMocks();
76
- vi.stubGlobal("fetch", vi.fn());
77
- mockAuthenticate.mockResolvedValue("urbauth-~zod=fake-cookie");
78
- configureClient({
79
- shipUrl: "https://groups.tlon.network",
80
- shipName: "~zod",
81
- verbose: false,
82
- getCode: async () => "123456",
83
- });
84
- mockScryUrbitPath.mockImplementation(async (_deps, params) => {
85
- if (params.path === "/storage/configuration.json") {
86
- return {
87
- currentBucket: "uploads",
88
- buckets: ["uploads"],
89
- publicUrlBase: "https://files.tlon.network/",
90
- presignedUrl: "https://files.tlon.network/presigned",
91
- region: "us-east-1",
92
- service: "presigned-url",
93
- };
94
- }
95
- if (params.path === "/storage/credentials.json") {
96
- return { "storage-update": {} };
97
- }
98
- if (params.path === "/genuine/secret.json") {
99
- return { secret: "genuine-secret" };
100
- }
101
- throw new Error(`Unexpected scry path: ${params.path}`);
102
- });
103
- });
104
-
105
- afterEach(() => {
106
- vi.unstubAllGlobals();
107
- vi.restoreAllMocks();
108
- });
109
-
110
- it("routes the memex upload URL through the SSRF guard", async () => {
111
- mockGuardedFetch
112
- .mockResolvedValueOnce(
113
- createGuardedResult(
114
- createMemexResponse("https://uploads.tlon.network/put"),
115
- "https://memex.tlon.network/v1/zod/upload",
116
- ),
117
- )
118
- .mockResolvedValueOnce(
119
- createGuardedResult(
120
- new Response(null, { status: 200 }),
121
- "https://uploads.tlon.network/put",
122
- ),
123
- );
124
-
125
- const result = await uploadFile({
126
- blob: new Blob(["image-bytes"], { type: "image/png" }),
127
- fileName: "avatar.png",
128
- contentType: "image/png",
129
- });
130
-
131
- expect(result).toEqual({ url: "https://memex.tlon.network/files/uploaded.png" });
132
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
133
- expect(mockGuardedFetch).toHaveBeenCalledTimes(2);
134
- const firstCall = guardedFetchCall(0);
135
- expect(firstCall?.url).toBe("https://memex.tlon.network/v1/zod/upload");
136
- expect(firstCall?.init?.method).toBe("PUT");
137
- expect(firstCall?.init?.headers).toEqual({ "Content-Type": "application/json" });
138
- expect(firstCall?.auditContext).toBe("tlon-memex-upload-url");
139
- expect(firstCall?.capture).toBe(false);
140
- expect(firstCall?.maxRedirects).toBe(0);
141
- const firstBodyRaw = firstCall?.init?.body;
142
- expect(typeof firstBodyRaw).toBe("string");
143
- const firstBody = JSON.parse(firstBodyRaw as string) as Record<string, unknown>;
144
- expect(firstBody.token).toBe("genuine-secret");
145
- expect(firstBody.contentLength).toBe(11);
146
- expect(firstBody.contentType).toBe("image/png");
147
- expect(typeof firstBody.fileName).toBe("string");
148
- const secondCall = guardedFetchCall(1);
149
- expect(secondCall?.url).toBe("https://uploads.tlon.network/put");
150
- expect(secondCall?.init?.method).toBe("PUT");
151
- expect(secondCall?.init?.headers).toEqual({
152
- "Cache-Control": "public, max-age=3600",
153
- "Content-Type": "image/png",
154
- });
155
- expect(secondCall?.auditContext).toBe("tlon-memex-upload");
156
- expect(secondCall?.capture).toBe(false);
157
- expect(secondCall?.maxRedirects).toBe(0);
158
- expect(secondCall?.init?.body).toBeInstanceOf(Blob);
159
- expect(mockRelease).toHaveBeenCalledTimes(2);
160
- });
161
-
162
- it("surfaces guarded upload failures for hosted Memex targets", async () => {
163
- mockGuardedFetch
164
- .mockResolvedValueOnce(
165
- createGuardedResult(
166
- createMemexResponse("https://uploads.tlon.network/put"),
167
- "https://memex.tlon.network/v1/zod/upload",
168
- ),
169
- )
170
- .mockRejectedValueOnce(new Error("Blocked upload target"));
171
-
172
- await expect(
173
- uploadFile({
174
- blob: new Blob(["image-bytes"], { type: "image/png" }),
175
- fileName: "avatar.png",
176
- contentType: "image/png",
177
- }),
178
- ).rejects.toThrow("Blocked upload target");
179
-
180
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
181
- expect(mockGuardedFetch).toHaveBeenCalledTimes(2);
182
- const uploadCall = guardedFetchCall(1);
183
- expect(uploadCall?.url).toBe("https://uploads.tlon.network/put");
184
- expect(uploadCall?.auditContext).toBe("tlon-memex-upload");
185
- expect(uploadCall?.capture).toBe(false);
186
- expect(uploadCall?.maxRedirects).toBe(0);
187
- expect(mockRelease).toHaveBeenCalledTimes(1);
188
- });
189
-
190
- it("rejects Memex upload targets outside the hosted Tlon domain allowlist", async () => {
191
- mockGuardedFetch.mockResolvedValueOnce(
192
- createGuardedResult(
193
- createMemexResponse("https://eviltlon.network/upload"),
194
- "https://memex.tlon.network/v1/zod/upload",
195
- ),
196
- );
197
-
198
- await expect(
199
- uploadFile({
200
- blob: new Blob(["image-bytes"], { type: "image/png" }),
201
- fileName: "avatar.png",
202
- contentType: "image/png",
203
- }),
204
- ).rejects.toThrow("Memex upload URL must target a trusted hosted Tlon domain");
205
-
206
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
207
- expect(mockGuardedFetch).toHaveBeenCalledTimes(1);
208
- expect(mockRelease).toHaveBeenCalledTimes(1);
209
- });
210
-
211
- it("rejects Memex hosted result URLs outside the hosted Tlon domain allowlist", async () => {
212
- mockGuardedFetch
213
- .mockResolvedValueOnce(
214
- createGuardedResult(
215
- createMemexResponse(
216
- "https://uploads.tlon.network/put",
217
- "https://evil.example/files/uploaded.png",
218
- ),
219
- "https://memex.tlon.network/v1/zod/upload",
220
- ),
221
- )
222
- .mockResolvedValueOnce(
223
- createGuardedResult(
224
- new Response(null, { status: 200 }),
225
- "https://uploads.tlon.network/put",
226
- ),
227
- );
228
-
229
- await expect(
230
- uploadFile({
231
- blob: new Blob(["image-bytes"], { type: "image/png" }),
232
- fileName: "avatar.png",
233
- contentType: "image/png",
234
- }),
235
- ).rejects.toThrow("Memex hosted URL must target a trusted hosted Tlon domain");
236
-
237
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
238
- expect(mockGuardedFetch).toHaveBeenCalledTimes(2);
239
- expect(mockRelease).toHaveBeenCalledTimes(2);
240
- });
241
-
242
- it("rejects Memex upload targets with a non-standard port", async () => {
243
- mockGuardedFetch.mockResolvedValueOnce(
244
- createGuardedResult(
245
- createMemexResponse("https://uploads.tlon.network:8443/put"),
246
- "https://memex.tlon.network/v1/zod/upload",
247
- ),
248
- );
249
-
250
- await expect(
251
- uploadFile({
252
- blob: new Blob(["image-bytes"], { type: "image/png" }),
253
- fileName: "avatar.png",
254
- contentType: "image/png",
255
- }),
256
- ).rejects.toThrow("Memex upload URL must not specify a non-standard port");
257
-
258
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
259
- expect(mockGuardedFetch).toHaveBeenCalledTimes(1);
260
- expect(mockRelease).toHaveBeenCalledTimes(1);
261
- });
262
-
263
- it("disables redirects for Memex upload targets", async () => {
264
- mockGuardedFetch
265
- .mockResolvedValueOnce(
266
- createGuardedResult(
267
- createMemexResponse("https://uploads.tlon.network/put"),
268
- "https://memex.tlon.network/v1/zod/upload",
269
- ),
270
- )
271
- .mockRejectedValueOnce(new Error("Too many redirects (limit: 0)"));
272
-
273
- await expect(
274
- uploadFile({
275
- blob: new Blob(["image-bytes"], { type: "image/png" }),
276
- fileName: "avatar.png",
277
- contentType: "image/png",
278
- }),
279
- ).rejects.toThrow("Too many redirects (limit: 0)");
280
-
281
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
282
- expect(mockGuardedFetch).toHaveBeenCalledTimes(2);
283
- const uploadCall = guardedFetchCall(1);
284
- expect(uploadCall?.url).toBe("https://uploads.tlon.network/put");
285
- expect(uploadCall?.auditContext).toBe("tlon-memex-upload");
286
- expect(uploadCall?.capture).toBe(false);
287
- expect(uploadCall?.maxRedirects).toBe(0);
288
- expect(mockRelease).toHaveBeenCalledTimes(1);
289
- });
290
-
291
- it("routes scheme-less hosted ship URLs through the Memex upload path", async () => {
292
- configureClient({
293
- shipUrl: "foo.tlon.network",
294
- shipName: "~zod",
295
- verbose: false,
296
- getCode: async () => "123456",
297
- });
298
- mockGuardedFetch
299
- .mockResolvedValueOnce(
300
- createGuardedResult(
301
- createMemexResponse("https://uploads.tlon.network/put"),
302
- "https://memex.tlon.network/v1/zod/upload",
303
- ),
304
- )
305
- .mockResolvedValueOnce(
306
- createGuardedResult(
307
- new Response(null, { status: 200 }),
308
- "https://uploads.tlon.network/put",
309
- ),
310
- );
311
-
312
- const result = await uploadFile({
313
- blob: new Blob(["image-bytes"], { type: "image/png" }),
314
- fileName: "avatar.png",
315
- contentType: "image/png",
316
- });
317
-
318
- expect(result).toEqual({ url: "https://memex.tlon.network/files/uploaded.png" });
319
- expect(mockGuardedFetch).toHaveBeenCalledTimes(2);
320
- expect(mockRelease).toHaveBeenCalledTimes(2);
321
- });
322
-
323
- it("rejects truly unparseable ship URLs as not hosted", async () => {
324
- configureClient({
325
- shipUrl: " ",
326
- shipName: "~zod",
327
- verbose: false,
328
- getCode: async () => "123456",
329
- });
330
- mockScryUrbitPath.mockImplementation(async (_deps, params) => {
331
- if (params.path === "/storage/configuration.json") {
332
- return {
333
- currentBucket: "uploads",
334
- buckets: ["uploads"],
335
- publicUrlBase: "https://files.tlon.network/",
336
- presignedUrl: "https://files.tlon.network/presigned",
337
- region: "us-east-1",
338
- service: "presigned-url",
339
- };
340
- }
341
- if (params.path === "/storage/credentials.json") {
342
- return { "storage-update": {} };
343
- }
344
- throw new Error(`Unexpected scry path: ${params.path}`);
345
- });
346
-
347
- await expect(
348
- uploadFile({
349
- blob: new Blob(["image-bytes"], { type: "image/png" }),
350
- fileName: "avatar.png",
351
- contentType: "image/png",
352
- }),
353
- ).rejects.toThrow("No storage credentials configured");
354
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
355
- expect(mockGuardedFetch).not.toHaveBeenCalled();
356
- expect(mockRelease).not.toHaveBeenCalled();
357
- });
358
-
359
- it("accepts hosted Memex upload URLs with an explicit :443 port", async () => {
360
- mockGuardedFetch
361
- .mockResolvedValueOnce(
362
- createGuardedResult(
363
- createMemexResponse("https://uploads.tlon.network:443/put"),
364
- "https://memex.tlon.network/v1/zod/upload",
365
- ),
366
- )
367
- .mockResolvedValueOnce(
368
- createGuardedResult(
369
- new Response(null, { status: 200 }),
370
- "https://uploads.tlon.network:443/put",
371
- ),
372
- );
373
-
374
- const result = await uploadFile({
375
- blob: new Blob(["image-bytes"], { type: "image/png" }),
376
- fileName: "avatar.png",
377
- contentType: "image/png",
378
- });
379
-
380
- expect(result).toEqual({ url: "https://memex.tlon.network/files/uploaded.png" });
381
- expect(mockGuardedFetch).toHaveBeenCalledTimes(2);
382
- expect(mockRelease).toHaveBeenCalledTimes(2);
383
- });
384
-
385
- it("disables redirects for the Memex upload URL lookup", async () => {
386
- mockGuardedFetch.mockRejectedValueOnce(new Error("Too many redirects (limit: 0)"));
387
-
388
- await expect(
389
- uploadFile({
390
- blob: new Blob(["image-bytes"], { type: "image/png" }),
391
- fileName: "avatar.png",
392
- contentType: "image/png",
393
- }),
394
- ).rejects.toThrow("Too many redirects (limit: 0)");
395
-
396
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
397
- expect(mockGuardedFetch).toHaveBeenCalledTimes(1);
398
- const lookupCall = guardedFetchCall(0);
399
- expect(lookupCall?.url).toBe("https://memex.tlon.network/v1/zod/upload");
400
- expect(lookupCall?.auditContext).toBe("tlon-memex-upload-url");
401
- expect(lookupCall?.capture).toBe(false);
402
- expect(lookupCall?.maxRedirects).toBe(0);
403
- expect(mockRelease).not.toHaveBeenCalled();
404
- });
405
- });
406
-
407
- describe("uploadFile custom S3 upload hardening", () => {
408
- beforeEach(() => {
409
- vi.clearAllMocks();
410
- vi.stubGlobal("fetch", vi.fn());
411
- mockAuthenticate.mockResolvedValue("urbauth-~zod=fake-cookie");
412
- configureClient({
413
- shipUrl: "https://ship.example.com",
414
- shipName: "~zod",
415
- verbose: false,
416
- getCode: async () => "123456",
417
- });
418
- mockScryUrbitPath.mockImplementation(async (_deps, params) => {
419
- if (params.path === "/storage/configuration.json") {
420
- return {
421
- currentBucket: "uploads",
422
- buckets: ["uploads"],
423
- publicUrlBase: "https://files.example.com/",
424
- presignedUrl: "",
425
- region: "us-east-1",
426
- service: "custom",
427
- };
428
- }
429
- if (params.path === "/storage/credentials.json") {
430
- return {
431
- "storage-update": {
432
- credentials: {
433
- endpoint: "https://s3.example.com",
434
- accessKeyId: "AKIAFAKE",
435
- secretAccessKey: "fake-secret",
436
- },
437
- },
438
- };
439
- }
440
- throw new Error(`Unexpected scry path: ${params.path}`);
441
- });
442
- });
443
-
444
- afterEach(() => {
445
- vi.unstubAllGlobals();
446
- vi.restoreAllMocks();
447
- });
448
-
449
- it("routes the custom S3 signed URL through the SSRF guard", async () => {
450
- mockGetSignedUrl.mockResolvedValueOnce("https://s3.example.com/uploads/file?sig=abc");
451
- mockGuardedFetch.mockResolvedValueOnce(
452
- createGuardedResult(
453
- new Response(null, { status: 200 }),
454
- "https://s3.example.com/uploads/file?sig=abc",
455
- ),
456
- );
457
-
458
- const result = await uploadFile({
459
- blob: new Blob(["image-bytes"], { type: "image/png" }),
460
- fileName: "avatar.png",
461
- contentType: "image/png",
462
- });
463
-
464
- expect(result.url.startsWith("https://files.example.com/")).toBe(true);
465
- expect(mockGuardedFetch).toHaveBeenCalledTimes(1);
466
- const uploadCall = guardedFetchCall(0);
467
- expect(uploadCall?.url).toBe("https://s3.example.com/uploads/file?sig=abc");
468
- expect(uploadCall?.init?.method).toBe("PUT");
469
- expect(uploadCall?.init?.headers).toBeUndefined();
470
- expect(uploadCall?.auditContext).toBe("tlon-custom-s3-upload");
471
- expect(uploadCall?.capture).toBe(false);
472
- expect(uploadCall?.maxRedirects).toBe(0);
473
- expect(uploadCall?.policy).toBeUndefined();
474
- expect(mockRelease).toHaveBeenCalledTimes(1);
475
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
476
- });
477
-
478
- it("surfaces guarded upload failures for custom S3 targets without calling release", async () => {
479
- mockGetSignedUrl.mockResolvedValueOnce("https://169.254.169.254/uploads/file?sig=abc");
480
- mockGuardedFetch.mockRejectedValueOnce(new Error("Blocked private network target"));
481
-
482
- await expect(
483
- uploadFile({
484
- blob: new Blob(["image-bytes"], { type: "image/png" }),
485
- fileName: "avatar.png",
486
- contentType: "image/png",
487
- }),
488
- ).rejects.toThrow("Blocked private network target");
489
-
490
- expect(mockGuardedFetch).toHaveBeenCalledTimes(1);
491
- expect(mockRelease).not.toHaveBeenCalled();
492
- expect(vi.mocked(globalThis.fetch)).not.toHaveBeenCalled();
493
- });
494
-
495
- it("passes the private-network opt-in to guarded custom S3 uploads", async () => {
496
- configureClient({
497
- shipUrl: "https://ship.example.com",
498
- shipName: "~zod",
499
- verbose: false,
500
- getCode: async () => "123456",
501
- dangerouslyAllowPrivateNetwork: true,
502
- });
503
- mockGetSignedUrl.mockResolvedValueOnce("https://10.0.0.15/uploads/file?sig=abc");
504
- mockGuardedFetch.mockResolvedValueOnce(
505
- createGuardedResult(
506
- new Response(null, { status: 200 }),
507
- "https://10.0.0.15/uploads/file?sig=abc",
508
- ),
509
- );
510
-
511
- const result = await uploadFile({
512
- blob: new Blob(["image-bytes"], { type: "image/png" }),
513
- fileName: "avatar.png",
514
- contentType: "image/png",
515
- });
516
-
517
- expect(result.url.startsWith("https://files.example.com/")).toBe(true);
518
- expect(mockGuardedFetch).toHaveBeenCalledTimes(1);
519
- const uploadCall = guardedFetchCall(0);
520
- expect(uploadCall?.url).toBe("https://10.0.0.15/uploads/file?sig=abc");
521
- expect(uploadCall?.auditContext).toBe("tlon-custom-s3-upload");
522
- expect(uploadCall?.capture).toBe(false);
523
- expect(uploadCall?.maxRedirects).toBe(0);
524
- expect(uploadCall?.policy).toEqual({ allowPrivateNetwork: true });
525
- expect(mockRelease).toHaveBeenCalledTimes(1);
526
- });
527
-
528
- it("rejects custom S3 result URLs that are not http(s)", async () => {
529
- mockScryUrbitPath.mockImplementation(async (_deps, params) => {
530
- if (params.path === "/storage/configuration.json") {
531
- return {
532
- currentBucket: "uploads",
533
- buckets: ["uploads"],
534
- publicUrlBase: "ftp://files.example.com/",
535
- presignedUrl: "",
536
- region: "us-east-1",
537
- service: "custom",
538
- };
539
- }
540
- if (params.path === "/storage/credentials.json") {
541
- return {
542
- "storage-update": {
543
- credentials: {
544
- endpoint: "https://s3.example.com",
545
- accessKeyId: "AKIAFAKE",
546
- secretAccessKey: "fake-secret",
547
- },
548
- },
549
- };
550
- }
551
- throw new Error(`Unexpected scry path: ${params.path}`);
552
- });
553
- mockGetSignedUrl.mockResolvedValueOnce("https://s3.example.com/uploads/file?sig=abc");
554
- mockGuardedFetch.mockResolvedValueOnce(
555
- createGuardedResult(
556
- new Response(null, { status: 200 }),
557
- "https://s3.example.com/uploads/file?sig=abc",
558
- ),
559
- );
560
-
561
- await expect(
562
- uploadFile({
563
- blob: new Blob(["image-bytes"], { type: "image/png" }),
564
- fileName: "avatar.png",
565
- contentType: "image/png",
566
- }),
567
- ).rejects.toThrow("Upload result URL must use http or https");
568
-
569
- expect(mockGuardedFetch).toHaveBeenCalledTimes(1);
570
- expect(mockRelease).toHaveBeenCalledTimes(1);
571
- });
572
- });