@useupup/server 3.3.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,2365 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+
23
+ // src/providers/s3-client.ts
24
+ function buildS3ClientConfig(storage) {
25
+ const config = { region: storage.region };
26
+ if (storage.accessKeyId && storage.secretAccessKey) {
27
+ config.credentials = {
28
+ accessKeyId: storage.accessKeyId,
29
+ secretAccessKey: storage.secretAccessKey
30
+ };
31
+ }
32
+ if (storage.endpoint) {
33
+ config.endpoint = storage.endpoint;
34
+ config.forcePathStyle = storage.forcePathStyle ?? true;
35
+ }
36
+ return config;
37
+ }
38
+ function cacheKey(storage) {
39
+ return [
40
+ storage.endpoint ?? "",
41
+ storage.region,
42
+ storage.bucket,
43
+ storage.accessKeyId ?? "",
44
+ String(storage.forcePathStyle ?? "")
45
+ ].join("\0");
46
+ }
47
+ function createS3Client(storage) {
48
+ const key = cacheKey(storage);
49
+ const cached = clients.get(key);
50
+ if (cached) return cached;
51
+ const client = new import_client_s3.S3Client(buildS3ClientConfig(storage));
52
+ clients.set(key, client);
53
+ return client;
54
+ }
55
+ var import_client_s3, clients;
56
+ var init_s3_client = __esm({
57
+ "src/providers/s3-client.ts"() {
58
+ "use strict";
59
+ import_client_s3 = require("@aws-sdk/client-s3");
60
+ clients = /* @__PURE__ */ new Map();
61
+ }
62
+ });
63
+
64
+ // src/providers/aws.ts
65
+ function computePartSize(fileSize, chunkSizeBytes) {
66
+ let partSize = chunkSizeBytes ?? MIN_PART_SIZE;
67
+ if (partSize < MIN_PART_SIZE) partSize = MIN_PART_SIZE;
68
+ const minPartSizeForFile = Math.ceil(fileSize / MAX_PARTS);
69
+ if (partSize < minPartSizeForFile) partSize = minPartSizeForFile;
70
+ return partSize;
71
+ }
72
+ async function generateSignedPublicUrl(storage, key, expiresIn = DEFAULT_DOWNLOAD_URL_EXPIRES_IN) {
73
+ const client = createS3Client(storage);
74
+ return (0, import_s3_request_presigner.getSignedUrl)(
75
+ client,
76
+ new import_client_s32.GetObjectCommand({ Bucket: storage.bucket, Key: key }),
77
+ { expiresIn }
78
+ );
79
+ }
80
+ async function generatePresignedUrl(storage, key, contentType, contentLength, expiresIn = DEFAULT_EXPIRES_IN, downloadUrlExpiresIn) {
81
+ const client = createS3Client(storage);
82
+ const command = new import_client_s32.PutObjectCommand({
83
+ Bucket: storage.bucket,
84
+ Key: key,
85
+ ContentType: contentType,
86
+ ContentLength: contentLength
87
+ });
88
+ const uploadUrl = await (0, import_s3_request_presigner.getSignedUrl)(client, command, {
89
+ expiresIn,
90
+ // Bind content-length into the signature so the PUT body cannot exceed the
91
+ // approved size (S1). Browsers/Node set Content-Length from the body; a
92
+ // larger body changes it and S3 rejects the signature.
93
+ signableHeaders: /* @__PURE__ */ new Set(["content-type", "content-length"])
94
+ });
95
+ const downloadUrl = await generateSignedPublicUrl(
96
+ storage,
97
+ key,
98
+ downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN
99
+ );
100
+ return {
101
+ key,
102
+ downloadUrl,
103
+ uploadUrl,
104
+ uploadHeaders: {
105
+ "Content-Type": contentType || "application/octet-stream"
106
+ },
107
+ expiresIn
108
+ };
109
+ }
110
+ async function initiateMultipartUpload(storage, key, contentType, fileSize, expiresIn = DEFAULT_EXPIRES_IN, chunkSizeBytes) {
111
+ const client = createS3Client(storage);
112
+ const command = new import_client_s32.CreateMultipartUploadCommand({
113
+ Bucket: storage.bucket,
114
+ Key: key,
115
+ ContentType: contentType
116
+ });
117
+ const response = await client.send(command);
118
+ if (!response.UploadId) {
119
+ throw new import_core5.UpupStorageError(
120
+ "Failed to initiate multipart upload: no UploadId",
121
+ storage.type,
122
+ "multipart-init"
123
+ );
124
+ }
125
+ const partSize = computePartSize(fileSize, chunkSizeBytes);
126
+ return { key, uploadId: response.UploadId, partSize, expiresIn };
127
+ }
128
+ async function generatePresignedPartUrl(storage, key, uploadId, partNumber, expiresIn = DEFAULT_EXPIRES_IN) {
129
+ const client = createS3Client(storage);
130
+ const command = new import_client_s32.UploadPartCommand({
131
+ Bucket: storage.bucket,
132
+ Key: key,
133
+ UploadId: uploadId,
134
+ PartNumber: partNumber
135
+ });
136
+ const uploadUrl = await (0, import_s3_request_presigner.getSignedUrl)(client, command, { expiresIn });
137
+ return { uploadUrl, expiresIn };
138
+ }
139
+ async function completeMultipartUpload(storage, key, uploadId, parts, downloadUrlExpiresIn) {
140
+ const client = createS3Client(storage);
141
+ const command = new import_client_s32.CompleteMultipartUploadCommand({
142
+ Bucket: storage.bucket,
143
+ Key: key,
144
+ UploadId: uploadId,
145
+ MultipartUpload: {
146
+ Parts: parts.sort((a, b) => a.partNumber - b.partNumber).map((p) => ({ PartNumber: p.partNumber, ETag: p.eTag }))
147
+ }
148
+ });
149
+ const result = await client.send(command);
150
+ const downloadUrl = await generateSignedPublicUrl(
151
+ storage,
152
+ key,
153
+ downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN
154
+ );
155
+ return {
156
+ key,
157
+ downloadUrl,
158
+ ...result.ETag !== void 0 ? { etag: result.ETag } : {}
159
+ };
160
+ }
161
+ async function abortMultipartUpload(storage, key, uploadId) {
162
+ const client = createS3Client(storage);
163
+ const command = new import_client_s32.AbortMultipartUploadCommand({
164
+ Bucket: storage.bucket,
165
+ Key: key,
166
+ UploadId: uploadId
167
+ });
168
+ await client.send(command);
169
+ return { ok: true };
170
+ }
171
+ async function listMultipartParts(storage, key, uploadId) {
172
+ const client = createS3Client(storage);
173
+ const parts = [];
174
+ let partNumberMarker;
175
+ for (; ; ) {
176
+ const command = new import_client_s32.ListPartsCommand({
177
+ Bucket: storage.bucket,
178
+ Key: key,
179
+ UploadId: uploadId,
180
+ PartNumberMarker: partNumberMarker
181
+ });
182
+ const response = await client.send(command);
183
+ if (response.Parts) {
184
+ for (const part of response.Parts) {
185
+ if (part.PartNumber != null && part.ETag) {
186
+ parts.push({
187
+ partNumber: part.PartNumber,
188
+ eTag: part.ETag,
189
+ ...part.Size !== void 0 ? { size: part.Size } : {}
190
+ });
191
+ }
192
+ }
193
+ }
194
+ if (!response.IsTruncated) break;
195
+ partNumberMarker = String(
196
+ response.Parts?.[response.Parts.length - 1]?.PartNumber
197
+ );
198
+ }
199
+ return { parts };
200
+ }
201
+ async function getMultipartUploadedSize(storage, key, uploadId) {
202
+ const client = createS3Client(storage);
203
+ let total = 0;
204
+ let partNumberMarker;
205
+ for (; ; ) {
206
+ const command = new import_client_s32.ListPartsCommand({
207
+ Bucket: storage.bucket,
208
+ Key: key,
209
+ UploadId: uploadId,
210
+ PartNumberMarker: partNumberMarker
211
+ });
212
+ const response = await client.send(command);
213
+ if (response.Parts) {
214
+ for (const part of response.Parts) {
215
+ total += part.Size ?? 0;
216
+ }
217
+ }
218
+ if (!response.IsTruncated) break;
219
+ partNumberMarker = String(
220
+ response.Parts?.[response.Parts.length - 1]?.PartNumber
221
+ );
222
+ }
223
+ return total;
224
+ }
225
+ async function checkStorageReachable(storage) {
226
+ try {
227
+ const client = createS3Client(storage);
228
+ await client.send(new import_client_s32.HeadBucketCommand({ Bucket: storage.bucket }));
229
+ return { ok: true };
230
+ } catch (error) {
231
+ return { ok: false, error };
232
+ }
233
+ }
234
+ var import_client_s32, import_s3_request_presigner, import_core5, DEFAULT_EXPIRES_IN, DEFAULT_DOWNLOAD_URL_EXPIRES_IN, MIN_PART_SIZE, MAX_PARTS;
235
+ var init_aws = __esm({
236
+ "src/providers/aws.ts"() {
237
+ "use strict";
238
+ import_client_s32 = require("@aws-sdk/client-s3");
239
+ import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
240
+ import_core5 = require("@useupup/core");
241
+ init_s3_client();
242
+ DEFAULT_EXPIRES_IN = 3600;
243
+ DEFAULT_DOWNLOAD_URL_EXPIRES_IN = 3600 * 24 * 3;
244
+ MIN_PART_SIZE = 5 * 1024 * 1024;
245
+ MAX_PARTS = 1e4;
246
+ }
247
+ });
248
+
249
+ // src/observability.ts
250
+ function toSafeError(e) {
251
+ return e instanceof Error ? { name: e.name, message: e.message, stack: e.stack } : { name: "NonError", message: String(e) };
252
+ }
253
+ function scrubSensitive(text) {
254
+ return SCRUBBERS.reduce((acc, [re, repl]) => acc.replace(re, repl), text);
255
+ }
256
+ function reportServerError(logger, event) {
257
+ const scrubbed = {
258
+ ...event,
259
+ message: scrubSensitive(event.message)
260
+ };
261
+ if (event.error) {
262
+ scrubbed.error = {
263
+ name: event.error.name,
264
+ message: scrubSensitive(event.error.message),
265
+ stack: event.error.stack ? scrubSensitive(event.error.stack) : void 0
266
+ };
267
+ }
268
+ ;
269
+ (logger ?? defaultLogger)(scrubbed);
270
+ }
271
+ var SCRUBBERS, defaultLogger;
272
+ var init_observability = __esm({
273
+ "src/observability.ts"() {
274
+ "use strict";
275
+ SCRUBBERS = [
276
+ // `Authorization: Bearer xxx` / `authorization=xxx` header dumps.
277
+ [/(authorization)(\s*[:=]\s*)(?:bearer\s+)?[^\s,;"']+/gi, "$1$2[REDACTED]"],
278
+ // Standalone bearer tokens not preceded by the header name.
279
+ [/\bbearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]"],
280
+ // SigV4 signature / credential / security-token query params or headers.
281
+ [
282
+ /(x-amz-(?:signature|credential|security-token))(\s*[:=]\s*)[^\s&,;"']+/gi,
283
+ "$1$2[REDACTED]"
284
+ ],
285
+ // AWS access-key ids.
286
+ [/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]"]
287
+ ];
288
+ defaultLogger = (event) => {
289
+ console.error("[upup:server]", JSON.stringify(event));
290
+ };
291
+ }
292
+ });
293
+
294
+ // src/transfer.ts
295
+ var transfer_exports = {};
296
+ __export(transfer_exports, {
297
+ transferDriveFileToS3: () => transferDriveFileToS3
298
+ });
299
+ async function transferDriveFileToS3(opts) {
300
+ const key = `${crypto.randomUUID()}-${opts.fileName}`;
301
+ if (opts.size > 0 && opts.size <= SINGLE_PUT_MAX_BYTES) {
302
+ return singlePut({ ...opts, key });
303
+ }
304
+ return streamingMultipart({ ...opts, key });
305
+ }
306
+ async function singlePut(opts) {
307
+ const buffer = await streamToUint8Array(opts.stream);
308
+ if (opts.maxBytes !== void 0 && buffer.byteLength > opts.maxBytes) {
309
+ throw new import_core10.UpupStorageError(
310
+ `Drive file exceeds the configured maxFileSize (${buffer.byteLength} > ${opts.maxBytes} bytes)`,
311
+ opts.storage.type,
312
+ "upload"
313
+ );
314
+ }
315
+ const client = createS3Client(opts.storage);
316
+ await client.send(
317
+ new import_client_s33.PutObjectCommand({
318
+ Bucket: opts.storage.bucket,
319
+ Key: opts.key,
320
+ ContentType: opts.mimeType,
321
+ Body: buffer
322
+ })
323
+ );
324
+ const url = await generateSignedPublicUrl(
325
+ opts.storage,
326
+ opts.key,
327
+ opts.downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN
328
+ );
329
+ return {
330
+ key: opts.key,
331
+ name: opts.fileName,
332
+ size: buffer.byteLength,
333
+ type: opts.mimeType,
334
+ url
335
+ };
336
+ }
337
+ async function streamingMultipart(opts) {
338
+ const client = createS3Client(opts.storage);
339
+ const init = await client.send(
340
+ new import_client_s33.CreateMultipartUploadCommand({
341
+ Bucket: opts.storage.bucket,
342
+ Key: opts.key,
343
+ ContentType: opts.mimeType
344
+ })
345
+ );
346
+ const uploadId = init.UploadId;
347
+ if (!uploadId)
348
+ throw new import_core10.UpupStorageError(
349
+ "Missing UploadId on multipart init",
350
+ opts.storage.type,
351
+ "multipart-init"
352
+ );
353
+ const parts = [];
354
+ let totalBytes = 0;
355
+ try {
356
+ let partNumber = 1;
357
+ for await (const chunk of chunkedStream(opts.stream, MIN_PART_SIZE)) {
358
+ const res = await client.send(
359
+ new import_client_s33.UploadPartCommand({
360
+ Bucket: opts.storage.bucket,
361
+ Key: opts.key,
362
+ UploadId: uploadId,
363
+ PartNumber: partNumber,
364
+ ContentLength: chunk.byteLength,
365
+ Body: chunk
366
+ })
367
+ );
368
+ if (!res.ETag) {
369
+ throw new import_core10.UpupStorageError(
370
+ `Missing ETag for part ${partNumber}`,
371
+ opts.storage.type,
372
+ "multipart-sign-part"
373
+ );
374
+ }
375
+ parts.push({ PartNumber: partNumber, ETag: res.ETag });
376
+ totalBytes += chunk.byteLength;
377
+ if (opts.maxBytes !== void 0 && totalBytes > opts.maxBytes) {
378
+ throw new import_core10.UpupStorageError(
379
+ `Drive file exceeds the configured maxFileSize (${totalBytes} > ${opts.maxBytes} bytes)`,
380
+ opts.storage.type,
381
+ "upload"
382
+ );
383
+ }
384
+ partNumber++;
385
+ }
386
+ if (parts.length === 0) {
387
+ throw new import_core10.UpupStorageError(
388
+ "Drive download produced no bytes",
389
+ opts.storage.type,
390
+ "upload"
391
+ );
392
+ }
393
+ await client.send(
394
+ new import_client_s33.CompleteMultipartUploadCommand({
395
+ Bucket: opts.storage.bucket,
396
+ Key: opts.key,
397
+ UploadId: uploadId,
398
+ MultipartUpload: { Parts: parts }
399
+ })
400
+ );
401
+ } catch (err) {
402
+ try {
403
+ await client.send(
404
+ new import_client_s33.AbortMultipartUploadCommand({
405
+ Bucket: opts.storage.bucket,
406
+ Key: opts.key,
407
+ UploadId: uploadId
408
+ })
409
+ );
410
+ } catch (abortErr) {
411
+ reportServerError(opts.onError, {
412
+ route: "files/transfer",
413
+ method: "POST",
414
+ status: 500,
415
+ code: import_core10.UpupErrorCode.STORAGE_ERROR,
416
+ message: `Failed to abort multipart upload after a transfer error (uploadId=${uploadId}, key=${opts.key})`,
417
+ requestId: opts.requestId,
418
+ error: toSafeError(abortErr)
419
+ });
420
+ }
421
+ throw err;
422
+ }
423
+ const url = await generateSignedPublicUrl(
424
+ opts.storage,
425
+ opts.key,
426
+ opts.downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN
427
+ );
428
+ return {
429
+ key: opts.key,
430
+ name: opts.fileName,
431
+ size: totalBytes,
432
+ type: opts.mimeType,
433
+ url
434
+ };
435
+ }
436
+ async function streamToUint8Array(stream) {
437
+ const reader = stream.getReader();
438
+ const chunks = [];
439
+ let total = 0;
440
+ for (; ; ) {
441
+ const { value, done } = await reader.read();
442
+ if (done) break;
443
+ chunks.push(value);
444
+ total += value.byteLength;
445
+ }
446
+ const out = new Uint8Array(total);
447
+ let offset = 0;
448
+ for (const c of chunks) {
449
+ out.set(c, offset);
450
+ offset += c.byteLength;
451
+ }
452
+ return out;
453
+ }
454
+ async function* chunkedStream(stream, chunkSize) {
455
+ const reader = stream.getReader();
456
+ let buffered = [];
457
+ let bufferedSize = 0;
458
+ const flush = () => {
459
+ const out = new Uint8Array(bufferedSize);
460
+ let offset = 0;
461
+ for (const c of buffered) {
462
+ out.set(c, offset);
463
+ offset += c.byteLength;
464
+ }
465
+ buffered = [];
466
+ bufferedSize = 0;
467
+ return out;
468
+ };
469
+ for (; ; ) {
470
+ const { value, done } = await reader.read();
471
+ if (done) break;
472
+ buffered.push(value);
473
+ bufferedSize += value.byteLength;
474
+ while (bufferedSize >= chunkSize) {
475
+ const out = new Uint8Array(chunkSize);
476
+ let remaining = chunkSize;
477
+ let offset = 0;
478
+ while (remaining > 0 && buffered.length > 0) {
479
+ const head = buffered[0];
480
+ if (head === void 0) break;
481
+ const take = Math.min(head.byteLength, remaining);
482
+ out.set(head.subarray(0, take), offset);
483
+ offset += take;
484
+ remaining -= take;
485
+ if (take < head.byteLength) {
486
+ buffered[0] = head.subarray(take);
487
+ } else {
488
+ buffered.shift();
489
+ }
490
+ }
491
+ bufferedSize -= chunkSize;
492
+ yield out;
493
+ }
494
+ }
495
+ if (bufferedSize > 0) yield flush();
496
+ }
497
+ var import_client_s33, import_core10, SINGLE_PUT_MAX_BYTES;
498
+ var init_transfer = __esm({
499
+ "src/transfer.ts"() {
500
+ "use strict";
501
+ import_client_s33 = require("@aws-sdk/client-s3");
502
+ import_core10 = require("@useupup/core");
503
+ init_s3_client();
504
+ init_aws();
505
+ init_observability();
506
+ SINGLE_PUT_MAX_BYTES = MIN_PART_SIZE;
507
+ }
508
+ });
509
+
510
+ // src/fastify.ts
511
+ var fastify_exports = {};
512
+ __export(fastify_exports, {
513
+ createUpupPlugin: () => createUpupPlugin
514
+ });
515
+ module.exports = __toCommonJS(fastify_exports);
516
+
517
+ // src/handler.ts
518
+ var import_core12 = require("@useupup/core");
519
+
520
+ // src/uploadToken.ts
521
+ var import_core = require("@useupup/core");
522
+ var UploadTokenError = class extends Error {
523
+ constructor(code, message) {
524
+ super(message);
525
+ this.name = "UploadTokenError";
526
+ this.code = code;
527
+ }
528
+ };
529
+ var DEFAULT_UPLOAD_TOKEN_TTL_SECONDS = 3600;
530
+ var MIN_SECRET_LENGTH = 16;
531
+ function assertUploadTokenSecret(secret) {
532
+ if (!secret || secret.length < MIN_SECRET_LENGTH) {
533
+ throw new import_core.UpupConfigError(
534
+ `[@useupup/server] config.uploadTokenSecret is required and must be at least ${MIN_SECRET_LENGTH} characters. Generate a stable, high-entropy secret (e.g. \`openssl rand -hex 32\`) and share the SAME value across every server instance/worker.`
535
+ );
536
+ }
537
+ }
538
+ var encoder = new TextEncoder();
539
+ function bytesToBase64Url(bytes) {
540
+ let bin = "";
541
+ for (const byte of bytes) bin += String.fromCharCode(byte);
542
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
543
+ }
544
+ function base64UrlToBytes(b64url) {
545
+ const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
546
+ const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
547
+ const bin = atob(b64 + pad);
548
+ const out = new Uint8Array(bin.length);
549
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
550
+ return out;
551
+ }
552
+ async function hmacSha256(secret, data) {
553
+ const key = await crypto.subtle.importKey(
554
+ "raw",
555
+ encoder.encode(secret),
556
+ { name: "HMAC", hash: "SHA-256" },
557
+ false,
558
+ ["sign"]
559
+ );
560
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
561
+ return new Uint8Array(sig);
562
+ }
563
+ function timingSafeEqual(a, b) {
564
+ if (a.length !== b.length) return false;
565
+ let diff = 0;
566
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
567
+ return diff === 0;
568
+ }
569
+ async function signUploadToken(secret, payload) {
570
+ const body = bytesToBase64Url(encoder.encode(JSON.stringify(payload)));
571
+ const sig = bytesToBase64Url(await hmacSha256(secret, body));
572
+ return `${body}.${sig}`;
573
+ }
574
+ async function verifyUploadToken(secret, token, nowMs, options = {}) {
575
+ if (typeof token !== "string" || !token.includes(".")) {
576
+ throw new UploadTokenError(
577
+ "malformed",
578
+ "Upload token is missing or malformed"
579
+ );
580
+ }
581
+ const [body, sig] = token.split(".");
582
+ if (!body || !sig) {
583
+ throw new UploadTokenError("malformed", "Upload token is malformed");
584
+ }
585
+ const expected = bytesToBase64Url(await hmacSha256(secret, body));
586
+ if (!timingSafeEqual(sig, expected)) {
587
+ throw new UploadTokenError(
588
+ "bad_signature",
589
+ "Upload token signature is invalid"
590
+ );
591
+ }
592
+ let payload;
593
+ try {
594
+ payload = JSON.parse(
595
+ new TextDecoder().decode(base64UrlToBytes(body))
596
+ );
597
+ } catch {
598
+ throw new UploadTokenError(
599
+ "malformed",
600
+ "Upload token payload is not valid JSON"
601
+ );
602
+ }
603
+ if (typeof payload.k !== "string" || typeof payload.u !== "string" || typeof payload.exp !== "number" || typeof payload.smin !== "number" || typeof payload.smax !== "number" || payload.sid !== void 0 && typeof payload.sid !== "string") {
604
+ throw new UploadTokenError(
605
+ "malformed",
606
+ "Upload token payload is missing required fields"
607
+ );
608
+ }
609
+ if (!options.allowExpired && payload.exp <= Math.floor(nowMs / 1e3)) {
610
+ throw new UploadTokenError("expired", "Upload token has expired");
611
+ }
612
+ return payload;
613
+ }
614
+
615
+ // src/validate-config.ts
616
+ var import_core2 = require("@useupup/core");
617
+ function isNonEmpty(v) {
618
+ return typeof v === "string" && v.trim().length > 0;
619
+ }
620
+ function validateServerConfig(config) {
621
+ const missing = [];
622
+ const invalid = [];
623
+ if (!config.storage) {
624
+ missing.push("storage");
625
+ } else if (typeof config.storage === "function") {
626
+ } else {
627
+ if (!isNonEmpty(config.storage.bucket)) missing.push("storage.bucket");
628
+ if (!isNonEmpty(config.storage.region)) missing.push("storage.region");
629
+ const hasId = config.storage.accessKeyId !== void 0;
630
+ const hasSecret = config.storage.secretAccessKey !== void 0;
631
+ if (hasId || hasSecret) {
632
+ if (!isNonEmpty(config.storage.accessKeyId))
633
+ missing.push("storage.accessKeyId");
634
+ if (!isNonEmpty(config.storage.secretAccessKey))
635
+ missing.push("storage.secretAccessKey");
636
+ }
637
+ }
638
+ const p = config.providers;
639
+ if (p?.googleDrive) {
640
+ if (!isNonEmpty(p.googleDrive.clientId))
641
+ missing.push("providers.googleDrive.clientId");
642
+ if (!isNonEmpty(p.googleDrive.clientSecret))
643
+ missing.push("providers.googleDrive.clientSecret");
644
+ }
645
+ if (p?.dropbox) {
646
+ if (!isNonEmpty(p.dropbox.appKey))
647
+ missing.push("providers.dropbox.appKey");
648
+ if (!isNonEmpty(p.dropbox.appSecret))
649
+ missing.push("providers.dropbox.appSecret");
650
+ }
651
+ if (p?.oneDrive) {
652
+ if (!isNonEmpty(p.oneDrive.clientId))
653
+ missing.push("providers.oneDrive.clientId");
654
+ if (!isNonEmpty(p.oneDrive.clientSecret))
655
+ missing.push("providers.oneDrive.clientSecret");
656
+ }
657
+ if (p?.box) {
658
+ if (!isNonEmpty(p.box.clientId)) missing.push("providers.box.clientId");
659
+ if (!isNonEmpty(p.box.clientSecret))
660
+ missing.push("providers.box.clientSecret");
661
+ }
662
+ const resumeWindow = config.multipartResumeWindowSeconds;
663
+ if (resumeWindow !== void 0 && (!Number.isInteger(resumeWindow) || resumeWindow < 0)) {
664
+ invalid.push(
665
+ `multipartResumeWindowSeconds must be a non-negative integer (got ${String(resumeWindow)}); use 0 to disable /multipart/resume`
666
+ );
667
+ }
668
+ if (missing.length > 0 || invalid.length > 0) {
669
+ const sections = [
670
+ missing.length > 0 ? "missing/empty required field(s):\n" + missing.map((m) => ` - ${m}`).join("\n") : "",
671
+ invalid.length > 0 ? "invalid field(s):\n" + invalid.map((m) => ` - ${m}`).join("\n") : ""
672
+ ].filter(Boolean);
673
+ throw new import_core2.UpupConfigError(
674
+ `[@useupup/server] Invalid config \u2014 ${sections.join("\n")}`
675
+ );
676
+ }
677
+ }
678
+
679
+ // src/storage.ts
680
+ var import_core3 = require("@useupup/core");
681
+ function assertS3Storage(storage) {
682
+ const storageType = storage.type;
683
+ if (typeof storageType === "string" && import_core3.NON_S3_STORAGE_PROVIDERS.has(storageType)) {
684
+ throw new import_core3.UpupConfigError(
685
+ `[@useupup/server] storage.type "${storageType}" has no S3-compatible API and cannot be served. upup uploads via the S3 API \u2014 use an S3-compatible provider (aws, minio, r2, wasabi, \u2026) and set storage.endpoint for non-AWS backends.`
686
+ );
687
+ }
688
+ }
689
+
690
+ // src/resolve-storage.ts
691
+ var import_core4 = require("@useupup/core");
692
+ function isStorageResolver(storage) {
693
+ return typeof storage === "function";
694
+ }
695
+ async function storageIdentity(storage) {
696
+ const material = [
697
+ storage.bucket,
698
+ storage.endpoint ?? "",
699
+ storage.region
700
+ ].join("\n");
701
+ const digest = await crypto.subtle.digest(
702
+ "SHA-256",
703
+ new TextEncoder().encode(material)
704
+ );
705
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 32);
706
+ }
707
+ function assertResolvedStorage(storage) {
708
+ const s = storage;
709
+ const missing = [];
710
+ if (!s || typeof s !== "object") {
711
+ throw new import_core4.UpupConfigError(
712
+ "[@useupup/server] the storage resolver did not return a storage config object."
713
+ );
714
+ }
715
+ if (typeof s.bucket !== "string" || s.bucket.trim() === "")
716
+ missing.push("bucket");
717
+ if (typeof s.region !== "string" || s.region.trim() === "")
718
+ missing.push("region");
719
+ if (missing.length > 0) {
720
+ throw new import_core4.UpupConfigError(
721
+ "[@useupup/server] the storage resolver returned a config missing required field(s): " + missing.map((m) => `storage.${m}`).join(", ")
722
+ );
723
+ }
724
+ assertS3Storage(s);
725
+ }
726
+ async function resolveStorage(config, ctx) {
727
+ if (!isStorageResolver(config.storage)) return config.storage;
728
+ const resolved = await config.storage(ctx);
729
+ assertResolvedStorage(resolved);
730
+ return resolved;
731
+ }
732
+ async function resolveBoundStorage(config, ctx, boundId) {
733
+ if (!isStorageResolver(config.storage)) return config.storage;
734
+ if (!boundId) {
735
+ throw new StorageBindingError(
736
+ "Upload token carries no storage binding; restart the upload from /multipart/init"
737
+ );
738
+ }
739
+ const resolved = await resolveStorage(config, {
740
+ ...ctx,
741
+ storageId: boundId
742
+ });
743
+ if (await storageIdentity(resolved) !== boundId) {
744
+ throw new StorageBindingError(
745
+ "Upload token is bound to different storage than the resolver returned"
746
+ );
747
+ }
748
+ return resolved;
749
+ }
750
+ var StorageBindingError = class extends Error {
751
+ constructor(message) {
752
+ super(message);
753
+ this.name = "StorageBindingError";
754
+ }
755
+ };
756
+
757
+ // src/health.ts
758
+ init_aws();
759
+ init_observability();
760
+ var cachedStorageCheck;
761
+ var cachedAt = 0;
762
+ var STORAGE_CHECK_TTL_MS = 3e4;
763
+ function isConfigComplete(config) {
764
+ const secretOk = Boolean(
765
+ config.uploadTokenSecret && config.uploadTokenSecret.length >= 16
766
+ );
767
+ if (isStorageResolver(config.storage)) return secretOk;
768
+ return Boolean(config.storage.bucket && config.storage.region && secretOk);
769
+ }
770
+ async function sha256Hex(input) {
771
+ const bytes = new TextEncoder().encode(input);
772
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
773
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
774
+ }
775
+ async function handleHealth(config, res) {
776
+ const configOk = isConfigComplete(config);
777
+ const staticStorage = isStorageResolver(config.storage) ? null : config.storage;
778
+ const now = Date.now();
779
+ if (staticStorage && (!cachedStorageCheck || now - cachedAt > STORAGE_CHECK_TTL_MS)) {
780
+ const result = await checkStorageReachable(staticStorage);
781
+ if (!result.ok) {
782
+ reportServerError(config.onError, {
783
+ route: "health",
784
+ method: "GET",
785
+ status: 200,
786
+ code: "STORAGE_ERROR",
787
+ message: "Health check: storage unreachable",
788
+ requestId: res.requestId,
789
+ error: toSafeError(result.error)
790
+ });
791
+ }
792
+ cachedStorageCheck = result.ok ? { ok: true } : { ok: false };
793
+ cachedAt = now;
794
+ }
795
+ const storageOk = staticStorage ? Boolean(cachedStorageCheck?.ok) : true;
796
+ const body = {
797
+ status: configOk && storageOk ? "ok" : "degraded",
798
+ checks: {
799
+ config: configOk ? "ok" : "incomplete",
800
+ storage: !staticStorage ? "skipped" : storageOk ? "ok" : "error"
801
+ },
802
+ // Non-secret operational summary — labels/flags/counts only, never any
803
+ // secret VALUE. Lets an operator eyeball how an instance is configured
804
+ // (which storage backend, whether anonymous access is open, how many
805
+ // drive providers are wired, the upload-token lifetime) from the same
806
+ // unauthenticated probe.
807
+ summary: {
808
+ storageType: staticStorage ? staticStorage.type : "dynamic",
809
+ anonymousUploads: Boolean(config.allowAnonymousUploads),
810
+ anonymousDrives: Boolean(config.allowAnonymous),
811
+ driveProviders: config.providers ? Object.keys(config.providers).length : 0,
812
+ uploadTokenTtlSeconds: DEFAULT_UPLOAD_TOKEN_TTL_SECONDS
813
+ }
814
+ };
815
+ if (config.health?.exposeSecretFingerprint && config.uploadTokenSecret) {
816
+ body.uploadTokenFingerprint = (await sha256Hex(config.uploadTokenSecret)).slice(0, 8);
817
+ }
818
+ return res.json(body);
819
+ }
820
+
821
+ // src/respond.ts
822
+ init_observability();
823
+ var import_core6 = require("@useupup/core");
824
+ function corsHeaders(req, config) {
825
+ const cors = config.cors;
826
+ if (!cors) return {};
827
+ const origin = req.headers.get("origin") ?? "";
828
+ const allowsWildcard = cors.allowedOrigins.includes("*");
829
+ const allowsOrigin = origin && cors.allowedOrigins.includes(origin);
830
+ if (!allowsWildcard && !allowsOrigin) return {};
831
+ const allowOrigin = origin ? origin : "*";
832
+ const headers = {
833
+ "Access-Control-Allow-Origin": allowOrigin,
834
+ "Access-Control-Allow-Methods": (cors.allowedMethods ?? ["GET", "POST", "OPTIONS"]).join(", "),
835
+ "Access-Control-Allow-Headers": (cors.allowedHeaders ?? ["Content-Type", "Authorization"]).join(", "),
836
+ "Access-Control-Max-Age": String(cors.maxAgeSeconds ?? 600),
837
+ Vary: "Origin"
838
+ };
839
+ if (allowsOrigin && allowOrigin !== "*") {
840
+ headers["Access-Control-Allow-Credentials"] = "true";
841
+ }
842
+ return headers;
843
+ }
844
+ function createResponder(req, config) {
845
+ const requestId = crypto.randomUUID();
846
+ const headers = {
847
+ ...corsHeaders(req, config),
848
+ "x-upup-request-id": requestId
849
+ };
850
+ const json = (data, status = 200) => new Response(JSON.stringify(data), {
851
+ status,
852
+ headers: { "Content-Type": "application/json", ...headers }
853
+ });
854
+ return {
855
+ requestId,
856
+ json,
857
+ html: (body, status = 200) => new Response(body, {
858
+ status,
859
+ headers: {
860
+ "Content-Type": "text/html; charset=utf-8",
861
+ ...headers
862
+ }
863
+ }),
864
+ redirect: (location, status = 302) => new Response(null, {
865
+ status,
866
+ headers: { ...headers, Location: location }
867
+ }),
868
+ noContent: (status = 204) => new Response(null, { status, headers }),
869
+ fail: (route, method, status, code, message, error) => {
870
+ reportServerError(config.onError, {
871
+ route,
872
+ method,
873
+ status,
874
+ code,
875
+ message,
876
+ requestId,
877
+ error: toSafeError(error)
878
+ });
879
+ return json({ error: message, code }, status);
880
+ }
881
+ };
882
+ }
883
+ async function parseJsonBody(req, res) {
884
+ try {
885
+ const value = await req.json();
886
+ return { ok: true, value };
887
+ } catch {
888
+ return {
889
+ ok: false,
890
+ response: res.json(
891
+ { error: "Invalid JSON body", code: import_core6.UpupErrorCode.BAD_REQUEST },
892
+ 400
893
+ )
894
+ };
895
+ }
896
+ }
897
+
898
+ // src/upload-routes.ts
899
+ var import_core7 = require("@useupup/core");
900
+ init_aws();
901
+
902
+ // src/tokenStore.ts
903
+ var OAUTH_STATE_TTL_SECONDS = 600;
904
+ var tokensKey = (userId, provider) => `upup:tokens:${userId}:${provider}`;
905
+ var oauthStateKey = (state) => `upup:oauth-state:${state}`;
906
+ async function getTokens(store, userId, provider) {
907
+ const raw = await store.get(tokensKey(userId, provider));
908
+ if (!raw) return null;
909
+ try {
910
+ return JSON.parse(raw);
911
+ } catch {
912
+ return null;
913
+ }
914
+ }
915
+ async function setTokens(store, userId, provider, tokens) {
916
+ let ttlSeconds;
917
+ if (!tokens.refreshToken && tokens.expiresAt) {
918
+ ttlSeconds = Math.max(
919
+ 0,
920
+ Math.ceil((tokens.expiresAt - Date.now()) / 1e3)
921
+ );
922
+ }
923
+ await store.set(
924
+ tokensKey(userId, provider),
925
+ JSON.stringify(tokens),
926
+ ttlSeconds
927
+ );
928
+ }
929
+ async function deleteTokens(store, userId, provider) {
930
+ await store.delete(tokensKey(userId, provider));
931
+ }
932
+ async function saveOAuthState(store, state, payload) {
933
+ await store.set(
934
+ oauthStateKey(state),
935
+ JSON.stringify(payload),
936
+ OAUTH_STATE_TTL_SECONDS
937
+ );
938
+ }
939
+ async function consumeOAuthState(store, state) {
940
+ const raw = await store.get(oauthStateKey(state));
941
+ if (!raw) return null;
942
+ await store.delete(oauthStateKey(state));
943
+ try {
944
+ return JSON.parse(raw);
945
+ } catch {
946
+ return null;
947
+ }
948
+ }
949
+ function generateOAuthState() {
950
+ const bytes = new Uint8Array(32);
951
+ crypto.getRandomValues(bytes);
952
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
953
+ }
954
+ var DEFAULT_USER_ID = "default";
955
+ async function resolveUserId(config, req) {
956
+ if (config.getUserId) return config.getUserId(req);
957
+ return DEFAULT_USER_ID;
958
+ }
959
+
960
+ // src/key.ts
961
+ function sanitizeFilename(name) {
962
+ const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._]+/, "").slice(0, 128);
963
+ return cleaned || "file";
964
+ }
965
+ function defaultKeyStrategy(ctx) {
966
+ const owner = ctx.userId ?? "anon";
967
+ return `${owner}/${crypto.randomUUID()}/${sanitizeFilename(ctx.fileName)}`;
968
+ }
969
+
970
+ // src/upload-routes.ts
971
+ init_observability();
972
+ function requireUploadAuthorization(config, res, route, method) {
973
+ if (!config.auth && !config.getUserId && !config.allowAnonymousUploads) {
974
+ return res.fail(
975
+ route,
976
+ method,
977
+ 403,
978
+ import_core7.UpupErrorCode.AUTH_REQUIRED,
979
+ "Anonymous uploads are disabled. Set allowAnonymousUploads:true, or configure auth/getUserId.",
980
+ new Error("anonymous upload rejected")
981
+ );
982
+ }
983
+ return null;
984
+ }
985
+ var DEFAULT_MULTIPART_RESUME_WINDOW_SECONDS = 86400;
986
+ async function verifyTokenOrRespond(config, token, res, route, method, allowExpired = false) {
987
+ assertUploadTokenSecret(config.uploadTokenSecret);
988
+ try {
989
+ return await verifyUploadToken(
990
+ config.uploadTokenSecret,
991
+ token,
992
+ Date.now(),
993
+ { allowExpired }
994
+ );
995
+ } catch (e) {
996
+ if (e instanceof UploadTokenError) {
997
+ reportServerError(config.onError, {
998
+ route,
999
+ method,
1000
+ status: 403,
1001
+ code: e.code,
1002
+ message: "Invalid upload token",
1003
+ requestId: res.requestId,
1004
+ error: toSafeError(e)
1005
+ });
1006
+ return res.json(
1007
+ { error: "Invalid upload token", code: e.code },
1008
+ 403
1009
+ );
1010
+ }
1011
+ throw e;
1012
+ }
1013
+ }
1014
+ async function enforceTokenOwner(config, req, payload, res, route, method) {
1015
+ if (!config.getUserId) return null;
1016
+ const currentUserId = await resolveUserId(config, req);
1017
+ const currentOwner = currentUserId === DEFAULT_USER_ID ? null : currentUserId;
1018
+ if (currentOwner !== payload.uid) {
1019
+ return res.fail(
1020
+ route,
1021
+ method,
1022
+ 403,
1023
+ import_core7.UpupErrorCode.AUTH_DENIED,
1024
+ "Upload token does not belong to the current user",
1025
+ new Error("upload-token uid mismatch")
1026
+ );
1027
+ }
1028
+ return null;
1029
+ }
1030
+ function matchesAllowedType(type, allowedTypes) {
1031
+ if (!allowedTypes?.length) return true;
1032
+ return allowedTypes.some((allowed) => {
1033
+ if (allowed === type) return true;
1034
+ if (allowed.endsWith("/*")) {
1035
+ return type.startsWith(`${allowed.slice(0, -2)}/`);
1036
+ }
1037
+ return false;
1038
+ });
1039
+ }
1040
+ async function validateUploadMetadata(req, config, body, res) {
1041
+ if (typeof body.name !== "string" || body.name.length === 0 || typeof body.type !== "string" || typeof body.size !== "number" || !Number.isFinite(body.size) || body.size < 0) {
1042
+ return res.json(
1043
+ { error: "Invalid file metadata", code: import_core7.UpupErrorCode.BAD_REQUEST },
1044
+ 400
1045
+ );
1046
+ }
1047
+ if (config.maxFileSize && body.size > config.maxFileSize) {
1048
+ return res.json({ error: "File too large" }, 413);
1049
+ }
1050
+ if (!matchesAllowedType(body.type, config.allowedTypes)) {
1051
+ return res.json({ error: "File type not allowed" }, 415);
1052
+ }
1053
+ if (config.hooks?.onBeforeUpload) {
1054
+ let allowed;
1055
+ try {
1056
+ allowed = await config.hooks.onBeforeUpload(body, req);
1057
+ } catch (error) {
1058
+ if (error instanceof import_core7.UpupError) {
1059
+ return res.json({ error: error.message, code: error.code }, 403);
1060
+ }
1061
+ throw error;
1062
+ }
1063
+ if (!allowed) {
1064
+ return res.json({ error: "Upload rejected" }, 403);
1065
+ }
1066
+ }
1067
+ return null;
1068
+ }
1069
+ async function resolveStorageOrFail(config, res, route, method, ctx) {
1070
+ try {
1071
+ return await resolveStorage(config, ctx);
1072
+ } catch (error) {
1073
+ return res.fail(
1074
+ route,
1075
+ method,
1076
+ 500,
1077
+ import_core7.UpupErrorCode.STORAGE_ERROR,
1078
+ "Storage configuration error",
1079
+ error
1080
+ );
1081
+ }
1082
+ }
1083
+ async function resolveBoundStorageOrFail(config, res, route, method, ctx, boundId) {
1084
+ try {
1085
+ return await resolveBoundStorage(config, ctx, boundId);
1086
+ } catch (error) {
1087
+ if (error instanceof StorageBindingError) {
1088
+ return res.fail(
1089
+ route,
1090
+ method,
1091
+ 403,
1092
+ import_core7.UpupErrorCode.AUTH_DENIED,
1093
+ "Upload token is not valid for the resolved storage",
1094
+ error
1095
+ );
1096
+ }
1097
+ return res.fail(
1098
+ route,
1099
+ method,
1100
+ 500,
1101
+ import_core7.UpupErrorCode.STORAGE_ERROR,
1102
+ "Storage configuration error",
1103
+ error
1104
+ );
1105
+ }
1106
+ }
1107
+ async function applyPresignResponseHook(config, response, ctx) {
1108
+ const hook = config.hooks?.onPresignResponse;
1109
+ if (!hook) return response;
1110
+ return await hook(response, ctx) ?? response;
1111
+ }
1112
+ async function runPostCompletionHooks(config, res, route, method, run) {
1113
+ if (!config.hooks) return;
1114
+ try {
1115
+ await run();
1116
+ } catch (error) {
1117
+ reportServerError(config.onError, {
1118
+ route,
1119
+ method,
1120
+ status: 200,
1121
+ // No dedicated HOOK_ERROR code exists in @useupup/core; the message
1122
+ // carries the real meaning — the upload itself succeeded.
1123
+ code: import_core7.UpupErrorCode.STORAGE_ERROR,
1124
+ message: "Post-completion hook threw after a durably-completed upload; the upload succeeded and the client was returned 200 (F-745).",
1125
+ requestId: res.requestId,
1126
+ error: toSafeError(error)
1127
+ });
1128
+ }
1129
+ }
1130
+ async function handlePresign(req, config, res) {
1131
+ const gate = requireUploadAuthorization(config, res, "presign", req.method);
1132
+ if (gate) return gate;
1133
+ const parsed = await parseJsonBody(req, res);
1134
+ if (!parsed.ok) return parsed.response;
1135
+ const body = parsed.value;
1136
+ const validationError = await validateUploadMetadata(req, config, body, res);
1137
+ if (validationError) return validationError;
1138
+ const userId = await resolveUserId(config, req);
1139
+ if (userId === null) return res.json({ error: "Unauthenticated" }, 401);
1140
+ const owner = userId === DEFAULT_USER_ID ? null : userId;
1141
+ const key = (config.keyStrategy ?? defaultKeyStrategy)({
1142
+ userId: owner,
1143
+ fileName: body.name,
1144
+ contentType: body.type,
1145
+ size: body.size,
1146
+ ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
1147
+ req
1148
+ });
1149
+ const storage = await resolveStorageOrFail(
1150
+ config,
1151
+ res,
1152
+ "presign",
1153
+ req.method,
1154
+ {
1155
+ req,
1156
+ phase: "presign",
1157
+ userId: owner,
1158
+ ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
1159
+ fileName: body.name,
1160
+ contentType: body.type,
1161
+ size: body.size
1162
+ }
1163
+ );
1164
+ if (storage instanceof Response) return storage;
1165
+ try {
1166
+ const result = await generatePresignedUrl(
1167
+ storage,
1168
+ key,
1169
+ body.type,
1170
+ body.size,
1171
+ void 0,
1172
+ config.downloadUrlExpiresIn
1173
+ );
1174
+ const payload = await applyPresignResponseHook(config, result, {
1175
+ req,
1176
+ phase: "presign",
1177
+ // Always the key that is IN the payload, on every phase.
1178
+ key: result.key,
1179
+ file: body,
1180
+ ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
1181
+ userId: owner
1182
+ });
1183
+ return res.json(payload, 200);
1184
+ } catch (error) {
1185
+ return res.fail(
1186
+ "presign",
1187
+ req.method,
1188
+ 500,
1189
+ import_core7.UpupErrorCode.PRESIGN_FAILED,
1190
+ "Presign failed",
1191
+ error
1192
+ );
1193
+ }
1194
+ }
1195
+ async function handleMultipartInit(req, config, res) {
1196
+ const gate = requireUploadAuthorization(
1197
+ config,
1198
+ res,
1199
+ "multipart/init",
1200
+ req.method
1201
+ );
1202
+ if (gate) return gate;
1203
+ const parsed = await parseJsonBody(req, res);
1204
+ if (!parsed.ok) return parsed.response;
1205
+ const body = parsed.value;
1206
+ try {
1207
+ const validationError = await validateUploadMetadata(
1208
+ req,
1209
+ config,
1210
+ body,
1211
+ res
1212
+ );
1213
+ if (validationError) return validationError;
1214
+ const userId = await resolveUserId(config, req);
1215
+ if (userId === null) return res.json({ error: "Unauthenticated" }, 401);
1216
+ const owner = userId === DEFAULT_USER_ID ? null : userId;
1217
+ const key = (config.keyStrategy ?? defaultKeyStrategy)({
1218
+ userId: owner,
1219
+ fileName: body.name,
1220
+ contentType: body.type,
1221
+ size: body.size,
1222
+ ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
1223
+ req
1224
+ });
1225
+ const storage = await resolveStorageOrFail(
1226
+ config,
1227
+ res,
1228
+ "multipart/init",
1229
+ req.method,
1230
+ {
1231
+ req,
1232
+ phase: "multipart-init",
1233
+ userId: owner,
1234
+ ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
1235
+ fileName: body.name,
1236
+ contentType: body.type,
1237
+ size: body.size
1238
+ }
1239
+ );
1240
+ if (storage instanceof Response) return storage;
1241
+ const result = await initiateMultipartUpload(
1242
+ storage,
1243
+ key,
1244
+ body.type,
1245
+ body.size,
1246
+ void 0,
1247
+ body.chunkSizeBytes
1248
+ );
1249
+ assertUploadTokenSecret(config.uploadTokenSecret);
1250
+ const sid = isStorageResolver(config.storage) ? await storageIdentity(storage) : void 0;
1251
+ const issuedAt = Math.floor(Date.now() / 1e3);
1252
+ const token = await signUploadToken(config.uploadTokenSecret, {
1253
+ k: result.key,
1254
+ u: result.uploadId,
1255
+ uid: owner,
1256
+ smin: 0,
1257
+ smax: body.size,
1258
+ ...sid !== void 0 ? { sid } : {},
1259
+ exp: issuedAt + DEFAULT_UPLOAD_TOKEN_TTL_SECONDS,
1260
+ iat: issuedAt
1261
+ });
1262
+ const payload = await applyPresignResponseHook(
1263
+ config,
1264
+ { ...result, token },
1265
+ {
1266
+ req,
1267
+ phase: "multipart-init",
1268
+ key: result.key,
1269
+ file: body,
1270
+ ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
1271
+ userId: owner
1272
+ }
1273
+ );
1274
+ return res.json(payload, 200);
1275
+ } catch (error) {
1276
+ return res.fail(
1277
+ "multipart/init",
1278
+ req.method,
1279
+ 500,
1280
+ import_core7.UpupErrorCode.STORAGE_ERROR,
1281
+ "Multipart init failed",
1282
+ error
1283
+ );
1284
+ }
1285
+ }
1286
+ function isNoSuchUpload(error) {
1287
+ if (typeof error !== "object" || error === null) return false;
1288
+ const shape = error;
1289
+ return shape.name === "NoSuchUpload" || shape.Code === "NoSuchUpload";
1290
+ }
1291
+ async function handleMultipartResume(req, config, res) {
1292
+ const parsed = await parseJsonBody(req, res);
1293
+ if (!parsed.ok) return parsed.response;
1294
+ const body = parsed.value;
1295
+ try {
1296
+ const payload = await verifyTokenOrRespond(
1297
+ config,
1298
+ body.token,
1299
+ res,
1300
+ "multipart/resume",
1301
+ req.method,
1302
+ true
1303
+ // allowExpired — the resume window below is the real bound
1304
+ );
1305
+ if (payload instanceof Response) return payload;
1306
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1307
+ const issuedAt = payload.iat ?? payload.exp - DEFAULT_UPLOAD_TOKEN_TTL_SECONDS;
1308
+ const windowSeconds = config.multipartResumeWindowSeconds ?? DEFAULT_MULTIPART_RESUME_WINDOW_SECONDS;
1309
+ if (nowSeconds > issuedAt + windowSeconds) {
1310
+ return res.fail(
1311
+ "multipart/resume",
1312
+ req.method,
1313
+ 403,
1314
+ "expired",
1315
+ "Upload resume window has expired",
1316
+ new Error("resume window elapsed")
1317
+ );
1318
+ }
1319
+ const owned = await enforceTokenOwner(
1320
+ config,
1321
+ req,
1322
+ payload,
1323
+ res,
1324
+ "multipart/resume",
1325
+ req.method
1326
+ );
1327
+ if (owned) return owned;
1328
+ const storage = await resolveBoundStorageOrFail(
1329
+ config,
1330
+ res,
1331
+ "multipart/resume",
1332
+ req.method,
1333
+ { req, phase: "multipart-resume", userId: payload.uid },
1334
+ payload.sid
1335
+ );
1336
+ if (storage instanceof Response) return storage;
1337
+ let listed;
1338
+ try {
1339
+ listed = await listMultipartParts(storage, payload.k, payload.u);
1340
+ } catch (error) {
1341
+ if (isNoSuchUpload(error)) {
1342
+ return res.fail(
1343
+ "multipart/resume",
1344
+ req.method,
1345
+ 404,
1346
+ import_core7.UpupErrorCode.NOT_FOUND,
1347
+ "Multipart upload no longer exists",
1348
+ error
1349
+ );
1350
+ }
1351
+ throw error;
1352
+ }
1353
+ assertUploadTokenSecret(config.uploadTokenSecret);
1354
+ const token = await signUploadToken(config.uploadTokenSecret, {
1355
+ // Every binding is copied from the VERIFIED payload, unchanged: same
1356
+ // key, same uploadId, same owner, same size envelope, same storage
1357
+ // identity. Only `exp` moves — `iat` is the original, so the window
1358
+ // does not roll.
1359
+ k: payload.k,
1360
+ u: payload.u,
1361
+ uid: payload.uid,
1362
+ smin: payload.smin,
1363
+ smax: payload.smax,
1364
+ ...payload.sid !== void 0 ? { sid: payload.sid } : {},
1365
+ // Clamp to the resume window, not a fresh full TTL. The window is
1366
+ // the operator's stated cap on how long a leaked token stays
1367
+ // usable (multipartResumeWindowSeconds); a re-issued token that
1368
+ // outlived it by up to a full TTL would make that cap — and the
1369
+ // documented leak mitigation — a lie.
1370
+ exp: Math.min(
1371
+ nowSeconds + DEFAULT_UPLOAD_TOKEN_TTL_SECONDS,
1372
+ issuedAt + windowSeconds
1373
+ ),
1374
+ iat: issuedAt
1375
+ });
1376
+ const response = {
1377
+ key: payload.k,
1378
+ token,
1379
+ parts: listed.parts
1380
+ };
1381
+ return res.json(response, 200);
1382
+ } catch (error) {
1383
+ return res.fail(
1384
+ "multipart/resume",
1385
+ req.method,
1386
+ 500,
1387
+ import_core7.UpupErrorCode.STORAGE_ERROR,
1388
+ "Multipart resume failed",
1389
+ error
1390
+ );
1391
+ }
1392
+ }
1393
+ async function handleMultipartSignPart(req, config, res) {
1394
+ const parsed = await parseJsonBody(req, res);
1395
+ if (!parsed.ok) return parsed.response;
1396
+ const body = parsed.value;
1397
+ try {
1398
+ const payload = await verifyTokenOrRespond(
1399
+ config,
1400
+ body.token,
1401
+ res,
1402
+ "multipart/sign-part",
1403
+ req.method
1404
+ );
1405
+ if (payload instanceof Response) return payload;
1406
+ const owned = await enforceTokenOwner(
1407
+ config,
1408
+ req,
1409
+ payload,
1410
+ res,
1411
+ "multipart/sign-part",
1412
+ req.method
1413
+ );
1414
+ if (owned) return owned;
1415
+ const storage = await resolveBoundStorageOrFail(
1416
+ config,
1417
+ res,
1418
+ "multipart/sign-part",
1419
+ req.method,
1420
+ {
1421
+ req,
1422
+ phase: "multipart-sign-part",
1423
+ userId: payload.uid
1424
+ },
1425
+ payload.sid
1426
+ );
1427
+ if (storage instanceof Response) return storage;
1428
+ const result = await generatePresignedPartUrl(
1429
+ storage,
1430
+ payload.k,
1431
+ payload.u,
1432
+ body.partNumber
1433
+ );
1434
+ const rewritten = await applyPresignResponseHook(config, result, {
1435
+ req,
1436
+ phase: "multipart-sign-part",
1437
+ // From the VERIFIED token — sign-part never sees a client-asserted
1438
+ // key, and has no file metadata to report.
1439
+ key: payload.k,
1440
+ userId: payload.uid
1441
+ });
1442
+ return res.json(rewritten, 200);
1443
+ } catch (error) {
1444
+ return res.fail(
1445
+ "multipart/sign-part",
1446
+ req.method,
1447
+ 500,
1448
+ import_core7.UpupErrorCode.STORAGE_ERROR,
1449
+ "Multipart sign failed",
1450
+ error
1451
+ );
1452
+ }
1453
+ }
1454
+ async function handleMultipartComplete(req, config, res) {
1455
+ const parsed = await parseJsonBody(req, res);
1456
+ if (!parsed.ok) return parsed.response;
1457
+ const body = parsed.value;
1458
+ try {
1459
+ const payload = await verifyTokenOrRespond(
1460
+ config,
1461
+ body.token,
1462
+ res,
1463
+ "multipart/complete",
1464
+ req.method
1465
+ );
1466
+ if (payload instanceof Response) return payload;
1467
+ const owned = await enforceTokenOwner(
1468
+ config,
1469
+ req,
1470
+ payload,
1471
+ res,
1472
+ "multipart/complete",
1473
+ req.method
1474
+ );
1475
+ if (owned) return owned;
1476
+ const storage = await resolveBoundStorageOrFail(
1477
+ config,
1478
+ res,
1479
+ "multipart/complete",
1480
+ req.method,
1481
+ { req, phase: "multipart-complete", userId: payload.uid },
1482
+ payload.sid
1483
+ );
1484
+ if (storage instanceof Response) return storage;
1485
+ const uploadedSize = await getMultipartUploadedSize(
1486
+ storage,
1487
+ payload.k,
1488
+ payload.u
1489
+ );
1490
+ if (uploadedSize < payload.smin || uploadedSize > payload.smax) {
1491
+ await abortMultipartUpload(storage, payload.k, payload.u);
1492
+ return res.json(
1493
+ { error: "Upload size outside signed envelope" },
1494
+ 403
1495
+ );
1496
+ }
1497
+ const result = await completeMultipartUpload(
1498
+ storage,
1499
+ payload.k,
1500
+ payload.u,
1501
+ body.parts,
1502
+ config.downloadUrlExpiresIn
1503
+ );
1504
+ const uploaded = {
1505
+ key: result.key,
1506
+ name: result.key.split("/").pop() ?? result.key,
1507
+ size: uploadedSize,
1508
+ // already computed above for the envelope check
1509
+ type: "",
1510
+ // not retained server-side on the multipart path
1511
+ url: result.downloadUrl ?? ""
1512
+ };
1513
+ await runPostCompletionHooks(
1514
+ config,
1515
+ res,
1516
+ "multipart/complete",
1517
+ req.method,
1518
+ async () => {
1519
+ if (config.hooks?.onFileUploaded)
1520
+ await config.hooks.onFileUploaded(uploaded, req);
1521
+ if (config.hooks?.onUploadComplete)
1522
+ await config.hooks.onUploadComplete([uploaded], req);
1523
+ }
1524
+ );
1525
+ return res.json(result, 200);
1526
+ } catch (error) {
1527
+ return res.fail(
1528
+ "multipart/complete",
1529
+ req.method,
1530
+ 500,
1531
+ import_core7.UpupErrorCode.STORAGE_ERROR,
1532
+ "Multipart complete failed",
1533
+ error
1534
+ );
1535
+ }
1536
+ }
1537
+ async function handleMultipartAbort(req, config, res) {
1538
+ const parsed = await parseJsonBody(req, res);
1539
+ if (!parsed.ok) return parsed.response;
1540
+ const body = parsed.value;
1541
+ try {
1542
+ const payload = await verifyTokenOrRespond(
1543
+ config,
1544
+ body.token,
1545
+ res,
1546
+ "multipart/abort",
1547
+ req.method
1548
+ );
1549
+ if (payload instanceof Response) return payload;
1550
+ const owned = await enforceTokenOwner(
1551
+ config,
1552
+ req,
1553
+ payload,
1554
+ res,
1555
+ "multipart/abort",
1556
+ req.method
1557
+ );
1558
+ if (owned) return owned;
1559
+ const storage = await resolveBoundStorageOrFail(
1560
+ config,
1561
+ res,
1562
+ "multipart/abort",
1563
+ req.method,
1564
+ { req, phase: "multipart-abort", userId: payload.uid },
1565
+ payload.sid
1566
+ );
1567
+ if (storage instanceof Response) return storage;
1568
+ const result = await abortMultipartUpload(storage, payload.k, payload.u);
1569
+ return res.json(result, 200);
1570
+ } catch (error) {
1571
+ return res.fail(
1572
+ "multipart/abort",
1573
+ req.method,
1574
+ 500,
1575
+ import_core7.UpupErrorCode.STORAGE_ERROR,
1576
+ "Multipart abort failed",
1577
+ error
1578
+ );
1579
+ }
1580
+ }
1581
+
1582
+ // src/oauth.ts
1583
+ var import_core8 = require("@useupup/core");
1584
+ init_observability();
1585
+ var VALID_PROVIDERS = [
1586
+ "google-drive",
1587
+ "one-drive",
1588
+ "dropbox",
1589
+ "box"
1590
+ ];
1591
+ function isValidProvider(p) {
1592
+ return VALID_PROVIDERS.includes(p);
1593
+ }
1594
+ function getProviderMeta(config, provider) {
1595
+ const providers = config.providers;
1596
+ if (!providers)
1597
+ return { error: "No OAuth providers configured", status: 500 };
1598
+ switch (provider) {
1599
+ case "google-drive": {
1600
+ const gc = providers.googleDrive;
1601
+ if (!gc)
1602
+ return { error: "Google Drive not configured", status: 400 };
1603
+ return {
1604
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1605
+ tokenUrl: "https://oauth2.googleapis.com/token",
1606
+ scope: "https://www.googleapis.com/auth/drive.readonly",
1607
+ clientId: gc.clientId,
1608
+ clientSecret: gc.clientSecret,
1609
+ extra: { access_type: "offline", prompt: "consent" }
1610
+ };
1611
+ }
1612
+ case "one-drive": {
1613
+ const oc = providers.oneDrive;
1614
+ if (!oc) return { error: "OneDrive not configured", status: 400 };
1615
+ const tenant = oc.tenantId ?? "common";
1616
+ return {
1617
+ authUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
1618
+ tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
1619
+ scope: "Files.Read.All offline_access",
1620
+ clientId: oc.clientId,
1621
+ clientSecret: oc.clientSecret
1622
+ };
1623
+ }
1624
+ case "dropbox": {
1625
+ const dc = providers.dropbox;
1626
+ if (!dc) return { error: "Dropbox not configured", status: 400 };
1627
+ return {
1628
+ authUrl: "https://www.dropbox.com/oauth2/authorize",
1629
+ tokenUrl: "https://api.dropboxapi.com/oauth2/token",
1630
+ scope: "files.content.read files.metadata.read",
1631
+ clientId: dc.appKey,
1632
+ clientSecret: dc.appSecret,
1633
+ extra: { token_access_type: "online" }
1634
+ };
1635
+ }
1636
+ case "box": {
1637
+ const bc = providers.box;
1638
+ if (!bc) return { error: "Box not configured", status: 400 };
1639
+ return {
1640
+ authUrl: "https://account.box.com/api/oauth2/authorize",
1641
+ tokenUrl: "https://api.box.com/oauth2/token",
1642
+ scope: "root_readonly",
1643
+ clientId: bc.clientId,
1644
+ clientSecret: bc.clientSecret
1645
+ };
1646
+ }
1647
+ }
1648
+ }
1649
+ function callbackUrlFor(req, provider) {
1650
+ const url = new URL(req.url);
1651
+ const base = `${url.origin}${url.pathname.replace(/\/auth\/[\w-]+(?:\/cb)?\/?$/, "")}`;
1652
+ return `${base}/auth/${provider}/cb`;
1653
+ }
1654
+ async function handleOAuthRedirect(req, config, provider, res) {
1655
+ if (!isValidProvider(provider)) {
1656
+ return res.json({ error: `Unknown provider: ${provider}` }, 400);
1657
+ }
1658
+ if (!config.tokenStore) {
1659
+ return res.json(
1660
+ { error: "tokenStore is required for OAuth flows" },
1661
+ 500
1662
+ );
1663
+ }
1664
+ const userId = await resolveUserId(config, req);
1665
+ if (!userId) return res.json({ error: "Unauthenticated" }, 401);
1666
+ const meta = getProviderMeta(config, provider);
1667
+ if ("error" in meta) return res.json({ error: meta.error }, meta.status);
1668
+ const state = generateOAuthState();
1669
+ const returnTo = new URL(req.url).searchParams.get("returnTo") ?? void 0;
1670
+ await saveOAuthState(config.tokenStore, state, {
1671
+ userId,
1672
+ provider,
1673
+ returnTo
1674
+ });
1675
+ const params = new URLSearchParams({
1676
+ client_id: meta.clientId,
1677
+ redirect_uri: callbackUrlFor(req, provider),
1678
+ response_type: "code",
1679
+ scope: meta.scope,
1680
+ state,
1681
+ ...meta.extra ?? {}
1682
+ });
1683
+ return res.redirect(`${meta.authUrl}?${params.toString()}`);
1684
+ }
1685
+ async function handleOAuthCallback(req, config, provider, res) {
1686
+ if (!isValidProvider(provider)) {
1687
+ return res.json({ error: `Unknown provider: ${provider}` }, 400);
1688
+ }
1689
+ if (!config.tokenStore) {
1690
+ return res.json({ error: "tokenStore is required" }, 500);
1691
+ }
1692
+ const url = new URL(req.url);
1693
+ const code = url.searchParams.get("code");
1694
+ const state = url.searchParams.get("state");
1695
+ const error = url.searchParams.get("error");
1696
+ if (error) return res.json({ error: `OAuth error: ${error}` }, 400);
1697
+ if (!code || !state)
1698
+ return res.json({ error: "Missing code or state" }, 400);
1699
+ const stateData = await consumeOAuthState(config.tokenStore, state);
1700
+ if (!stateData || stateData.provider !== provider) {
1701
+ return res.json({ error: "Invalid or expired state" }, 400);
1702
+ }
1703
+ const meta = getProviderMeta(config, provider);
1704
+ if ("error" in meta) return res.json({ error: meta.error }, meta.status);
1705
+ const tokenRes = await fetch(meta.tokenUrl, {
1706
+ method: "POST",
1707
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1708
+ body: new URLSearchParams({
1709
+ code,
1710
+ client_id: meta.clientId,
1711
+ client_secret: meta.clientSecret,
1712
+ redirect_uri: callbackUrlFor(req, provider),
1713
+ grant_type: "authorization_code"
1714
+ })
1715
+ });
1716
+ if (!tokenRes.ok) {
1717
+ const body = await tokenRes.text();
1718
+ reportServerError(config.onError, {
1719
+ route: `auth/${provider}/cb`,
1720
+ method: req.method,
1721
+ status: 502,
1722
+ code: import_core8.UpupErrorCode.AUTH_PROVIDER_ERROR,
1723
+ message: "Token exchange failed",
1724
+ error: toSafeError(new Error(body.slice(0, 500)))
1725
+ });
1726
+ return res.json(
1727
+ {
1728
+ error: "Token exchange failed",
1729
+ code: import_core8.UpupErrorCode.AUTH_PROVIDER_ERROR
1730
+ },
1731
+ 502
1732
+ );
1733
+ }
1734
+ const payload = await tokenRes.json();
1735
+ const tokens = {
1736
+ accessToken: payload.access_token,
1737
+ expiresAt: payload.expires_in ? Date.now() + payload.expires_in * 1e3 : void 0,
1738
+ scope: payload.scope,
1739
+ tokenType: payload.token_type,
1740
+ refreshToken: payload.refresh_token
1741
+ };
1742
+ await setTokens(config.tokenStore, stateData.userId, provider, tokens);
1743
+ const validatedReturn = validateReturnTo(
1744
+ stateData.returnTo,
1745
+ req,
1746
+ config.cors
1747
+ );
1748
+ const targetOrigins = concreteAllowedOrigins(config.cors);
1749
+ return res.html(
1750
+ buildOAuthSuccessPage(provider, {
1751
+ targetOrigins,
1752
+ ...validatedReturn !== void 0 ? { returnTo: validatedReturn } : {}
1753
+ })
1754
+ );
1755
+ }
1756
+ function validateReturnTo(returnTo, req, cors) {
1757
+ if (!returnTo) return void 0;
1758
+ const serverOrigin = new URL(req.url).origin;
1759
+ let resolved;
1760
+ try {
1761
+ resolved = new URL(returnTo, serverOrigin);
1762
+ } catch {
1763
+ return void 0;
1764
+ }
1765
+ if (resolved.origin === serverOrigin) return resolved.toString();
1766
+ const concrete = (cors?.allowedOrigins ?? []).filter((o) => o !== "*");
1767
+ if (concrete.includes(resolved.origin)) return resolved.toString();
1768
+ return void 0;
1769
+ }
1770
+ function concreteAllowedOrigins(cors) {
1771
+ return (cors?.allowedOrigins ?? []).filter((o) => o !== "*");
1772
+ }
1773
+ function buildOAuthSuccessPage(provider, opts) {
1774
+ const safeProvider = provider.replace(/[^a-z0-9-]/gi, "");
1775
+ const { returnTo, targetOrigins } = opts;
1776
+ let postMessageScript;
1777
+ if (targetOrigins.length > 0) {
1778
+ postMessageScript = targetOrigins.map(
1779
+ (origin) => `window.opener.postMessage({ type: 'upup:oauth-success', provider: ${JSON.stringify(safeProvider)} }, ${JSON.stringify(origin)});`
1780
+ ).join("\n ");
1781
+ } else {
1782
+ postMessageScript = `window.opener.postMessage({ type: 'upup:oauth-success', provider: ${JSON.stringify(safeProvider)} }, '*' /* token-free payload */);`;
1783
+ }
1784
+ const elseBody = returnTo ? `window.location.replace(${JSON.stringify(returnTo)});` : `document.body.textContent = 'Connected to ' + ${JSON.stringify(safeProvider)} + '. You may close this window.';`;
1785
+ return `<!doctype html>
1786
+ <html><head><title>Connected</title></head><body>
1787
+ <script>
1788
+ try {
1789
+ if (window.opener) {
1790
+ ${postMessageScript}
1791
+ window.close();
1792
+ } else if (${JSON.stringify(returnTo ?? "")}) {
1793
+ ${elseBody}
1794
+ } else {
1795
+ document.body.textContent = 'Connected to ' + ${JSON.stringify(safeProvider)} + '. You may close this window.';
1796
+ }
1797
+ } catch (e) {
1798
+ document.body.textContent = 'Connected. You may close this window.';
1799
+ }
1800
+ </script>
1801
+ </body></html>`;
1802
+ }
1803
+ async function refreshAccessToken(config, provider, userId, tokens) {
1804
+ if (!tokens.refreshToken || !config.tokenStore) return null;
1805
+ const meta = getProviderMeta(config, provider);
1806
+ if ("error" in meta) return null;
1807
+ const res = await fetch(meta.tokenUrl, {
1808
+ method: "POST",
1809
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1810
+ body: new URLSearchParams({
1811
+ grant_type: "refresh_token",
1812
+ refresh_token: tokens.refreshToken,
1813
+ client_id: meta.clientId,
1814
+ client_secret: meta.clientSecret
1815
+ })
1816
+ });
1817
+ if (!res.ok) {
1818
+ const body = await res.text().catch(() => "");
1819
+ reportServerError(config.onError, {
1820
+ route: `auth/${provider}/refresh`,
1821
+ method: "POST",
1822
+ status: res.status,
1823
+ code: import_core8.UpupErrorCode.AUTH_EXPIRED,
1824
+ message: "Drive token refresh failed",
1825
+ error: toSafeError(new Error(body.slice(0, 500) || res.statusText))
1826
+ });
1827
+ await deleteTokens(config.tokenStore, userId, provider);
1828
+ return null;
1829
+ }
1830
+ const payload = await res.json();
1831
+ const next = {
1832
+ accessToken: payload.access_token,
1833
+ expiresAt: payload.expires_in ? Date.now() + payload.expires_in * 1e3 : void 0,
1834
+ scope: payload.scope ?? tokens.scope,
1835
+ tokenType: payload.token_type ?? tokens.tokenType,
1836
+ // Some providers omit refresh_token on refresh -> keep the existing one.
1837
+ refreshToken: payload.refresh_token ?? tokens.refreshToken
1838
+ };
1839
+ await setTokens(config.tokenStore, userId, provider, next);
1840
+ return next;
1841
+ }
1842
+
1843
+ // src/drive-routes.ts
1844
+ var import_core11 = require("@useupup/core");
1845
+
1846
+ // src/drive-clients.ts
1847
+ var import_core9 = require("@useupup/core");
1848
+ var DRIVE_CLIENTS = {
1849
+ "google-drive": {
1850
+ listFiles: listGoogleDriveFiles,
1851
+ fetchFile: fetchGoogleDriveFile
1852
+ },
1853
+ "one-drive": { listFiles: listOneDriveFiles, fetchFile: fetchOneDriveFile },
1854
+ dropbox: { listFiles: listDropboxFiles, fetchFile: fetchDropboxFile },
1855
+ box: { listFiles: listBoxFiles, fetchFile: fetchBoxFile }
1856
+ };
1857
+ function getDriveClient(provider) {
1858
+ return DRIVE_CLIENTS[provider];
1859
+ }
1860
+ async function driveFetch(url, init = {}) {
1861
+ const res = await fetch(url, init);
1862
+ if (res.status === 401) {
1863
+ throw new import_core9.UpupNetworkError("Drive API 401", 401);
1864
+ }
1865
+ if (!res.ok) {
1866
+ const text = await res.text().catch(() => "");
1867
+ throw new import_core9.UpupNetworkError(
1868
+ `Drive API ${res.status}: ${text.slice(0, 200) || res.statusText}`,
1869
+ res.status
1870
+ );
1871
+ }
1872
+ return res;
1873
+ }
1874
+ function escapeDriveQueryValue(value) {
1875
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1876
+ }
1877
+ function escapeODataSearchValue(value) {
1878
+ return value.replace(/'/g, "''");
1879
+ }
1880
+ function httpHeaderSafeJson(value) {
1881
+ return JSON.stringify(value).replace(
1882
+ /[\u007f-\uffff]/g,
1883
+ (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
1884
+ );
1885
+ }
1886
+ async function listGoogleDriveFiles(accessToken, opts) {
1887
+ const parent = opts.folderId ?? "root";
1888
+ const q = opts.search ? `name contains '${escapeDriveQueryValue(opts.search)}' and trashed = false` : `'${parent}' in parents and trashed = false`;
1889
+ const params = new URLSearchParams({
1890
+ q,
1891
+ fields: "files(id,name,size,mimeType,thumbnailLink,modifiedTime,iconLink)",
1892
+ pageSize: "200"
1893
+ });
1894
+ const res = await driveFetch(
1895
+ `https://www.googleapis.com/drive/v3/files?${params.toString()}`,
1896
+ { headers: { Authorization: `Bearer ${accessToken}` } }
1897
+ );
1898
+ const data = await res.json();
1899
+ return data.files.map((f) => ({
1900
+ id: f.id,
1901
+ name: f.name,
1902
+ size: f.size ? Number(f.size) : void 0,
1903
+ mimeType: f.mimeType,
1904
+ thumbnailUrl: f.thumbnailLink,
1905
+ isFolder: f.mimeType === "application/vnd.google-apps.folder",
1906
+ modifiedAt: f.modifiedTime
1907
+ }));
1908
+ }
1909
+ async function fetchGoogleDriveFile(accessToken, body) {
1910
+ const metaRes = await driveFetch(
1911
+ `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(
1912
+ body.fileId
1913
+ )}?fields=name,size,mimeType`,
1914
+ { headers: { Authorization: `Bearer ${accessToken}` } }
1915
+ );
1916
+ const meta = await metaRes.json();
1917
+ const dlRes = await driveFetch(
1918
+ `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(
1919
+ body.fileId
1920
+ )}?alt=media`,
1921
+ { headers: { Authorization: `Bearer ${accessToken}` } }
1922
+ );
1923
+ if (!dlRes.body) throw new import_core9.UpupNetworkError("Empty download body");
1924
+ return {
1925
+ stream: dlRes.body,
1926
+ size: Number(meta.size ?? body.size ?? 0),
1927
+ fileName: body.fileName ?? meta.name,
1928
+ mimeType: body.mimeType ?? meta.mimeType ?? "application/octet-stream"
1929
+ };
1930
+ }
1931
+ async function listOneDriveFiles(accessToken, opts) {
1932
+ const path = opts.search ? `/me/drive/root/search(q='${encodeURIComponent(escapeODataSearchValue(opts.search))}')` : opts.folderId ? `/me/drive/items/${encodeURIComponent(opts.folderId)}/children` : "/me/drive/root/children";
1933
+ const res = await driveFetch(`https://graph.microsoft.com/v1.0${path}`, {
1934
+ headers: { Authorization: `Bearer ${accessToken}` }
1935
+ });
1936
+ const data = await res.json();
1937
+ return data.value.map((f) => ({
1938
+ id: f.id,
1939
+ name: f.name,
1940
+ size: f.size,
1941
+ mimeType: f.file?.mimeType,
1942
+ isFolder: !!f.folder,
1943
+ modifiedAt: f.lastModifiedDateTime
1944
+ }));
1945
+ }
1946
+ async function fetchOneDriveFile(accessToken, body) {
1947
+ const metaRes = await driveFetch(
1948
+ `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(
1949
+ body.fileId
1950
+ )}`,
1951
+ { headers: { Authorization: `Bearer ${accessToken}` } }
1952
+ );
1953
+ const meta = await metaRes.json();
1954
+ const dlRes = await driveFetch(
1955
+ `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(
1956
+ body.fileId
1957
+ )}/content`,
1958
+ { headers: { Authorization: `Bearer ${accessToken}` } }
1959
+ );
1960
+ if (!dlRes.body) throw new import_core9.UpupNetworkError("Empty download body");
1961
+ return {
1962
+ stream: dlRes.body,
1963
+ size: meta.size ?? body.size ?? 0,
1964
+ fileName: body.fileName ?? meta.name,
1965
+ mimeType: body.mimeType ?? meta.file?.mimeType ?? "application/octet-stream"
1966
+ };
1967
+ }
1968
+ async function listDropboxFiles(accessToken, opts) {
1969
+ const endpoint = opts.search ? "https://api.dropboxapi.com/2/files/search_v2" : "https://api.dropboxapi.com/2/files/list_folder";
1970
+ const body = opts.search ? { query: opts.search } : { path: opts.folderId ?? "", recursive: false };
1971
+ const res = await driveFetch(endpoint, {
1972
+ method: "POST",
1973
+ headers: {
1974
+ Authorization: `Bearer ${accessToken}`,
1975
+ "Content-Type": "application/json"
1976
+ },
1977
+ body: JSON.stringify(body)
1978
+ });
1979
+ const data = await res.json();
1980
+ const entries = "entries" in data ? data.entries : data.matches.map((m) => m.metadata.metadata);
1981
+ return entries.filter((e) => e[".tag"] !== "deleted").map((e) => ({
1982
+ id: e.path_lower ?? e.id,
1983
+ name: e.name,
1984
+ size: "size" in e ? e.size : void 0,
1985
+ isFolder: e[".tag"] === "folder",
1986
+ modifiedAt: "server_modified" in e ? e.server_modified : void 0
1987
+ }));
1988
+ }
1989
+ async function fetchDropboxFile(accessToken, body) {
1990
+ const dlRes = await driveFetch(
1991
+ "https://content.dropboxapi.com/2/files/download",
1992
+ {
1993
+ method: "POST",
1994
+ headers: {
1995
+ Authorization: `Bearer ${accessToken}`,
1996
+ "Dropbox-API-Arg": httpHeaderSafeJson({ path: body.fileId })
1997
+ }
1998
+ }
1999
+ );
2000
+ if (!dlRes.body) throw new import_core9.UpupNetworkError("Empty download body");
2001
+ const apiResult = dlRes.headers.get("Dropbox-API-Result");
2002
+ let name = body.fileName ?? "download";
2003
+ let size = body.size ?? 0;
2004
+ if (apiResult) {
2005
+ try {
2006
+ const parsed = JSON.parse(apiResult);
2007
+ name = body.fileName ?? parsed.name ?? name;
2008
+ size = parsed.size ?? size;
2009
+ } catch {
2010
+ }
2011
+ }
2012
+ return {
2013
+ stream: dlRes.body,
2014
+ size,
2015
+ fileName: name,
2016
+ mimeType: body.mimeType ?? dlRes.headers.get("Content-Type") ?? "application/octet-stream"
2017
+ };
2018
+ }
2019
+ async function listBoxFiles(accessToken, opts) {
2020
+ if (opts.search) {
2021
+ const params = new URLSearchParams({
2022
+ query: opts.search,
2023
+ limit: "200"
2024
+ });
2025
+ const res2 = await driveFetch(
2026
+ `https://api.box.com/2.0/search?${params.toString()}`,
2027
+ { headers: { Authorization: `Bearer ${accessToken}` } }
2028
+ );
2029
+ const data2 = await res2.json();
2030
+ return data2.entries.map((e) => ({
2031
+ id: e.id,
2032
+ name: e.name,
2033
+ size: e.size,
2034
+ isFolder: e.type === "folder",
2035
+ modifiedAt: e.modified_at
2036
+ }));
2037
+ }
2038
+ const folderId = opts.folderId ?? "0";
2039
+ const res = await driveFetch(
2040
+ `https://api.box.com/2.0/folders/${encodeURIComponent(folderId)}/items?limit=200&fields=id,name,size,type,modified_at`,
2041
+ { headers: { Authorization: `Bearer ${accessToken}` } }
2042
+ );
2043
+ const data = await res.json();
2044
+ return data.entries.map((e) => ({
2045
+ id: e.id,
2046
+ name: e.name,
2047
+ size: e.size,
2048
+ isFolder: e.type === "folder",
2049
+ modifiedAt: e.modified_at
2050
+ }));
2051
+ }
2052
+ async function fetchBoxFile(accessToken, body) {
2053
+ const metaRes = await driveFetch(
2054
+ `https://api.box.com/2.0/files/${encodeURIComponent(body.fileId)}`,
2055
+ { headers: { Authorization: `Bearer ${accessToken}` } }
2056
+ );
2057
+ const meta = await metaRes.json();
2058
+ const dlRes = await driveFetch(
2059
+ `https://api.box.com/2.0/files/${encodeURIComponent(body.fileId)}/content`,
2060
+ {
2061
+ headers: { Authorization: `Bearer ${accessToken}` },
2062
+ redirect: "follow"
2063
+ }
2064
+ );
2065
+ if (!dlRes.body) throw new import_core9.UpupNetworkError("Empty download body");
2066
+ return {
2067
+ stream: dlRes.body,
2068
+ size: meta.size ?? body.size ?? 0,
2069
+ fileName: body.fileName ?? meta.name,
2070
+ mimeType: body.mimeType ?? dlRes.headers.get("Content-Type") ?? "application/octet-stream"
2071
+ };
2072
+ }
2073
+
2074
+ // src/drive-routes.ts
2075
+ async function handleListFiles(req, config, provider, res) {
2076
+ if (!isValidProvider(provider)) {
2077
+ return res.json({ error: `Unknown provider: ${provider}` }, 400);
2078
+ }
2079
+ if (!config.tokenStore)
2080
+ return res.json({ error: "tokenStore is required" }, 500);
2081
+ const userId = await resolveUserId(config, req);
2082
+ if (!userId) return res.json({ error: "Unauthenticated" }, 401);
2083
+ let tokens = await getTokens(config.tokenStore, userId, provider);
2084
+ if (!tokens) {
2085
+ return res.json({ reauth: true, provider }, 401);
2086
+ }
2087
+ if (tokens.refreshToken && tokens.expiresAt && Date.now() > tokens.expiresAt - 3e4) {
2088
+ const refreshed = await refreshAccessToken(
2089
+ config,
2090
+ provider,
2091
+ userId,
2092
+ tokens
2093
+ );
2094
+ if (!refreshed) {
2095
+ return res.json({ reauth: true, provider }, 401);
2096
+ }
2097
+ tokens = refreshed;
2098
+ }
2099
+ const url = new URL(req.url);
2100
+ const folderId = url.searchParams.get("folderId") ?? void 0;
2101
+ const search = url.searchParams.get("search") ?? void 0;
2102
+ try {
2103
+ const files = await getDriveClient(provider).listFiles(
2104
+ tokens.accessToken,
2105
+ { folderId, search }
2106
+ );
2107
+ return res.json({ provider, files }, 200);
2108
+ } catch (err) {
2109
+ if (err instanceof import_core11.UpupNetworkError && err.status === 401) {
2110
+ await deleteTokens(config.tokenStore, userId, provider);
2111
+ return res.json({ reauth: true, provider }, 401);
2112
+ }
2113
+ return res.fail(
2114
+ `files/${provider}`,
2115
+ req.method,
2116
+ 500,
2117
+ import_core11.UpupErrorCode.STORAGE_ERROR,
2118
+ "Drive request failed",
2119
+ err
2120
+ );
2121
+ }
2122
+ }
2123
+ async function handleFileTransfer(req, config, provider, res) {
2124
+ if (!isValidProvider(provider)) {
2125
+ return res.json({ error: `Unknown provider: ${provider}` }, 400);
2126
+ }
2127
+ if (!config.tokenStore)
2128
+ return res.json({ error: "tokenStore is required" }, 500);
2129
+ const userId = await resolveUserId(config, req);
2130
+ if (!userId) return res.json({ error: "Unauthenticated" }, 401);
2131
+ let tokens = await getTokens(config.tokenStore, userId, provider);
2132
+ if (!tokens) return res.json({ reauth: true, provider }, 401);
2133
+ if (tokens.refreshToken && tokens.expiresAt && Date.now() > tokens.expiresAt - 3e4) {
2134
+ const refreshed = await refreshAccessToken(
2135
+ config,
2136
+ provider,
2137
+ userId,
2138
+ tokens
2139
+ );
2140
+ if (!refreshed) {
2141
+ return res.json({ reauth: true, provider }, 401);
2142
+ }
2143
+ tokens = refreshed;
2144
+ }
2145
+ let body;
2146
+ try {
2147
+ body = await req.json();
2148
+ } catch {
2149
+ return res.json({ error: "Invalid JSON body" }, 400);
2150
+ }
2151
+ if (!body.fileId) return res.json({ error: "Missing fileId" }, 400);
2152
+ if (config.maxFileSize && typeof body.size === "number" && body.size > config.maxFileSize) {
2153
+ return res.json({ error: "File too large" }, 413);
2154
+ }
2155
+ if (!matchesAllowedType(body.mimeType ?? "", config.allowedTypes)) {
2156
+ return res.json({ error: "File type not allowed" }, 415);
2157
+ }
2158
+ try {
2159
+ const { stream, size, fileName, mimeType } = await getDriveClient(
2160
+ provider
2161
+ ).fetchFile(tokens.accessToken, body);
2162
+ const storage = await resolveStorage(config, {
2163
+ req,
2164
+ phase: "drive-transfer",
2165
+ userId,
2166
+ ...body.metadata !== void 0 ? { metadata: body.metadata } : {},
2167
+ fileName,
2168
+ contentType: mimeType,
2169
+ size
2170
+ });
2171
+ const { transferDriveFileToS3: transferDriveFileToS32 } = await Promise.resolve().then(() => (init_transfer(), transfer_exports));
2172
+ const result = await transferDriveFileToS32({
2173
+ stream,
2174
+ size,
2175
+ fileName,
2176
+ mimeType,
2177
+ storage,
2178
+ // Enforce maxFileSize against the ACTUAL streamed bytes, and route
2179
+ // an abort-cleanup failure through onError instead of swallowing it
2180
+ // (F-743 / F-744).
2181
+ maxBytes: config.maxFileSize,
2182
+ onError: config.onError,
2183
+ requestId: res.requestId,
2184
+ downloadUrlExpiresIn: config.downloadUrlExpiresIn
2185
+ });
2186
+ await runPostCompletionHooks(
2187
+ config,
2188
+ res,
2189
+ `files/${provider}/transfer`,
2190
+ req.method,
2191
+ async () => {
2192
+ if (config.hooks?.onFileUploaded)
2193
+ await config.hooks.onFileUploaded(result, req);
2194
+ }
2195
+ );
2196
+ return res.json({ provider, ...result }, 200);
2197
+ } catch (err) {
2198
+ if (err instanceof import_core11.UpupNetworkError && err.status === 401) {
2199
+ await deleteTokens(config.tokenStore, userId, provider);
2200
+ return res.json({ reauth: true, provider }, 401);
2201
+ }
2202
+ return res.fail(
2203
+ `files/${provider}/transfer`,
2204
+ req.method,
2205
+ 500,
2206
+ import_core11.UpupErrorCode.STORAGE_ERROR,
2207
+ "Drive request failed",
2208
+ err
2209
+ );
2210
+ }
2211
+ }
2212
+
2213
+ // src/handler.ts
2214
+ function createUpupHandler(config) {
2215
+ assertUploadTokenSecret(config.uploadTokenSecret);
2216
+ validateServerConfig(config);
2217
+ if (!isStorageResolver(config.storage)) assertS3Storage(config.storage);
2218
+ if ((config.providers || config.tokenStore) && !config.getUserId && !config.allowAnonymous) {
2219
+ throw new import_core12.UpupConfigError(
2220
+ "[@useupup/server] drive providers / tokenStore require config.getUserId to scope tokens per user. Set getUserId, or set allowAnonymous:true to intentionally share ONE anonymous namespace (demos only)."
2221
+ );
2222
+ }
2223
+ if (config.allowAnonymousUploads) {
2224
+ console.warn(
2225
+ "[@useupup/server] allowAnonymousUploads:true \u2014 /presign and /multipart/init accept UNAUTHENTICATED uploads under a shared anonymous namespace. Demos / upstream-auth deployments only; never enable in multi-tenant production."
2226
+ );
2227
+ }
2228
+ return async (req) => {
2229
+ const url = new URL(req.url);
2230
+ const path = url.pathname.replace(/\/+$/, "") || "/";
2231
+ const res = createResponder(req, config);
2232
+ try {
2233
+ if (req.method === "OPTIONS") {
2234
+ return res.noContent();
2235
+ }
2236
+ if (req.method === "GET" && path.endsWith("/health")) {
2237
+ return await handleHealth(config, res);
2238
+ }
2239
+ if (config.auth) {
2240
+ const authorized = await config.auth(req);
2241
+ if (!authorized) {
2242
+ return res.json({ error: "Unauthorized" }, 401);
2243
+ }
2244
+ }
2245
+ if (req.method === "POST" && path.endsWith("/presign")) {
2246
+ return await handlePresign(req, config, res);
2247
+ }
2248
+ if (req.method === "POST" && path.endsWith("/multipart/init")) {
2249
+ return await handleMultipartInit(req, config, res);
2250
+ }
2251
+ if (req.method === "POST" && path.endsWith("/multipart/sign-part")) {
2252
+ return await handleMultipartSignPart(req, config, res);
2253
+ }
2254
+ if (req.method === "POST" && path.endsWith("/multipart/complete")) {
2255
+ return await handleMultipartComplete(req, config, res);
2256
+ }
2257
+ if (req.method === "POST" && path.endsWith("/multipart/abort")) {
2258
+ return await handleMultipartAbort(req, config, res);
2259
+ }
2260
+ if (req.method === "POST" && path.endsWith("/multipart/resume") && (config.multipartResumeWindowSeconds ?? DEFAULT_MULTIPART_RESUME_WINDOW_SECONDS) > 0) {
2261
+ return await handleMultipartResume(req, config, res);
2262
+ }
2263
+ const authMatch = path.match(/\/auth\/([\w-]+?)(?:\/(cb))?$/);
2264
+ if (req.method === "GET" && authMatch) {
2265
+ const provider = authMatch[1];
2266
+ if (provider === void 0) {
2267
+ return res.json({ error: "Not found" }, 404);
2268
+ }
2269
+ const isCallback = authMatch[2] === "cb";
2270
+ if (isCallback) {
2271
+ return await handleOAuthCallback(req, config, provider, res);
2272
+ }
2273
+ return await handleOAuthRedirect(req, config, provider, res);
2274
+ }
2275
+ const filesMatch = path.match(
2276
+ /\/files\/([\w-]+?)(?:\/(transfer))?$/
2277
+ );
2278
+ if (filesMatch) {
2279
+ const provider = filesMatch[1];
2280
+ if (provider === void 0) {
2281
+ return res.json({ error: "Not found" }, 404);
2282
+ }
2283
+ const isTransfer = filesMatch[2] === "transfer";
2284
+ if (req.method === "POST" && isTransfer) {
2285
+ return await handleFileTransfer(req, config, provider, res);
2286
+ }
2287
+ if (req.method === "GET" && !isTransfer) {
2288
+ return await handleListFiles(req, config, provider, res);
2289
+ }
2290
+ }
2291
+ return res.json({ error: "Not found" }, 404);
2292
+ } catch (error) {
2293
+ return res.fail(
2294
+ "router",
2295
+ req.method,
2296
+ 500,
2297
+ import_core12.UpupErrorCode.STORAGE_ERROR,
2298
+ "Internal error",
2299
+ error
2300
+ );
2301
+ }
2302
+ };
2303
+ }
2304
+
2305
+ // src/node-http-bridge.ts
2306
+ function nodeHeadersToWeb(h) {
2307
+ const headers = new Headers();
2308
+ for (const [k, v] of Object.entries(h)) {
2309
+ if (v === void 0) continue;
2310
+ if (Array.isArray(v)) for (const one of v) headers.append(k, one);
2311
+ else headers.set(k, v);
2312
+ }
2313
+ return headers;
2314
+ }
2315
+ function toWebRequest(input) {
2316
+ const hasBody = input.method !== "GET" && input.method !== "HEAD";
2317
+ return new Request(input.url, {
2318
+ method: input.method,
2319
+ headers: nodeHeadersToWeb(input.headers),
2320
+ ...hasBody && input.body != null ? { body: input.body } : {}
2321
+ });
2322
+ }
2323
+ async function writeWebResponse(sink, webRes) {
2324
+ sink.status(webRes.status);
2325
+ webRes.headers.forEach((value, key) => {
2326
+ if (key.toLowerCase() === "content-length") return;
2327
+ sink.setHeader(key, value);
2328
+ });
2329
+ sink.send(Buffer.from(await webRes.arrayBuffer()));
2330
+ }
2331
+
2332
+ // src/fastify.ts
2333
+ function createUpupPlugin(config, opts) {
2334
+ const handler = createUpupHandler(config);
2335
+ const routePath = opts?.path ?? "/upup/*";
2336
+ return (fastify) => {
2337
+ fastify.all(
2338
+ routePath,
2339
+ async (request, reply) => {
2340
+ const url = `${request.protocol}://${request.hostname}${request.url}`;
2341
+ const webReq = toWebRequest({
2342
+ url,
2343
+ method: request.method,
2344
+ headers: request.headers,
2345
+ body: request.method !== "GET" && request.method !== "HEAD" ? JSON.stringify(request.body) : void 0
2346
+ });
2347
+ const webRes = await handler(webReq);
2348
+ await writeWebResponse(
2349
+ {
2350
+ status: (c) => reply.code(c),
2351
+ setHeader: (k, v) => reply.header(k, v),
2352
+ send: (b) => reply.send(b)
2353
+ },
2354
+ webRes
2355
+ );
2356
+ }
2357
+ );
2358
+ return Promise.resolve();
2359
+ };
2360
+ }
2361
+ // Annotate the CommonJS export names for ESM import in node:
2362
+ 0 && (module.exports = {
2363
+ createUpupPlugin
2364
+ });
2365
+ //# sourceMappingURL=fastify.cjs.map