create-100x-mobile 0.4.10 → 0.5.0

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.
@@ -0,0 +1,762 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.storageWorkerTemplate = storageWorkerTemplate;
4
+ function storageWorkerTemplate() {
5
+ return `import { init, i, type User } from "@instantdb/admin";
6
+ import type { storageApi } from "../../alchemy.run";
7
+
8
+ type Env = typeof storageApi.Env;
9
+
10
+ interface ErrorContext {
11
+ [key: string]: string | number | boolean;
12
+ }
13
+
14
+ interface StorageApiError extends Error {
15
+ name: "StorageApiError";
16
+ status: number;
17
+ context: ErrorContext;
18
+ }
19
+
20
+ interface UploadRecord {
21
+ id: string;
22
+ key: string;
23
+ ownerId: string;
24
+ fileName: string;
25
+ contentType: string;
26
+ size: number;
27
+ createdAt: string;
28
+ }
29
+
30
+ interface UploadRow {
31
+ id: string;
32
+ object_key: string;
33
+ owner_id: string;
34
+ file_name: string;
35
+ content_type: string;
36
+ size: number;
37
+ created_at: string;
38
+ }
39
+
40
+ interface UserStorageRow {
41
+ total_size: number | null;
42
+ }
43
+
44
+ interface UserDailyUploadRow {
45
+ upload_count: number | null;
46
+ }
47
+
48
+ interface UploadPolicy {
49
+ maxUploadBytes: number;
50
+ userStorageLimitBytes: number;
51
+ dailyUploadLimit: number;
52
+ allowedContentTypes: string[];
53
+ }
54
+
55
+ const defaultAllowedContentTypes = [
56
+ "image/jpeg",
57
+ "image/png",
58
+ "image/webp",
59
+ "application/pdf",
60
+ "text/plain",
61
+ ];
62
+ const defaultMaxUploadBytes = 25 * 1024 * 1024;
63
+ const defaultUserStorageLimitBytes = 500 * 1024 * 1024;
64
+ const defaultDailyUploadLimit = 100;
65
+ const localDevelopmentOrigins = [
66
+ "http://localhost:3000",
67
+ "http://localhost:8080",
68
+ "http://localhost:8081",
69
+ "http://localhost:19006",
70
+ ];
71
+ const instantSchema = i.schema({
72
+ entities: {
73
+ cloudflareObjects: i.entity({
74
+ uploadId: i.string().indexed(),
75
+ key: i.string(),
76
+ ownerId: i.string().indexed(),
77
+ fileName: i.string(),
78
+ contentType: i.string(),
79
+ size: i.number(),
80
+ workerUrl: i.string(),
81
+ createdAt: i.number().indexed(),
82
+ }),
83
+ },
84
+ });
85
+
86
+ function createStorageApiError(
87
+ status: number,
88
+ message: string,
89
+ context: ErrorContext,
90
+ ): StorageApiError {
91
+ return Object.assign(new Error(message), {
92
+ name: "StorageApiError" as const,
93
+ status,
94
+ context,
95
+ });
96
+ }
97
+
98
+ function isStorageApiError(error: unknown): error is StorageApiError {
99
+ return (
100
+ error instanceof Error &&
101
+ error.name === "StorageApiError" &&
102
+ "status" in error &&
103
+ "context" in error
104
+ );
105
+ }
106
+
107
+ function toErrorMessage(error: unknown): string {
108
+ if (error instanceof Error) {
109
+ return error.message;
110
+ }
111
+ return String(error);
112
+ }
113
+
114
+ function getCsvValues(value: string | undefined, fallback: string[]): string[] {
115
+ const values = (value ?? "")
116
+ .split(",")
117
+ .map((part) => part.trim())
118
+ .filter(Boolean);
119
+ return values.length ? values : fallback;
120
+ }
121
+
122
+ function getNumericBinding(
123
+ value: string | undefined,
124
+ fallback: number,
125
+ bindingName: string,
126
+ ): number {
127
+ if (!value) {
128
+ return fallback;
129
+ }
130
+
131
+ const parsed = Number(value);
132
+ if (!Number.isInteger(parsed) || parsed <= 0) {
133
+ throw createStorageApiError(500, "Numeric Worker binding is invalid.", {
134
+ binding: bindingName,
135
+ value,
136
+ });
137
+ }
138
+ return parsed;
139
+ }
140
+
141
+ function getUploadPolicy(env: Env): UploadPolicy {
142
+ return {
143
+ maxUploadBytes: getNumericBinding(
144
+ env.MAX_UPLOAD_BYTES,
145
+ defaultMaxUploadBytes,
146
+ "MAX_UPLOAD_BYTES",
147
+ ),
148
+ userStorageLimitBytes: getNumericBinding(
149
+ env.USER_STORAGE_LIMIT_BYTES,
150
+ defaultUserStorageLimitBytes,
151
+ "USER_STORAGE_LIMIT_BYTES",
152
+ ),
153
+ dailyUploadLimit: getNumericBinding(
154
+ env.DAILY_UPLOAD_LIMIT,
155
+ defaultDailyUploadLimit,
156
+ "DAILY_UPLOAD_LIMIT",
157
+ ),
158
+ allowedContentTypes: getCsvValues(
159
+ env.ALLOWED_CONTENT_TYPES,
160
+ defaultAllowedContentTypes,
161
+ ),
162
+ };
163
+ }
164
+
165
+ function getAllowedOrigins(env: Env): string[] {
166
+ return getCsvValues(env.ALLOWED_ORIGINS, localDevelopmentOrigins);
167
+ }
168
+
169
+ function getCorsHeaders(request: Request, env: Env): Headers {
170
+ const headers = new Headers({
171
+ "access-control-allow-methods": "GET,POST,DELETE,OPTIONS",
172
+ "access-control-allow-headers": "authorization,content-type,x-file-name",
173
+ "access-control-max-age": "86400",
174
+ "vary": "Origin",
175
+ "x-content-type-options": "nosniff",
176
+ });
177
+
178
+ const origin = request.headers.get("origin");
179
+ if (!origin) {
180
+ return headers;
181
+ }
182
+
183
+ if (getAllowedOrigins(env).includes(origin)) {
184
+ headers.set("access-control-allow-origin", origin);
185
+ return headers;
186
+ }
187
+
188
+ throw createStorageApiError(403, "Origin is not allowed.", {
189
+ origin,
190
+ });
191
+ }
192
+
193
+ function jsonResponse(
194
+ request: Request,
195
+ env: Env,
196
+ body: unknown,
197
+ status: number,
198
+ ): Response {
199
+ const headers = getCorsHeaders(request, env);
200
+ headers.set("cache-control", "no-store");
201
+ return Response.json(body, {
202
+ status,
203
+ headers,
204
+ });
205
+ }
206
+
207
+ function errorJsonResponse(
208
+ request: Request,
209
+ env: Env,
210
+ error: StorageApiError,
211
+ ): Response {
212
+ const headers = new Headers({
213
+ "cache-control": "no-store",
214
+ "content-type": "application/json",
215
+ "x-content-type-options": "nosniff",
216
+ });
217
+
218
+ const origin = request.headers.get("origin");
219
+ if (origin && getAllowedOrigins(env).includes(origin)) {
220
+ headers.set("access-control-allow-origin", origin);
221
+ headers.set("vary", "Origin");
222
+ }
223
+
224
+ return Response.json(
225
+ {
226
+ error: error.message,
227
+ context: error.context,
228
+ },
229
+ {
230
+ status: error.status,
231
+ headers,
232
+ },
233
+ );
234
+ }
235
+
236
+ function getRequiredHeader(headers: Headers, name: string): string {
237
+ const value = headers.get(name)?.trim();
238
+ if (!value) {
239
+ throw createStorageApiError(400, "Required header is missing.", {
240
+ header: name,
241
+ });
242
+ }
243
+ return value;
244
+ }
245
+
246
+ function getBearerToken(headers: Headers): string {
247
+ const authorization = headers.get("authorization")?.trim() ?? "";
248
+ const [scheme, token] = authorization.split(" ");
249
+ if (scheme?.toLowerCase() !== "bearer" || !token) {
250
+ throw createStorageApiError(401, "Bearer authorization token is required.", {
251
+ header: "authorization",
252
+ });
253
+ }
254
+ return token;
255
+ }
256
+
257
+ function requireInstantAppId(env: Env): string {
258
+ if (!env.INSTANT_APP_ID) {
259
+ throw createStorageApiError(500, "INSTANT_APP_ID binding is not configured.", {
260
+ binding: "INSTANT_APP_ID",
261
+ });
262
+ }
263
+ return env.INSTANT_APP_ID;
264
+ }
265
+
266
+ async function requireInstantUser(request: Request, env: Env): Promise<User> {
267
+ const appId = requireInstantAppId(env);
268
+ const token = getBearerToken(request.headers);
269
+ try {
270
+ const db = init({ appId });
271
+ return await db.auth.verifyToken(token);
272
+ } catch (error) {
273
+ throw createStorageApiError(401, "InstantDB auth token is invalid.", {
274
+ cause: toErrorMessage(error),
275
+ });
276
+ }
277
+ }
278
+
279
+ function requireInstantAdminToken(env: Env): string {
280
+ if (!env.INSTANT_ADMIN_TOKEN) {
281
+ throw createStorageApiError(
282
+ 500,
283
+ "INSTANT_ADMIN_TOKEN binding is not configured.",
284
+ {
285
+ binding: "INSTANT_ADMIN_TOKEN",
286
+ },
287
+ );
288
+ }
289
+ return env.INSTANT_ADMIN_TOKEN;
290
+ }
291
+
292
+ function getWorkerOrigin(request: Request): string {
293
+ const url = new URL(request.url);
294
+ return url.origin;
295
+ }
296
+
297
+ function getRequestBody(request: Request): ReadableStream<Uint8Array> {
298
+ if (!request.body) {
299
+ throw createStorageApiError(400, "Upload request body is required.", {
300
+ method: request.method,
301
+ });
302
+ }
303
+ return request.body;
304
+ }
305
+
306
+ function getContentLength(request: Request): number | null {
307
+ const rawContentLength = request.headers.get("content-length");
308
+ if (!rawContentLength) {
309
+ return null;
310
+ }
311
+
312
+ const contentLength = Number(rawContentLength);
313
+ if (!Number.isInteger(contentLength) || contentLength < 0) {
314
+ throw createStorageApiError(400, "Invalid content-length header.", {
315
+ contentLength: rawContentLength,
316
+ });
317
+ }
318
+ return contentLength;
319
+ }
320
+
321
+ function assertUploadSize(contentLength: number | null, policy: UploadPolicy): void {
322
+ if (contentLength !== null && contentLength > policy.maxUploadBytes) {
323
+ throw createStorageApiError(413, "Upload exceeds the configured size limit.", {
324
+ contentLength,
325
+ maxUploadBytes: policy.maxUploadBytes,
326
+ });
327
+ }
328
+ }
329
+
330
+ function assertContentType(contentType: string, policy: UploadPolicy): void {
331
+ const normalizedContentType = contentType.toLowerCase();
332
+ if (!policy.allowedContentTypes.includes(normalizedContentType)) {
333
+ throw createStorageApiError(415, "Content type is not allowed.", {
334
+ contentType,
335
+ });
336
+ }
337
+ }
338
+
339
+ function sanitizeFileName(fileName: string): string {
340
+ const normalized = fileName
341
+ .replace(/[\\\\/]/g, "-")
342
+ .replace(/[\\u0000-\\u001f\\u007f]/g, "")
343
+ .replace(/\\s+/g, " ")
344
+ .trim()
345
+ .slice(0, 160);
346
+ if (!normalized || normalized === "." || normalized === "..") {
347
+ throw createStorageApiError(400, "File name is invalid.", {
348
+ fileName,
349
+ });
350
+ }
351
+ return normalized;
352
+ }
353
+
354
+ function createLimitedBodyStream(
355
+ body: ReadableStream<Uint8Array>,
356
+ maxUploadBytes: number,
357
+ ): ReadableStream<Uint8Array> {
358
+ let totalBytes = 0;
359
+ return body.pipeThrough(
360
+ new TransformStream<Uint8Array, Uint8Array>({
361
+ transform(chunk, controller) {
362
+ totalBytes += chunk.byteLength;
363
+ if (totalBytes > maxUploadBytes) {
364
+ throw createStorageApiError(
365
+ 413,
366
+ "Upload stream exceeds the configured size limit.",
367
+ {
368
+ maxUploadBytes,
369
+ },
370
+ );
371
+ }
372
+ controller.enqueue(chunk);
373
+ },
374
+ }),
375
+ );
376
+ }
377
+
378
+ function assertUploadOwner(record: UploadRecord, user: User): void {
379
+ if (record.ownerId !== user.id) {
380
+ throw createStorageApiError(403, "Upload does not belong to the authenticated user.", {
381
+ uploadId: record.id,
382
+ ownerId: record.ownerId,
383
+ authenticatedUserId: user.id,
384
+ });
385
+ }
386
+ }
387
+
388
+ function toUploadRecord(row: UploadRow): UploadRecord {
389
+ return {
390
+ id: row.id,
391
+ key: row.object_key,
392
+ ownerId: row.owner_id,
393
+ fileName: row.file_name,
394
+ contentType: row.content_type,
395
+ size: row.size,
396
+ createdAt: row.created_at,
397
+ };
398
+ }
399
+
400
+ function toUploadResponse(request: Request, record: UploadRecord): UploadRecord & {
401
+ url: string;
402
+ } {
403
+ const url = new URL(request.url);
404
+ url.pathname = "/" + ["uploads", record.id, "content"].join("/");
405
+ url.search = "";
406
+ return {
407
+ ...record,
408
+ url: url.toString(),
409
+ };
410
+ }
411
+
412
+ function getDayStartIso(date: Date): string {
413
+ return new Date(
414
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
415
+ ).toISOString();
416
+ }
417
+
418
+ async function getUserStorageBytes(env: Env, ownerId: string): Promise<number> {
419
+ const row = await env.DB.prepare(
420
+ "SELECT COALESCE(SUM(size), 0) AS total_size FROM uploads WHERE owner_id = ?",
421
+ )
422
+ .bind(ownerId)
423
+ .first<UserStorageRow>();
424
+ return row?.total_size ?? 0;
425
+ }
426
+
427
+ async function getDailyUploadCount(
428
+ env: Env,
429
+ ownerId: string,
430
+ dayStartIso: string,
431
+ ): Promise<number> {
432
+ const row = await env.DB.prepare(
433
+ "SELECT COUNT(*) AS upload_count FROM uploads WHERE owner_id = ? AND created_at >= ?",
434
+ )
435
+ .bind(ownerId, dayStartIso)
436
+ .first<UserDailyUploadRow>();
437
+ return row?.upload_count ?? 0;
438
+ }
439
+
440
+ async function assertUserQuotaBeforeUpload(
441
+ env: Env,
442
+ ownerId: string,
443
+ contentLength: number | null,
444
+ policy: UploadPolicy,
445
+ ): Promise<void> {
446
+ const [currentStorageBytes, dailyUploadCount] = await Promise.all([
447
+ getUserStorageBytes(env, ownerId),
448
+ getDailyUploadCount(env, ownerId, getDayStartIso(new Date())),
449
+ ]);
450
+
451
+ if (dailyUploadCount >= policy.dailyUploadLimit) {
452
+ throw createStorageApiError(429, "Daily upload limit reached.", {
453
+ ownerId,
454
+ dailyUploadLimit: policy.dailyUploadLimit,
455
+ });
456
+ }
457
+
458
+ if (
459
+ contentLength !== null &&
460
+ currentStorageBytes + contentLength > policy.userStorageLimitBytes
461
+ ) {
462
+ throw createStorageApiError(413, "User storage quota would be exceeded.", {
463
+ ownerId,
464
+ currentStorageBytes,
465
+ contentLength,
466
+ userStorageLimitBytes: policy.userStorageLimitBytes,
467
+ });
468
+ }
469
+ }
470
+
471
+ async function assertUserQuotaAfterUpload(
472
+ env: Env,
473
+ ownerId: string,
474
+ objectSize: number,
475
+ policy: UploadPolicy,
476
+ ): Promise<void> {
477
+ const currentStorageBytes = await getUserStorageBytes(env, ownerId);
478
+ if (currentStorageBytes + objectSize > policy.userStorageLimitBytes) {
479
+ throw createStorageApiError(413, "User storage quota would be exceeded.", {
480
+ ownerId,
481
+ currentStorageBytes,
482
+ objectSize,
483
+ userStorageLimitBytes: policy.userStorageLimitBytes,
484
+ });
485
+ }
486
+ }
487
+
488
+ async function insertUploadRecord(
489
+ env: Env,
490
+ record: UploadRecord,
491
+ ): Promise<void> {
492
+ await env.DB.prepare(
493
+ "INSERT INTO uploads (id, object_key, owner_id, file_name, content_type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
494
+ )
495
+ .bind(
496
+ record.id,
497
+ record.key,
498
+ record.ownerId,
499
+ record.fileName,
500
+ record.contentType,
501
+ record.size,
502
+ record.createdAt,
503
+ )
504
+ .run();
505
+ }
506
+
507
+ async function deleteUploadRecord(env: Env, uploadId: string): Promise<void> {
508
+ await env.DB.prepare("DELETE FROM uploads WHERE id = ?").bind(uploadId).run();
509
+ }
510
+
511
+ async function syncInstantUploadRecord(
512
+ request: Request,
513
+ env: Env,
514
+ record: UploadRecord,
515
+ ): Promise<void> {
516
+ const appId = requireInstantAppId(env);
517
+ const adminToken = requireInstantAdminToken(env);
518
+ const db = init({
519
+ appId,
520
+ adminToken,
521
+ schema: instantSchema,
522
+ });
523
+
524
+ await db.transact(
525
+ db.tx.cloudflareObjects[record.id].create({
526
+ uploadId: record.id,
527
+ key: record.key,
528
+ ownerId: record.ownerId,
529
+ fileName: record.fileName,
530
+ contentType: record.contentType,
531
+ size: record.size,
532
+ workerUrl: getWorkerOrigin(request),
533
+ createdAt: Date.parse(record.createdAt),
534
+ }),
535
+ );
536
+ }
537
+
538
+ async function deleteInstantUploadRecord(
539
+ env: Env,
540
+ uploadId: string,
541
+ ): Promise<void> {
542
+ const appId = requireInstantAppId(env);
543
+ const adminToken = requireInstantAdminToken(env);
544
+ const db = init({
545
+ appId,
546
+ adminToken,
547
+ schema: instantSchema,
548
+ });
549
+
550
+ await db.transact(db.tx.cloudflareObjects[uploadId].delete());
551
+ }
552
+
553
+ async function findUploadRecord(env: Env, uploadId: string): Promise<UploadRecord> {
554
+ const row = await env.DB.prepare(
555
+ "SELECT id, object_key, owner_id, file_name, content_type, size, created_at FROM uploads WHERE id = ?",
556
+ )
557
+ .bind(uploadId)
558
+ .first<UploadRow>();
559
+
560
+ if (!row) {
561
+ throw createStorageApiError(404, "Upload record was not found.", {
562
+ uploadId,
563
+ });
564
+ }
565
+
566
+ return toUploadRecord(row);
567
+ }
568
+
569
+ async function handleUpload(
570
+ request: Request,
571
+ env: Env,
572
+ policy: UploadPolicy,
573
+ ): Promise<Response> {
574
+ const user = await requireInstantUser(request, env);
575
+ const fileName = sanitizeFileName(getRequiredHeader(request.headers, "x-file-name"));
576
+ const contentType = getRequiredHeader(request.headers, "content-type").toLowerCase();
577
+ assertContentType(contentType, policy);
578
+ const contentLength = getContentLength(request);
579
+ assertUploadSize(contentLength, policy);
580
+ await assertUserQuotaBeforeUpload(env, user.id, contentLength, policy);
581
+
582
+ const uploadId = crypto.randomUUID();
583
+ const objectKey = ["uploads", user.id, uploadId].join("/");
584
+ let object: R2Object;
585
+ try {
586
+ object = await env.STORAGE.put(
587
+ objectKey,
588
+ createLimitedBodyStream(getRequestBody(request), policy.maxUploadBytes),
589
+ {
590
+ httpMetadata: {
591
+ contentType,
592
+ },
593
+ customMetadata: {
594
+ fileName,
595
+ ownerId: user.id,
596
+ uploadId,
597
+ },
598
+ },
599
+ );
600
+ } catch (error) {
601
+ if (isStorageApiError(error)) {
602
+ throw error;
603
+ }
604
+ throw createStorageApiError(500, "Could not store upload object.", {
605
+ objectKey,
606
+ cause: toErrorMessage(error),
607
+ });
608
+ }
609
+
610
+ try {
611
+ await assertUserQuotaAfterUpload(env, user.id, object.size, policy);
612
+ const record: UploadRecord = {
613
+ id: uploadId,
614
+ key: objectKey,
615
+ ownerId: user.id,
616
+ fileName,
617
+ contentType,
618
+ size: object.size,
619
+ createdAt: new Date().toISOString(),
620
+ };
621
+ await insertUploadRecord(env, record);
622
+ await syncInstantUploadRecord(request, env, record);
623
+ return jsonResponse(request, env, toUploadResponse(request, record), 201);
624
+ } catch (error) {
625
+ await env.STORAGE.delete(objectKey);
626
+ await deleteUploadRecord(env, uploadId);
627
+ if (isStorageApiError(error)) {
628
+ throw error;
629
+ }
630
+ throw createStorageApiError(500, "Could not persist upload metadata.", {
631
+ objectKey,
632
+ cause: toErrorMessage(error),
633
+ });
634
+ }
635
+ }
636
+
637
+ async function handleGetUpload(
638
+ request: Request,
639
+ env: Env,
640
+ uploadId: string,
641
+ ): Promise<Response> {
642
+ const user = await requireInstantUser(request, env);
643
+ const record = await findUploadRecord(env, uploadId);
644
+ assertUploadOwner(record, user);
645
+ return jsonResponse(request, env, toUploadResponse(request, record), 200);
646
+ }
647
+
648
+ async function handleGetUploadContent(
649
+ request: Request,
650
+ env: Env,
651
+ uploadId: string,
652
+ ): Promise<Response> {
653
+ const user = await requireInstantUser(request, env);
654
+ const record = await findUploadRecord(env, uploadId);
655
+ assertUploadOwner(record, user);
656
+ const object = await env.STORAGE.get(record.key);
657
+ if (!object) {
658
+ throw createStorageApiError(404, "Stored object was not found.", {
659
+ uploadId,
660
+ objectKey: record.key,
661
+ });
662
+ }
663
+
664
+ const headers = getCorsHeaders(request, env);
665
+ object.writeHttpMetadata(headers);
666
+ headers.set("cache-control", "private, no-store");
667
+ headers.set("content-disposition", "inline; filename*=UTF-8''" + encodeURIComponent(record.fileName));
668
+ headers.set("etag", object.httpEtag);
669
+ return new Response(object.body, {
670
+ headers,
671
+ });
672
+ }
673
+
674
+ async function handleDeleteUpload(
675
+ request: Request,
676
+ env: Env,
677
+ uploadId: string,
678
+ ): Promise<Response> {
679
+ const user = await requireInstantUser(request, env);
680
+ const record = await findUploadRecord(env, uploadId);
681
+ assertUploadOwner(record, user);
682
+ await env.STORAGE.delete(record.key);
683
+ await deleteUploadRecord(env, uploadId);
684
+ await deleteInstantUploadRecord(env, uploadId);
685
+ return jsonResponse(request, env, { deleted: true, id: uploadId }, 200);
686
+ }
687
+
688
+ async function routeRequest(request: Request, env: Env): Promise<Response> {
689
+ if (request.method === "OPTIONS") {
690
+ return new Response(null, {
691
+ status: 204,
692
+ headers: getCorsHeaders(request, env),
693
+ });
694
+ }
695
+
696
+ const url = new URL(request.url);
697
+ const segments = url.pathname.split("/").filter(Boolean);
698
+ const policy = getUploadPolicy(env);
699
+
700
+ if (request.method === "GET" && url.pathname === "/health") {
701
+ return jsonResponse(request, env, { ok: true }, 200);
702
+ }
703
+
704
+ if (request.method === "POST" && segments.length === 1 && segments[0] === "uploads") {
705
+ return await handleUpload(request, env, policy);
706
+ }
707
+
708
+ if (segments.length === 2 && segments[0] === "uploads") {
709
+ if (request.method === "GET") {
710
+ return await handleGetUpload(request, env, segments[1]);
711
+ }
712
+ if (request.method === "DELETE") {
713
+ return await handleDeleteUpload(request, env, segments[1]);
714
+ }
715
+ }
716
+
717
+ if (
718
+ request.method === "GET" &&
719
+ segments.length === 3 &&
720
+ segments[0] === "uploads" &&
721
+ segments[2] === "content"
722
+ ) {
723
+ return await handleGetUploadContent(request, env, segments[1]);
724
+ }
725
+
726
+ throw createStorageApiError(404, "Route was not found.", {
727
+ method: request.method,
728
+ path: url.pathname,
729
+ });
730
+ }
731
+
732
+ export default {
733
+ async fetch(request: Request, env: Env): Promise<Response> {
734
+ try {
735
+ return await routeRequest(request, env);
736
+ } catch (error) {
737
+ if (isStorageApiError(error)) {
738
+ console.warn(
739
+ JSON.stringify({
740
+ level: "warn",
741
+ event: "storage_api_error",
742
+ message: error.message,
743
+ status: error.status,
744
+ context: error.context,
745
+ }),
746
+ );
747
+ return errorJsonResponse(request, env, error);
748
+ }
749
+
750
+ console.error(
751
+ JSON.stringify({
752
+ level: "error",
753
+ event: "unhandled_storage_api_error",
754
+ message: toErrorMessage(error),
755
+ }),
756
+ );
757
+ throw error;
758
+ }
759
+ },
760
+ } satisfies ExportedHandler<Env>;
761
+ `;
762
+ }