@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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/dist/config-3b434689.d.cts +297 -0
- package/dist/config-3b434689.d.ts +297 -0
- package/dist/express.cjs +2360 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +19 -0
- package/dist/express.d.ts +19 -0
- package/dist/express.js +2358 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.cjs +2365 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +32 -0
- package/dist/fastify.d.ts +32 -0
- package/dist/fastify.js +2363 -0
- package/dist/fastify.js.map +1 -0
- package/dist/handler-030ee446.d.cts +6 -0
- package/dist/handler-9f2df17e.d.ts +6 -0
- package/dist/hono.cjs +2314 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +7 -0
- package/dist/hono.d.ts +7 -0
- package/dist/hono.js +2312 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +2349 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +2342 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +2359 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +27 -0
- package/dist/next.d.ts +27 -0
- package/dist/next.js +2355 -0
- package/dist/next.js.map +1 -0
- package/dist/node-http-bridge.cjs +59 -0
- package/dist/node-http-bridge.cjs.map +1 -0
- package/dist/node-http-bridge.d.cts +29 -0
- package/dist/node-http-bridge.d.ts +29 -0
- package/dist/node-http-bridge.js +32 -0
- package/dist/node-http-bridge.js.map +1 -0
- package/package.json +102 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/s3-client.ts","../src/providers/aws.ts","../src/observability.ts","../src/transfer.ts","../src/express.ts","../src/handler.ts","../src/uploadToken.ts","../src/validate-config.ts","../src/storage.ts","../src/resolve-storage.ts","../src/health.ts","../src/respond.ts","../src/upload-routes.ts","../src/tokenStore.ts","../src/key.ts","../src/oauth.ts","../src/drive-routes.ts","../src/drive-clients.ts","../src/node-http-bridge.ts"],"sourcesContent":["import { S3Client, type S3ClientConfig } from '@aws-sdk/client-s3'\nimport type { UpupStorageConfig } from '../config'\n\n/**\n * Build the AWS SDK S3 client config from upup storage settings.\n * Pure + exported so it can be unit-tested without constructing a client.\n * When `endpoint` is set (MinIO / R2 / DO Spaces / on-prem) path-style\n * addressing is enabled by default — MinIO requires it.\n */\nexport function buildS3ClientConfig(\n storage: UpupStorageConfig,\n): S3ClientConfig {\n const config: S3ClientConfig = { region: storage.region }\n if (storage.accessKeyId && storage.secretAccessKey) {\n config.credentials = {\n accessKeyId: storage.accessKeyId,\n secretAccessKey: storage.secretAccessKey,\n }\n }\n if (storage.endpoint) {\n config.endpoint = storage.endpoint\n config.forcePathStyle = storage.forcePathStyle ?? true\n }\n return config\n}\n\n// Per-destination client cache (#337). With a static config this holds exactly\n// one entry and replaces the per-call `new S3Client(...)`; with a resolver it\n// keeps a multi-bucket deployment from building a fresh client (and its\n// connection pool) on every presign. Keyed on the connection identity —\n// endpoint, region, bucket, and the access key ID so a credential rotation\n// yields a NEW client rather than reusing one signing with the old key. The\n// SECRET key is never part of the key and is never logged.\nconst clients = new Map<string, S3Client>()\n\nfunction cacheKey(storage: UpupStorageConfig): string {\n return [\n storage.endpoint ?? '',\n storage.region,\n storage.bucket,\n storage.accessKeyId ?? '',\n String(storage.forcePathStyle ?? ''),\n ].join('\u0000')\n}\n\nexport function createS3Client(storage: UpupStorageConfig): S3Client {\n const key = cacheKey(storage)\n const cached = clients.get(key)\n if (cached) return cached\n const client = new S3Client(buildS3ClientConfig(storage))\n clients.set(key, client)\n return client\n}\n\n/** Test-only: drop every cached client so a suite can assert on construction. */\nexport function _resetS3ClientCacheForTests(): void {\n clients.clear()\n}\n","import {\r\n PutObjectCommand,\r\n GetObjectCommand,\r\n CreateMultipartUploadCommand,\r\n UploadPartCommand,\r\n CompleteMultipartUploadCommand,\r\n AbortMultipartUploadCommand,\r\n ListPartsCommand,\r\n HeadBucketCommand,\r\n} from '@aws-sdk/client-s3'\r\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner'\r\nimport { UpupStorageError } from '@useupup/core'\r\nimport type {\r\n PresignedUrlResponse,\r\n MultipartInitResponse,\r\n MultipartSignPartResponse,\r\n MultipartCompleteResponse,\r\n MultipartAbortResponse,\r\n MultipartListPartsResponse,\r\n MultipartPart,\r\n} from '@useupup/core'\r\nimport type { UpupStorageConfig } from '../config'\r\nimport { createS3Client } from './s3-client'\r\n\r\nconst DEFAULT_EXPIRES_IN = 3600\r\n// Exported: the fallback for `config.downloadUrlExpiresIn` (#343). Every signed\r\n// GET this package hands out resolves through here, so the 3-day policy has one\r\n// definition rather than one per call site.\r\nexport const DEFAULT_DOWNLOAD_URL_EXPIRES_IN = 3600 * 24 * 3 // 3 days\r\n// Exported: this is the one canonical home for the 5 MiB S3 part-size floor —\r\n// also the fixed memory-safety cap `transfer.ts` uses for its singlePut/\r\n// multipart routing decision (F-501, F-653).\r\nexport const MIN_PART_SIZE = 5 * 1024 * 1024 // 5 MiB\r\nconst MAX_PARTS = 10_000\r\n\r\n// Exported for its boundary tests: the MAX_PARTS clamp is what keeps a\r\n// >48.8 GiB file from failing at part 10,001, and it must stay covered.\r\nexport function computePartSize(\r\n fileSize: number,\r\n chunkSizeBytes?: number,\r\n): number {\r\n let partSize = chunkSizeBytes ?? MIN_PART_SIZE\r\n if (partSize < MIN_PART_SIZE) partSize = MIN_PART_SIZE\r\n const minPartSizeForFile = Math.ceil(fileSize / MAX_PARTS)\r\n if (partSize < minPartSizeForFile) partSize = minPartSizeForFile\r\n return partSize\r\n}\r\n\r\n// Exported: transfer.ts's drive-transfer path uses this same signed-URL\r\n// producer as the direct-presign path below, so the 3-day TTL + signing body\r\n// are single-sourced instead of duplicated (F-653).\r\nexport async function generateSignedPublicUrl(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n expiresIn = DEFAULT_DOWNLOAD_URL_EXPIRES_IN,\r\n): Promise<string> {\r\n const client = createS3Client(storage)\r\n return getSignedUrl(\r\n client,\r\n new GetObjectCommand({ Bucket: storage.bucket, Key: key }),\r\n { expiresIn },\r\n )\r\n}\r\n\r\nexport async function generatePresignedUrl(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n contentType: string,\r\n contentLength: number,\r\n expiresIn = DEFAULT_EXPIRES_IN,\r\n downloadUrlExpiresIn?: number,\r\n): Promise<PresignedUrlResponse> {\r\n const client = createS3Client(storage)\r\n\r\n const command = new PutObjectCommand({\r\n Bucket: storage.bucket,\r\n Key: key,\r\n ContentType: contentType,\r\n ContentLength: contentLength,\r\n })\r\n\r\n const uploadUrl = await getSignedUrl(client, command, {\r\n expiresIn,\r\n // Bind content-length into the signature so the PUT body cannot exceed the\r\n // approved size (S1). Browsers/Node set Content-Length from the body; a\r\n // larger body changes it and S3 rejects the signature.\r\n signableHeaders: new Set(['content-type', 'content-length']),\r\n })\r\n\r\n const downloadUrl = await generateSignedPublicUrl(\r\n storage,\r\n key,\r\n downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN,\r\n )\r\n\r\n return {\r\n key,\r\n downloadUrl,\r\n uploadUrl,\r\n uploadHeaders: {\r\n 'Content-Type': contentType || 'application/octet-stream',\r\n },\r\n expiresIn,\r\n }\r\n}\r\n\r\nexport async function initiateMultipartUpload(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n contentType: string,\r\n fileSize: number,\r\n expiresIn = DEFAULT_EXPIRES_IN,\r\n chunkSizeBytes?: number,\r\n): Promise<MultipartInitResponse> {\r\n const client = createS3Client(storage)\r\n\r\n const command = new CreateMultipartUploadCommand({\r\n Bucket: storage.bucket,\r\n Key: key,\r\n ContentType: contentType,\r\n })\r\n\r\n const response = await client.send(command)\r\n\r\n if (!response.UploadId) {\r\n throw new UpupStorageError(\r\n 'Failed to initiate multipart upload: no UploadId',\r\n storage.type,\r\n 'multipart-init',\r\n )\r\n }\r\n\r\n const partSize = computePartSize(fileSize, chunkSizeBytes)\r\n\r\n return { key, uploadId: response.UploadId, partSize, expiresIn }\r\n}\r\n\r\nexport async function generatePresignedPartUrl(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n uploadId: string,\r\n partNumber: number,\r\n expiresIn = DEFAULT_EXPIRES_IN,\r\n): Promise<MultipartSignPartResponse> {\r\n const client = createS3Client(storage)\r\n\r\n const command = new UploadPartCommand({\r\n Bucket: storage.bucket,\r\n Key: key,\r\n UploadId: uploadId,\r\n PartNumber: partNumber,\r\n })\r\n\r\n const uploadUrl = await getSignedUrl(client, command, { expiresIn })\r\n\r\n return { uploadUrl, expiresIn }\r\n}\r\n\r\nexport async function completeMultipartUpload(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n uploadId: string,\r\n parts: MultipartPart[],\r\n downloadUrlExpiresIn?: number,\r\n): Promise<MultipartCompleteResponse> {\r\n const client = createS3Client(storage)\r\n\r\n const command = new CompleteMultipartUploadCommand({\r\n Bucket: storage.bucket,\r\n Key: key,\r\n UploadId: uploadId,\r\n MultipartUpload: {\r\n Parts: parts\r\n .sort((a, b) => a.partNumber - b.partNumber)\r\n .map(p => ({ PartNumber: p.partNumber, ETag: p.eTag })),\r\n },\r\n })\r\n\r\n const result = await client.send(command)\r\n const downloadUrl = await generateSignedPublicUrl(\r\n storage,\r\n key,\r\n downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN,\r\n )\r\n\r\n return {\r\n key,\r\n downloadUrl,\r\n ...(result.ETag !== undefined ? { etag: result.ETag } : {}),\r\n }\r\n}\r\n\r\nexport async function abortMultipartUpload(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n uploadId: string,\r\n): Promise<MultipartAbortResponse> {\r\n const client = createS3Client(storage)\r\n\r\n const command = new AbortMultipartUploadCommand({\r\n Bucket: storage.bucket,\r\n Key: key,\r\n UploadId: uploadId,\r\n })\r\n\r\n await client.send(command)\r\n\r\n return { ok: true }\r\n}\r\n\r\n/**\r\n * Parts S3/MinIO currently holds for an in-progress multipart upload, each with\r\n * the byte count actually stored (`size`, straight off ListParts' `Size`). The\r\n * size is what makes a resume safe rather than hopeful: the client re-derives\r\n * its own chunk boundaries and refuses to complete unless every stored part\r\n * lands exactly where its chunking would have put it, so a part written by a\r\n * different chunk size can never be spliced into the middle of the object.\r\n */\r\nexport async function listMultipartParts(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n uploadId: string,\r\n): Promise<MultipartListPartsResponse> {\r\n const client = createS3Client(storage)\r\n const parts: MultipartPart[] = []\r\n let partNumberMarker: string | undefined\r\n\r\n for (;;) {\r\n const command = new ListPartsCommand({\r\n Bucket: storage.bucket,\r\n Key: key,\r\n UploadId: uploadId,\r\n PartNumberMarker: partNumberMarker,\r\n })\r\n\r\n const response = await client.send(command)\r\n\r\n if (response.Parts) {\r\n for (const part of response.Parts) {\r\n if (part.PartNumber != null && part.ETag) {\r\n parts.push({\r\n partNumber: part.PartNumber,\r\n eTag: part.ETag,\r\n ...(part.Size !== undefined ? { size: part.Size } : {}),\r\n })\r\n }\r\n }\r\n }\r\n\r\n if (!response.IsTruncated) break\r\n partNumberMarker = String(\r\n response.Parts?.[response.Parts.length - 1]?.PartNumber,\r\n )\r\n }\r\n\r\n return { parts }\r\n}\r\n\r\n/**\r\n * Server-internal only (not part of the shared `@useupup/core` multipart contract):\r\n * sums the ACTUAL bytes S3 has received for an in-progress multipart upload, by\r\n * reading `Size` off each part from `ListParts`. Used to enforce the signed size\r\n * envelope (`smin`/`smax`) at complete-time — the client can assert whatever\r\n * `size` it likes at init, but the bytes actually stored are authoritative\r\n * (closes the multipart variant of S1: init small, upload large parts, complete).\r\n * Paginates on `IsTruncated`, mirroring `listMultipartParts`.\r\n *\r\n * Deliberately NOT re-expressed as a sum over `listMultipartParts` now that the\r\n * latter reports `size`: that function DROPS any part S3 reports without a\r\n * PartNumber or ETag, and this one must count every byte the provider says it\r\n * holds. Routing the envelope check through a filtered list would let bytes go\r\n * unbilled against smax — a security regression, not a tidy-up.\r\n */\r\nexport async function getMultipartUploadedSize(\r\n storage: UpupStorageConfig,\r\n key: string,\r\n uploadId: string,\r\n): Promise<number> {\r\n const client = createS3Client(storage)\r\n let total = 0\r\n let partNumberMarker: string | undefined\r\n\r\n for (;;) {\r\n const command = new ListPartsCommand({\r\n Bucket: storage.bucket,\r\n Key: key,\r\n UploadId: uploadId,\r\n PartNumberMarker: partNumberMarker,\r\n })\r\n\r\n const response = await client.send(command)\r\n\r\n if (response.Parts) {\r\n for (const part of response.Parts) {\r\n total += part.Size ?? 0\r\n }\r\n }\r\n\r\n if (!response.IsTruncated) break\r\n partNumberMarker = String(\r\n response.Parts?.[response.Parts.length - 1]?.PartNumber,\r\n )\r\n }\r\n\r\n return total\r\n}\r\n\r\n/**\r\n * Cheap reachability probe for /health: a HeadBucketCommand touches S3/MinIO\r\n * without listing or transferring any object data. Used by handleHealth,\r\n * TTL-cached there so a probing client can't hammer the real provider.\r\n */\r\nexport async function checkStorageReachable(\r\n storage: UpupStorageConfig,\r\n): Promise<{ ok: true } | { ok: false; error: unknown }> {\r\n try {\r\n const client = createS3Client(storage)\r\n await client.send(new HeadBucketCommand({ Bucket: storage.bucket }))\r\n return { ok: true }\r\n } catch (error) {\r\n // upup-catch: reachability probe returns the failure in a Result for the\r\n // caller (handleHealth) to log/shape — deliberately not thrown here.\r\n return { ok: false, error }\r\n }\r\n}\r\n","// packages/server/src/observability.ts\r\n//\r\n// The one logger seam for @useupup/server. Every error path in handler.ts routes\r\n// through reportServerError() so integrators have exactly one place to wire an\r\n// external sink (Datadog/Sentry/etc.) via config.onError, and a sane default\r\n// (console.error) means the incident-blind-spot case is closed out of the box.\r\n//\r\n// Redaction contract: a UpupServerErrorEvent may ONLY be built from a static\r\n// route string, req.method, the HTTP status, a machine code, a generic\r\n// message, and the caught error's name/message/stack. Callers must never put\r\n// request bodies, tokens, uploadTokenSecret, S3 credentials, signatures, or\r\n// Authorization headers into an event.\r\n\r\nexport type UpupServerErrorEvent = {\r\n route: string\r\n method: string\r\n status: number\r\n code: string\r\n message: string\r\n requestId?: string | undefined\r\n error?: { name: string; message: string; stack?: string | undefined }\r\n}\r\n\r\nexport type UpupServerLogger = (event: UpupServerErrorEvent) => void\r\n\r\nexport function toSafeError(\r\n e: unknown,\r\n): NonNullable<UpupServerErrorEvent['error']> {\r\n return e instanceof Error\r\n ? { name: e.name, message: e.message, stack: e.stack }\r\n : { name: 'NonError', message: String(e) }\r\n}\r\n\r\n// Best-effort redaction of credential-shaped substrings from free-form error\r\n// text (an error's message + stack) before it reaches config.onError. This\r\n// makes the module's redaction contract STRUCTURAL rather than a comment a\r\n// caller must remember (F-746): even an error whose message accidentally\r\n// interpolated a token/Authorization header/signed-URL is scrubbed at this one\r\n// seam. It is conservative by design — each pattern targets a known secret\r\n// shape and leaves ordinary stack frames (file paths, function names) intact;\r\n// it is defense-in-depth, not licence to build an event from a secret.\r\nconst SCRUBBERS: ReadonlyArray<readonly [RegExp, string]> = [\r\n // `Authorization: Bearer xxx` / `authorization=xxx` header dumps.\r\n [/(authorization)(\\s*[:=]\\s*)(?:bearer\\s+)?[^\\s,;\"']+/gi, '$1$2[REDACTED]'],\r\n // Standalone bearer tokens not preceded by the header name.\r\n [/\\bbearer\\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED]'],\r\n // SigV4 signature / credential / security-token query params or headers.\r\n [\r\n /(x-amz-(?:signature|credential|security-token))(\\s*[:=]\\s*)[^\\s&,;\"']+/gi,\r\n '$1$2[REDACTED]',\r\n ],\r\n // AWS access-key ids.\r\n [/\\bAKIA[0-9A-Z]{16}\\b/g, '[REDACTED_AWS_KEY]'],\r\n]\r\n\r\nfunction scrubSensitive(text: string): string {\r\n return SCRUBBERS.reduce((acc, [re, repl]) => acc.replace(re, repl), text)\r\n}\r\n\r\nconst defaultLogger: UpupServerLogger = event => {\r\n console.error('[upup:server]', JSON.stringify(event))\r\n}\r\n\r\nexport function reportServerError(\r\n logger: UpupServerLogger | undefined,\r\n event: UpupServerErrorEvent,\r\n): void {\r\n // Scrub at the single seam every error path routes through, so redaction\r\n // cannot be forgotten by a caller that hand-built the event (F-746).\r\n const scrubbed: UpupServerErrorEvent = {\r\n ...event,\r\n message: scrubSensitive(event.message),\r\n }\r\n if (event.error) {\r\n scrubbed.error = {\r\n name: event.error.name,\r\n message: scrubSensitive(event.error.message),\r\n stack: event.error.stack\r\n ? scrubSensitive(event.error.stack)\r\n : undefined,\r\n }\r\n }\r\n ;(logger ?? defaultLogger)(scrubbed)\r\n}\r\n","import {\r\n PutObjectCommand,\r\n CreateMultipartUploadCommand,\r\n UploadPartCommand,\r\n CompleteMultipartUploadCommand,\r\n AbortMultipartUploadCommand,\r\n} from '@aws-sdk/client-s3'\r\nimport { UpupStorageError, UpupErrorCode } from '@useupup/core'\r\nimport type { UpupStorageConfig, UploadedFile } from './config'\r\nimport { createS3Client } from './providers/s3-client'\r\nimport {\r\n MIN_PART_SIZE,\r\n generateSignedPublicUrl,\r\n DEFAULT_DOWNLOAD_URL_EXPIRES_IN,\r\n} from './providers/aws'\r\nimport {\r\n reportServerError,\r\n toSafeError,\r\n type UpupServerLogger,\r\n} from './observability'\r\n\r\n// Hard cap on buffered memory per transfer: files at or under this size are\r\n// singlePut (whole body buffered once); everything else streams through\r\n// bounded MIN_PART_SIZE multipart parts, regardless of file size (F-501).\r\n// This bound is intentionally NOT configurable — memory safety must not be a\r\n// raisable knob (the removed `multipartThreshold` let an integrator reintroduce\r\n// unbounded buffering by raising it).\r\nconst SINGLE_PUT_MAX_BYTES = MIN_PART_SIZE\r\n\r\nexport async function transferDriveFileToS3(opts: {\r\n stream: ReadableStream<Uint8Array>\r\n size: number\r\n fileName: string\r\n mimeType: string\r\n storage: UpupStorageConfig\r\n /** Authoritative cap enforced against the ACTUAL streamed bytes (not the\r\n * client/drive-declared size). Exceeding it aborts + rejects (F-743). */\r\n maxBytes?: number | undefined\r\n /** Observability sink so a failed compensating abort is reported, not\r\n * swallowed (F-744). */\r\n onError?: UpupServerLogger | undefined\r\n requestId?: string | undefined\r\n /** TTL for the signed GET returned as `url`; defaults to 3 days (#343). */\r\n downloadUrlExpiresIn?: number | undefined\r\n}): Promise<UploadedFile> {\r\n const key = `${crypto.randomUUID()}-${opts.fileName}`\r\n\r\n if (opts.size > 0 && opts.size <= SINGLE_PUT_MAX_BYTES) {\r\n return singlePut({ ...opts, key })\r\n }\r\n return streamingMultipart({ ...opts, key })\r\n}\r\n\r\nasync function singlePut(opts: {\r\n stream: ReadableStream<Uint8Array>\r\n size: number\r\n fileName: string\r\n mimeType: string\r\n storage: UpupStorageConfig\r\n key: string\r\n maxBytes?: number | undefined\r\n downloadUrlExpiresIn?: number | undefined\r\n}): Promise<UploadedFile> {\r\n const buffer = await streamToUint8Array(opts.stream)\r\n // Enforce maxFileSize against the bytes we actually received before writing\r\n // anything to S3 (F-743): the routing decision used a declared size the\r\n // server never reconciled with the real payload.\r\n if (opts.maxBytes !== undefined && buffer.byteLength > opts.maxBytes) {\r\n throw new UpupStorageError(\r\n `Drive file exceeds the configured maxFileSize (${buffer.byteLength} > ${opts.maxBytes} bytes)`,\r\n opts.storage.type,\r\n 'upload',\r\n )\r\n }\r\n const client = createS3Client(opts.storage)\r\n await client.send(\r\n new PutObjectCommand({\r\n Bucket: opts.storage.bucket,\r\n Key: opts.key,\r\n ContentType: opts.mimeType,\r\n Body: buffer,\r\n }),\r\n )\r\n const url = await generateSignedPublicUrl(\r\n opts.storage,\r\n opts.key,\r\n opts.downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN,\r\n )\r\n return {\r\n key: opts.key,\r\n name: opts.fileName,\r\n size: buffer.byteLength,\r\n type: opts.mimeType,\r\n url,\r\n }\r\n}\r\n\r\nasync function streamingMultipart(opts: {\r\n stream: ReadableStream<Uint8Array>\r\n size: number\r\n fileName: string\r\n mimeType: string\r\n storage: UpupStorageConfig\r\n key: string\r\n maxBytes?: number | undefined\r\n onError?: UpupServerLogger | undefined\r\n requestId?: string | undefined\r\n downloadUrlExpiresIn?: number | undefined\r\n}): Promise<UploadedFile> {\r\n const client = createS3Client(opts.storage)\r\n\r\n const init = await client.send(\r\n new CreateMultipartUploadCommand({\r\n Bucket: opts.storage.bucket,\r\n Key: opts.key,\r\n ContentType: opts.mimeType,\r\n }),\r\n )\r\n const uploadId = init.UploadId\r\n if (!uploadId)\r\n throw new UpupStorageError(\r\n 'Missing UploadId on multipart init',\r\n opts.storage.type,\r\n 'multipart-init',\r\n )\r\n\r\n const parts: Array<{ PartNumber: number; ETag: string }> = []\r\n let totalBytes = 0\r\n\r\n try {\r\n let partNumber = 1\r\n for await (const chunk of chunkedStream(opts.stream, MIN_PART_SIZE)) {\r\n const res = await client.send(\r\n new UploadPartCommand({\r\n Bucket: opts.storage.bucket,\r\n Key: opts.key,\r\n UploadId: uploadId,\r\n PartNumber: partNumber,\r\n ContentLength: chunk.byteLength,\r\n Body: chunk,\r\n }),\r\n )\r\n if (!res.ETag) {\r\n throw new UpupStorageError(\r\n `Missing ETag for part ${partNumber}`,\r\n opts.storage.type,\r\n 'multipart-sign-part',\r\n )\r\n }\r\n parts.push({ PartNumber: partNumber, ETag: res.ETag })\r\n totalBytes += chunk.byteLength\r\n // Enforce the cap against real egress; exceeding it throws into the\r\n // catch below, which aborts the whole multipart upload so no partial\r\n // object persists (F-743).\r\n if (opts.maxBytes !== undefined && totalBytes > opts.maxBytes) {\r\n throw new UpupStorageError(\r\n `Drive file exceeds the configured maxFileSize (${totalBytes} > ${opts.maxBytes} bytes)`,\r\n opts.storage.type,\r\n 'upload',\r\n )\r\n }\r\n partNumber++\r\n }\r\n\r\n if (parts.length === 0) {\r\n throw new UpupStorageError(\r\n 'Drive download produced no bytes',\r\n opts.storage.type,\r\n 'upload',\r\n )\r\n }\r\n\r\n await client.send(\r\n new CompleteMultipartUploadCommand({\r\n Bucket: opts.storage.bucket,\r\n Key: opts.key,\r\n UploadId: uploadId,\r\n MultipartUpload: { Parts: parts },\r\n }),\r\n )\r\n } catch (err) {\r\n try {\r\n await client.send(\r\n new AbortMultipartUploadCommand({\r\n Bucket: opts.storage.bucket,\r\n Key: opts.key,\r\n UploadId: uploadId,\r\n }),\r\n )\r\n } catch (abortErr) {\r\n // upup-catch: the transfer already failed AND the compensating abort\r\n // failed too, leaving an incomplete multipart upload lingering\r\n // (billable until a lifecycle rule reaps it). REPORT it through the\r\n // observability seam (F-744) — the old code swallowed it with a bare\r\n // `.catch(() => {})` — then re-throw the ORIGINAL error below so the\r\n // route still surfaces the real cause.\r\n reportServerError(opts.onError, {\r\n route: 'files/transfer',\r\n method: 'POST',\r\n status: 500,\r\n code: UpupErrorCode.STORAGE_ERROR,\r\n message: `Failed to abort multipart upload after a transfer error (uploadId=${uploadId}, key=${opts.key})`,\r\n requestId: opts.requestId,\r\n error: toSafeError(abortErr),\r\n })\r\n }\r\n throw err\r\n }\r\n\r\n const url = await generateSignedPublicUrl(\r\n opts.storage,\r\n opts.key,\r\n opts.downloadUrlExpiresIn ?? DEFAULT_DOWNLOAD_URL_EXPIRES_IN,\r\n )\r\n return {\r\n key: opts.key,\r\n name: opts.fileName,\r\n size: totalBytes,\r\n type: opts.mimeType,\r\n url,\r\n }\r\n}\r\n\r\nasync function streamToUint8Array(\r\n stream: ReadableStream<Uint8Array>,\r\n): Promise<Uint8Array> {\r\n const reader = stream.getReader()\r\n const chunks: Uint8Array[] = []\r\n let total = 0\r\n for (;;) {\r\n const { value, done } = await reader.read()\r\n if (done) break\r\n chunks.push(value)\r\n total += value.byteLength\r\n }\r\n const out = new Uint8Array(total)\r\n let offset = 0\r\n for (const c of chunks) {\r\n out.set(c, offset)\r\n offset += c.byteLength\r\n }\r\n return out\r\n}\r\n\r\nasync function* chunkedStream(\r\n stream: ReadableStream<Uint8Array>,\r\n chunkSize: number,\r\n): AsyncGenerator<Uint8Array> {\r\n const reader = stream.getReader()\r\n let buffered: Uint8Array[] = []\r\n let bufferedSize = 0\r\n\r\n const flush = (): Uint8Array => {\r\n const out = new Uint8Array(bufferedSize)\r\n let offset = 0\r\n for (const c of buffered) {\r\n out.set(c, offset)\r\n offset += c.byteLength\r\n }\r\n buffered = []\r\n bufferedSize = 0\r\n return out\r\n }\r\n\r\n for (;;) {\r\n const { value, done } = await reader.read()\r\n if (done) break\r\n buffered.push(value)\r\n bufferedSize += value.byteLength\r\n while (bufferedSize >= chunkSize) {\r\n const out = new Uint8Array(chunkSize)\r\n let remaining = chunkSize\r\n let offset = 0\r\n while (remaining > 0 && buffered.length > 0) {\r\n const head = buffered[0]\r\n if (head === undefined) break\r\n const take = Math.min(head.byteLength, remaining)\r\n out.set(head.subarray(0, take), offset)\r\n offset += take\r\n remaining -= take\r\n if (take < head.byteLength) {\r\n buffered[0] = head.subarray(take)\r\n } else {\r\n buffered.shift()\r\n }\r\n }\r\n bufferedSize -= chunkSize\r\n yield out\r\n }\r\n }\r\n\r\n if (bufferedSize > 0) yield flush()\r\n}\r\n","import { createUpupHandler } from './handler'\r\nimport type { UpupServerConfig } from './config'\r\nimport { toWebRequest, writeWebResponse } from './node-http-bridge'\r\n\r\ninterface ExpressReq {\r\n protocol: string\r\n get(name: string): string | undefined\r\n originalUrl: string\r\n method: string\r\n headers: Record<string, string | string[] | undefined>\r\n body: unknown\r\n}\r\n\r\ninterface ExpressRes {\r\n status(code: number): ExpressRes\r\n setHeader(name: string, value: string): ExpressRes\r\n send(body: string | Buffer): void\r\n}\r\n\r\nexport function createUpupMiddleware(config: UpupServerConfig) {\r\n const handler = createUpupHandler(config)\r\n\r\n return async (\r\n req: ExpressReq,\r\n res: ExpressRes,\r\n _next: () => void,\r\n ): Promise<void> => {\r\n const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`\r\n const webReq = toWebRequest({\r\n url,\r\n method: req.method,\r\n headers: req.headers,\r\n body:\r\n req.method !== 'GET' && req.method !== 'HEAD'\r\n ? JSON.stringify(req.body)\r\n : undefined,\r\n })\r\n\r\n const webRes = await handler(webReq)\r\n\r\n await writeWebResponse(\r\n {\r\n status: c => res.status(c),\r\n setHeader: (k, v) => res.setHeader(k, v),\r\n send: b => {\r\n res.send(b)\r\n },\r\n },\r\n webRes,\r\n )\r\n }\r\n}\r\n\r\nexport type { UpupServerConfig }\r\n","import { UpupErrorCode, UpupConfigError } from '@useupup/core'\r\nimport type { UpupServerConfig } from './config'\r\nimport { assertUploadTokenSecret } from './uploadToken'\r\nimport { validateServerConfig } from './validate-config'\r\nimport { assertS3Storage } from './storage'\r\nimport { isStorageResolver } from './resolve-storage'\r\nimport { handleHealth } from './health'\r\nimport { createResponder } from './respond'\r\nimport {\r\n handlePresign,\r\n handleMultipartInit,\r\n handleMultipartSignPart,\r\n handleMultipartComplete,\r\n handleMultipartAbort,\r\n handleMultipartResume,\r\n DEFAULT_MULTIPART_RESUME_WINDOW_SECONDS,\r\n} from './upload-routes'\r\nimport { handleOAuthRedirect, handleOAuthCallback } from './oauth'\r\nimport { handleListFiles, handleFileTransfer } from './drive-routes'\r\n\r\nexport type RouteHandler = (req: Request) => Promise<Response>\r\n\r\nexport function createUpupHandler(config: UpupServerConfig): RouteHandler {\r\n // Secure-by-default: a stable token secret is mandatory (the multipart routes\r\n // are always live and issue/verify tokens), and drive/tokenStore use requires\r\n // a real identity resolver unless anonymous is explicitly opted into.\r\n assertUploadTokenSecret(config.uploadTokenSecret)\r\n\r\n // Fail-fast on missing/empty required fields (bucket/region, half-set creds,\r\n // provider creds) so a forgotten env var throws HERE, not as a confusing 500\r\n // at request time — and regardless of whether the app went through the opt-in\r\n // defineUpupConfig wrapper (F-852). Runs before the storage.type check below\r\n // so an entirely-missing `storage` is reported as such, not a TypeError.\r\n validateServerConfig(config)\r\n\r\n // Fail-fast (F-657): storage.type accepted all 21 StorageProvider values, but\r\n // the S3 upload path (buildS3ClientConfig) is honored by none of them — it\r\n // always builds an @aws-sdk/client-s3 client via endpoint/forcePathStyle/\r\n // credentials/region. A provider with no S3-compatible surface (currently\r\n // just Azure) could never function, with zero compile- or startup-time\r\n // signal until now.\r\n // A resolver has no type to check yet — the same guard runs on whatever it\r\n // returns, per request, in resolve-storage.ts (#337).\r\n if (!isStorageResolver(config.storage)) assertS3Storage(config.storage)\r\n if (\r\n (config.providers || config.tokenStore) &&\r\n !config.getUserId &&\r\n !config.allowAnonymous\r\n ) {\r\n throw new UpupConfigError(\r\n '[@useupup/server] drive providers / tokenStore require config.getUserId to ' +\r\n 'scope tokens per user. Set getUserId, or set allowAnonymous:true to ' +\r\n 'intentionally share ONE anonymous namespace (demos only).',\r\n )\r\n }\r\n\r\n // Loud construct-time signal: anonymous uploads accept unauthenticated\r\n // callers under a shared namespace. One line so a demo flag left on in\r\n // production is impossible to miss in the boot logs.\r\n if (config.allowAnonymousUploads) {\r\n console.warn(\r\n '[@useupup/server] allowAnonymousUploads:true — /presign and /multipart/init ' +\r\n 'accept UNAUTHENTICATED uploads under a shared anonymous namespace. ' +\r\n 'Demos / upstream-auth deployments only; never enable in multi-tenant production.',\r\n )\r\n }\r\n\r\n return async (req: Request): Promise<Response> => {\r\n const url = new URL(req.url)\r\n // Frameworks with trailing-slash normalization (Next.js\r\n // trailingSlash:true) 308 a POST /presign to /presign/ with method and\r\n // body preserved — match routes on the slash-stripped path so the\r\n // redirected request doesn't 404.\r\n const path = url.pathname.replace(/\\/+$/, '') || '/'\r\n const res = createResponder(req, config)\r\n\r\n try {\r\n if (req.method === 'OPTIONS') {\r\n return res.noContent()\r\n }\r\n\r\n // Health check sits BEFORE the auth gate so uptime/deploy probes work\r\n // unauthenticated (F-426/F-428).\r\n if (req.method === 'GET' && path.endsWith('/health')) {\r\n return await handleHealth(config, res)\r\n }\r\n\r\n // Auth check\r\n if (config.auth) {\r\n const authorized = await config.auth(req)\r\n if (!authorized) {\r\n return res.json({ error: 'Unauthorized' }, 401)\r\n }\r\n }\r\n\r\n // Route matching\r\n if (req.method === 'POST' && path.endsWith('/presign')) {\r\n return await handlePresign(req, config, res)\r\n }\r\n if (req.method === 'POST' && path.endsWith('/multipart/init')) {\r\n return await handleMultipartInit(req, config, res)\r\n }\r\n if (\r\n req.method === 'POST' &&\r\n path.endsWith('/multipart/sign-part')\r\n ) {\r\n return await handleMultipartSignPart(req, config, res)\r\n }\r\n if (req.method === 'POST' && path.endsWith('/multipart/complete')) {\r\n return await handleMultipartComplete(req, config, res)\r\n }\r\n if (req.method === 'POST' && path.endsWith('/multipart/abort')) {\r\n return await handleMultipartAbort(req, config, res)\r\n }\r\n // multipartResumeWindowSeconds:0 turns cross-reload resume OFF by\r\n // not routing at all — the path falls through to the 404 below, so a\r\n // disabled server is indistinguishable from one too old to have the\r\n // route, and clients already fall back to a fresh init on 4xx.\r\n if (\r\n req.method === 'POST' &&\r\n path.endsWith('/multipart/resume') &&\r\n (config.multipartResumeWindowSeconds ??\r\n DEFAULT_MULTIPART_RESUME_WINDOW_SECONDS) > 0\r\n ) {\r\n return await handleMultipartResume(req, config, res)\r\n }\r\n\r\n // OAuth routes: GET /auth/:provider and GET /auth/:provider/cb\r\n const authMatch = path.match(/\\/auth\\/([\\w-]+?)(?:\\/(cb))?$/)\r\n if (req.method === 'GET' && authMatch) {\r\n const provider = authMatch[1]\r\n if (provider === undefined) {\r\n return res.json({ error: 'Not found' }, 404)\r\n }\r\n const isCallback = authMatch[2] === 'cb'\r\n if (isCallback) {\r\n return await handleOAuthCallback(req, config, provider, res)\r\n }\r\n return await handleOAuthRedirect(req, config, provider, res)\r\n }\r\n\r\n // File routes: GET /files/:provider and POST /files/:provider/transfer\r\n const filesMatch = path.match(\r\n /\\/files\\/([\\w-]+?)(?:\\/(transfer))?$/,\r\n )\r\n if (filesMatch) {\r\n const provider = filesMatch[1]\r\n if (provider === undefined) {\r\n return res.json({ error: 'Not found' }, 404)\r\n }\r\n const isTransfer = filesMatch[2] === 'transfer'\r\n if (req.method === 'POST' && isTransfer) {\r\n return await handleFileTransfer(req, config, provider, res)\r\n }\r\n if (req.method === 'GET' && !isTransfer) {\r\n return await handleListFiles(req, config, provider, res)\r\n }\r\n }\r\n\r\n return res.json({ error: 'Not found' }, 404)\r\n } catch (error) {\r\n // Transport safety net (F-421/F-104): a last-resort catch so an unhandled\r\n // throw anywhere in routing surfaces as a logged, CORS-headered, coded 500\r\n // instead of an uncaught framework exception with no log and no CORS. This\r\n // is the single home for last-resort handling — the express/fastify/hono/\r\n // next adapters need no per-adapter catch of their own.\r\n return res.fail(\r\n 'router',\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Internal error',\r\n error,\r\n )\r\n }\r\n }\r\n}\r\n","// packages/server/src/uploadToken.ts\r\n//\r\n// Stateless, HMAC-signed upload token. Issued at multipart-init (binding the\r\n// server-chosen key + S3 uploadId + size envelope + expiry) and re-derived from\r\n// the VERIFIED token on sign-part / complete / abort, so the client can never\r\n// assert the key/uploadId on the way back (closes S2). Web Crypto only, so it\r\n// runs on Node 18+, edge, and Cloudflare Workers — matching the rest of the\r\n// package (already uses global crypto.randomUUID / getRandomValues).\r\n\r\nimport { UpupConfigError } from '@useupup/core'\r\n\r\nexport interface UploadTokenPayload {\r\n /** Object key the upload is bound to. */\r\n k: string\r\n /** S3 multipart uploadId. */\r\n u: string\r\n /** Resolved userId, or null for anonymous (server-namespaced) uploads. */\r\n uid: string | null\r\n /** Minimum allowed total size, bytes. */\r\n smin: number\r\n /** Maximum allowed total size, bytes. */\r\n smax: number\r\n /**\r\n * Opaque identity of the storage this upload's `init` resolved (#337).\r\n * Present only when `config.storage` is a resolver; a static config binds\r\n * nothing because there is only ever one destination. Signed, so the client\r\n * cannot steer a continuation into another bucket.\r\n */\r\n sid?: string\r\n /** Expiry, epoch SECONDS. */\r\n exp: number\r\n /**\r\n * Issue time of the ORIGINAL multipart init, epoch SECONDS. Optional: tokens\r\n * minted before cross-reload resume shipped carry no `iat` and must keep\r\n * verifying, so the resume route falls back to `exp - TTL`. Carried forward\r\n * UNCHANGED by /multipart/resume — the resume window is anchored at the\r\n * original init, so rolling resumes can never extend it.\r\n */\r\n iat?: number\r\n}\r\n\r\nexport type UploadTokenErrorCode = 'malformed' | 'bad_signature' | 'expired'\r\n\r\nexport class UploadTokenError extends Error {\r\n code: UploadTokenErrorCode\r\n constructor(code: UploadTokenErrorCode, message: string) {\r\n super(message)\r\n this.name = 'UploadTokenError'\r\n this.code = code\r\n }\r\n}\r\n\r\nexport const DEFAULT_UPLOAD_TOKEN_TTL_SECONDS = 3600\r\nconst MIN_SECRET_LENGTH = 16\r\n\r\n/** Secure-by-default guard used at handler construction. */\r\nexport function assertUploadTokenSecret(\r\n secret: string | undefined,\r\n): asserts secret is string {\r\n if (!secret || secret.length < MIN_SECRET_LENGTH) {\r\n throw new UpupConfigError(\r\n `[@useupup/server] config.uploadTokenSecret is required and must be at least ${MIN_SECRET_LENGTH} characters. ` +\r\n 'Generate a stable, high-entropy secret (e.g. `openssl rand -hex 32`) and share the SAME value across every server instance/worker.',\r\n )\r\n }\r\n}\r\n\r\nconst encoder = new TextEncoder()\r\n\r\nfunction bytesToBase64Url(bytes: Uint8Array): string {\r\n let bin = ''\r\n for (const byte of bytes) bin += String.fromCharCode(byte)\r\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\r\n}\r\n\r\nfunction base64UrlToBytes(b64url: string): Uint8Array {\r\n const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/')\r\n const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4))\r\n const bin = atob(b64 + pad)\r\n const out = new Uint8Array(bin.length)\r\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\r\n return out\r\n}\r\n\r\nasync function hmacSha256(secret: string, data: string): Promise<Uint8Array> {\r\n const key = await crypto.subtle.importKey(\r\n 'raw',\r\n encoder.encode(secret),\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n false,\r\n ['sign'],\r\n )\r\n const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(data))\r\n return new Uint8Array(sig)\r\n}\r\n\r\n/** Constant-time compare — never branch on content before lengths match. */\r\nfunction timingSafeEqual(a: string, b: string): boolean {\r\n if (a.length !== b.length) return false\r\n let diff = 0\r\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\r\n return diff === 0\r\n}\r\n\r\nexport async function signUploadToken(\r\n secret: string,\r\n payload: UploadTokenPayload,\r\n): Promise<string> {\r\n const body = bytesToBase64Url(encoder.encode(JSON.stringify(payload)))\r\n const sig = bytesToBase64Url(await hmacSha256(secret, body))\r\n return `${body}.${sig}`\r\n}\r\n\r\nexport interface VerifyUploadTokenOptions {\r\n /**\r\n * Accept a token whose `exp` has already passed. ONLY /multipart/resume sets\r\n * this: its whole job is to hand an expired token back as a fresh one, and it\r\n * applies its OWN, tighter bound afterwards (the resume window anchored at\r\n * `iat`). Every other route leaves it unset and keeps rejecting expired\r\n * tokens exactly as before — signature/shape checks are identical either way,\r\n * so this relaxes expiry and nothing else.\r\n */\r\n allowExpired?: boolean\r\n}\r\n\r\nexport async function verifyUploadToken(\r\n secret: string,\r\n token: string,\r\n nowMs: number,\r\n options: VerifyUploadTokenOptions = {},\r\n): Promise<UploadTokenPayload> {\r\n if (typeof token !== 'string' || !token.includes('.')) {\r\n throw new UploadTokenError(\r\n 'malformed',\r\n 'Upload token is missing or malformed',\r\n )\r\n }\r\n const [body, sig] = token.split('.')\r\n if (!body || !sig) {\r\n throw new UploadTokenError('malformed', 'Upload token is malformed')\r\n }\r\n // Verify the signature BEFORE trusting any payload bytes.\r\n const expected = bytesToBase64Url(await hmacSha256(secret, body))\r\n if (!timingSafeEqual(sig, expected)) {\r\n throw new UploadTokenError(\r\n 'bad_signature',\r\n 'Upload token signature is invalid',\r\n )\r\n }\r\n let payload: UploadTokenPayload\r\n try {\r\n payload = JSON.parse(\r\n new TextDecoder().decode(base64UrlToBytes(body)),\r\n ) as UploadTokenPayload\r\n } catch {\r\n throw new UploadTokenError(\r\n 'malformed',\r\n 'Upload token payload is not valid JSON',\r\n )\r\n }\r\n if (\r\n typeof payload.k !== 'string' ||\r\n typeof payload.u !== 'string' ||\r\n typeof payload.exp !== 'number' ||\r\n typeof payload.smin !== 'number' ||\r\n typeof payload.smax !== 'number' ||\r\n (payload.sid !== undefined && typeof payload.sid !== 'string')\r\n ) {\r\n throw new UploadTokenError(\r\n 'malformed',\r\n 'Upload token payload is missing required fields',\r\n )\r\n }\r\n if (!options.allowExpired && payload.exp <= Math.floor(nowMs / 1000)) {\r\n throw new UploadTokenError('expired', 'Upload token has expired')\r\n }\r\n return payload\r\n}\r\n","// packages/server/src/validate-config.ts\r\n//\r\n// Construct-time required-field validation, folded out of @useupup/next's opt-in\r\n// defineUpupConfig so it runs for EVERY caller of createUpupHandler regardless\r\n// of wrapper (F-852). A server-package factory could never import a next-\r\n// package helper, so playground/landing — which call createUpupNextHandler\r\n// (→ createUpupHandler) directly — previously bypassed the fail-fast entirely.\r\n// Throws ONE UpupConfigError listing every missing/empty required field, so a\r\n// forgotten env var (the classic `process.env.X!` -> \"\") fails fast and loud\r\n// instead of surfacing as a confusing 500 at request time.\r\n\r\nimport { UpupConfigError } from '@useupup/core'\r\nimport type { UpupServerConfig } from './config'\r\n\r\nfunction isNonEmpty(v: unknown): v is string {\r\n return typeof v === 'string' && v.trim().length > 0\r\n}\r\n\r\nexport function validateServerConfig(config: UpupServerConfig): void {\r\n const missing: string[] = []\r\n const invalid: string[] = []\r\n\r\n // Runtime guard: callers may pass partial/invalid objects at boot time.\r\n if (!(config as Partial<UpupServerConfig>).storage) {\r\n missing.push('storage')\r\n } else if (typeof config.storage === 'function') {\r\n // A storage RESOLVER has no fields yet — its result is validated per\r\n // request in resolve-storage.ts, which is the first moment it exists.\r\n } else {\r\n if (!isNonEmpty(config.storage.bucket)) missing.push('storage.bucket')\r\n if (!isNonEmpty(config.storage.region)) missing.push('storage.region')\r\n // Both-or-neither: undefined creds are OK (IAM role), but a half-set\r\n // pair (the classic `process.env.X!` -> \"\" bug) must fail loudly.\r\n const hasId = config.storage.accessKeyId !== undefined\r\n const hasSecret = config.storage.secretAccessKey !== undefined\r\n if (hasId || hasSecret) {\r\n if (!isNonEmpty(config.storage.accessKeyId))\r\n missing.push('storage.accessKeyId')\r\n if (!isNonEmpty(config.storage.secretAccessKey))\r\n missing.push('storage.secretAccessKey')\r\n }\r\n }\r\n\r\n const p = config.providers\r\n if (p?.googleDrive) {\r\n if (!isNonEmpty(p.googleDrive.clientId))\r\n missing.push('providers.googleDrive.clientId')\r\n if (!isNonEmpty(p.googleDrive.clientSecret))\r\n missing.push('providers.googleDrive.clientSecret')\r\n }\r\n if (p?.dropbox) {\r\n if (!isNonEmpty(p.dropbox.appKey))\r\n missing.push('providers.dropbox.appKey')\r\n if (!isNonEmpty(p.dropbox.appSecret))\r\n missing.push('providers.dropbox.appSecret')\r\n }\r\n if (p?.oneDrive) {\r\n if (!isNonEmpty(p.oneDrive.clientId))\r\n missing.push('providers.oneDrive.clientId')\r\n if (!isNonEmpty(p.oneDrive.clientSecret))\r\n missing.push('providers.oneDrive.clientSecret')\r\n }\r\n if (p?.box) {\r\n if (!isNonEmpty(p.box.clientId)) missing.push('providers.box.clientId')\r\n if (!isNonEmpty(p.box.clientSecret))\r\n missing.push('providers.box.clientSecret')\r\n }\r\n\r\n // A window that is negative, fractional, or NaN is a typo, not a policy —\r\n // and silently coercing it would either disable resume or extend it forever.\r\n // `0` is the sanctioned \"route off\" value, so only sub-zero is rejected.\r\n const resumeWindow = config.multipartResumeWindowSeconds\r\n if (\r\n resumeWindow !== undefined &&\r\n (!Number.isInteger(resumeWindow) || resumeWindow < 0)\r\n ) {\r\n invalid.push(\r\n `multipartResumeWindowSeconds must be a non-negative integer (got ${String(resumeWindow)}); use 0 to disable /multipart/resume`,\r\n )\r\n }\r\n\r\n if (missing.length > 0 || invalid.length > 0) {\r\n const sections = [\r\n missing.length > 0\r\n ? 'missing/empty required field(s):\\n' +\r\n missing.map(m => ` - ${m}`).join('\\n')\r\n : '',\r\n invalid.length > 0\r\n ? 'invalid field(s):\\n' +\r\n invalid.map(m => ` - ${m}`).join('\\n')\r\n : '',\r\n ].filter(Boolean)\r\n throw new UpupConfigError(\r\n `[@useupup/server] Invalid config — ${sections.join('\\n')}`,\r\n )\r\n }\r\n}\r\n","// packages/server/src/storage.ts\r\n//\r\n// The one home for \"is this storage config something @useupup/server can serve?\".\r\n// createUpupHandler asserts it at construct time and getDownloadUrl asserts it\r\n// per call, so the check and its wording live here rather than being duplicated\r\n// (and drifting) at each entry point.\r\n\r\nimport { UpupConfigError, NON_S3_STORAGE_PROVIDERS } from '@useupup/core'\r\nimport type { UpupStorageConfig } from './config'\r\n\r\n/**\r\n * Reject a provider with no S3-compatible API (F-657). The S3 upload path\r\n * (buildS3ClientConfig) always builds an @aws-sdk/client-s3 client, so such a\r\n * provider could never function — fail loudly instead of 500ing at request time.\r\n */\r\nexport function assertS3Storage(storage: UpupStorageConfig): void {\r\n const storageType = storage.type\r\n if (\r\n typeof storageType === 'string' &&\r\n (NON_S3_STORAGE_PROVIDERS as ReadonlySet<string>).has(storageType)\r\n ) {\r\n throw new UpupConfigError(\r\n `[@useupup/server] storage.type \"${storageType}\" has no S3-compatible API and cannot be served. ` +\r\n 'upup uploads via the S3 API — use an S3-compatible provider ' +\r\n '(aws, minio, r2, wasabi, …) and set storage.endpoint for non-AWS backends.',\r\n )\r\n }\r\n}\r\n","// packages/server/src/resolve-storage.ts\r\n//\r\n// Per-request storage resolution (#337). `config.storage` is either one static\r\n// object — in which case every function here is a passthrough and behavior is\r\n// byte-identical to before — or a resolver invoked per request.\r\n//\r\n// The hard part is the multipart lifecycle. `init` resolves a bucket, but\r\n// `sign-part` / `complete` / `abort` arrive later carrying only a token, with\r\n// none of the metadata the routing decision was made from. Re-running the\r\n// resolver blind would send them somewhere else. Accepting a client-supplied\r\n// hint would let anyone redirect a continuation into a bucket of their\r\n// choosing.\r\n//\r\n// So `init` stamps a STORAGE IDENTITY into the HMAC-signed upload token, and\r\n// each continuation hands it back to the resolver as `ctx.storageId`. The\r\n// resolver returns the matching storage; the server then re-derives the\r\n// identity of what came back and rejects a mismatch. The client never supplies\r\n// the identity unsigned, and a resolver that ignores `storageId` fails closed\r\n// rather than writing to the wrong bucket.\r\n//\r\n// The identity is a hash of the DESTINATION (bucket + endpoint + region), never\r\n// of credentials: rotating an access key must not strand in-flight uploads, and\r\n// nothing secret may sit in a token the client can read.\r\n\r\nimport { UpupConfigError } from '@useupup/core'\r\nimport type {\r\n UpupServerConfig,\r\n UpupStorageConfig,\r\n UpupStorageResolver,\r\n StorageResolverContext,\r\n} from './config'\r\nimport { assertS3Storage } from './storage'\r\n\r\nexport function isStorageResolver(\r\n storage: UpupServerConfig['storage'],\r\n): storage is UpupStorageResolver {\r\n return typeof storage === 'function'\r\n}\r\n\r\n/**\r\n * A stable, opaque, non-secret id for a storage DESTINATION. Deterministic\r\n * across instances and restarts (a plain SHA-256), so a token issued by one\r\n * worker verifies on any other.\r\n */\r\nexport async function storageIdentity(\r\n storage: UpupStorageConfig,\r\n): Promise<string> {\r\n const material = [\r\n storage.bucket,\r\n storage.endpoint ?? '',\r\n storage.region,\r\n ].join('\\n')\r\n const digest = await crypto.subtle.digest(\r\n 'SHA-256',\r\n new TextEncoder().encode(material),\r\n )\r\n return Array.from(new Uint8Array(digest))\r\n .map(b => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n .slice(0, 32)\r\n}\r\n\r\n/** The same required-field rules validateServerConfig applies at construct\r\n * time, applied to what a resolver just returned. */\r\nfunction assertResolvedStorage(\r\n storage: unknown,\r\n): asserts storage is UpupStorageConfig {\r\n const s = storage as Partial<UpupStorageConfig> | null | undefined\r\n const missing: string[] = []\r\n if (!s || typeof s !== 'object') {\r\n throw new UpupConfigError(\r\n '[@useupup/server] the storage resolver did not return a storage config object.',\r\n )\r\n }\r\n if (typeof s.bucket !== 'string' || s.bucket.trim() === '')\r\n missing.push('bucket')\r\n if (typeof s.region !== 'string' || s.region.trim() === '')\r\n missing.push('region')\r\n if (missing.length > 0) {\r\n throw new UpupConfigError(\r\n '[@useupup/server] the storage resolver returned a config missing required field(s): ' +\r\n missing.map(m => `storage.${m}`).join(', '),\r\n )\r\n }\r\n assertS3Storage(s as UpupStorageConfig)\r\n}\r\n\r\n/**\r\n * The storage for one request. Static configs are returned as-is (already\r\n * validated at construct time); a resolver's result is validated here, because\r\n * this is the first moment it exists.\r\n *\r\n * Throws UpupConfigError on a bad result — callers turn that into a 500 through\r\n * the Responder rather than letting a misrouted upload proceed.\r\n */\r\nexport async function resolveStorage(\r\n config: UpupServerConfig,\r\n ctx: StorageResolverContext,\r\n): Promise<UpupStorageConfig> {\r\n if (!isStorageResolver(config.storage)) return config.storage\r\n const resolved = await config.storage(ctx)\r\n assertResolvedStorage(resolved)\r\n return resolved\r\n}\r\n\r\n/**\r\n * Resolve for a multipart CONTINUATION and prove it landed where `init` did.\r\n *\r\n * A token with no `sid` predates the resolver (or was issued while `storage`\r\n * was still static) and is rejected rather than routed by guesswork — upload\r\n * tokens live an hour, so the client simply restarts from `init`.\r\n */\r\nexport async function resolveBoundStorage(\r\n config: UpupServerConfig,\r\n ctx: StorageResolverContext,\r\n boundId: string | undefined,\r\n): Promise<UpupStorageConfig> {\r\n if (!isStorageResolver(config.storage)) return config.storage\r\n if (!boundId) {\r\n throw new StorageBindingError(\r\n 'Upload token carries no storage binding; restart the upload from /multipart/init',\r\n )\r\n }\r\n const resolved = await resolveStorage(config, {\r\n ...ctx,\r\n storageId: boundId,\r\n })\r\n if ((await storageIdentity(resolved)) !== boundId) {\r\n throw new StorageBindingError(\r\n 'Upload token is bound to different storage than the resolver returned',\r\n )\r\n }\r\n return resolved\r\n}\r\n\r\n/** A continuation that cannot be proven to reach the storage its init chose.\r\n * Distinct from UpupConfigError because it is a 403, not a 500 — the request\r\n * is not authorized for that storage, the server is not misconfigured. */\r\nexport class StorageBindingError extends Error {\r\n constructor(message: string) {\r\n super(message)\r\n this.name = 'StorageBindingError'\r\n }\r\n}\r\n","// packages/server/src/health.ts\r\n//\r\n// GET /health — answers \"is config complete?\" and \"can we reach storage?\" in\r\n// one unauthenticated request, without a real upload. Booleans + a config-\r\n// completeness summary only by default; no secret VALUES are ever returned.\r\n// The cross-instance secret fingerprint (first 8 hex of SHA-256(secret)) is\r\n// opt-in via config.health.exposeSecretFingerprint — enough to spot fleet\r\n// drift (a rolling redeploy that didn't share the same uploadTokenSecret)\r\n// without being reversible to the secret itself.\r\n\r\nimport type { UpupServerConfig } from './config'\r\nimport type { Responder } from './respond'\r\nimport { checkStorageReachable } from './providers/aws'\r\nimport { reportServerError, toSafeError } from './observability'\r\nimport { DEFAULT_UPLOAD_TOKEN_TTL_SECONDS } from './uploadToken'\r\nimport { isStorageResolver } from './resolve-storage'\r\n\r\ntype StorageCheckCache = { ok: true } | { ok: false } | undefined\r\nlet cachedStorageCheck: StorageCheckCache\r\nlet cachedAt = 0\r\nconst STORAGE_CHECK_TTL_MS = 30_000\r\n\r\n/** Test-only: force the next handleHealth() call to re-probe storage instead\r\n * of serving the TTL-cached result. Not exported from the package's public\r\n * entry (index.ts) — internal to this module's test suite. */\r\nexport function _resetStorageCheckCacheForTests(): void {\r\n cachedStorageCheck = undefined\r\n cachedAt = 0\r\n}\r\n\r\nfunction isConfigComplete(config: UpupServerConfig): boolean {\r\n const secretOk = Boolean(\r\n config.uploadTokenSecret && config.uploadTokenSecret.length >= 16,\r\n )\r\n // A resolver has no fields to check until a request supplies a context;\r\n // its result is validated per request instead (#337).\r\n if (isStorageResolver(config.storage)) return secretOk\r\n return Boolean(config.storage.bucket && config.storage.region && secretOk)\r\n}\r\n\r\nasync function sha256Hex(input: string): Promise<string> {\r\n const bytes = new TextEncoder().encode(input)\r\n const digest = await crypto.subtle.digest('SHA-256', bytes)\r\n return Array.from(new Uint8Array(digest))\r\n .map(b => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n}\r\n\r\nexport async function handleHealth(\r\n config: UpupServerConfig,\r\n res: Responder,\r\n): Promise<Response> {\r\n const configOk = isConfigComplete(config)\r\n\r\n // With a storage RESOLVER there is no single destination to probe, and\r\n // inventing a synthetic request to feed the integrator's resolver would\r\n // reach a real backend on an unauthenticated route. Report the probe as\r\n // skipped rather than guessing (#337).\r\n const staticStorage = isStorageResolver(config.storage)\r\n ? null\r\n : config.storage\r\n const now = Date.now()\r\n if (\r\n staticStorage &&\r\n (!cachedStorageCheck || now - cachedAt > STORAGE_CHECK_TTL_MS)\r\n ) {\r\n const result = await checkStorageReachable(staticStorage)\r\n if (!result.ok) {\r\n reportServerError(config.onError, {\r\n route: 'health',\r\n method: 'GET',\r\n status: 200,\r\n code: 'STORAGE_ERROR',\r\n message: 'Health check: storage unreachable',\r\n requestId: res.requestId,\r\n error: toSafeError(result.error),\r\n })\r\n }\r\n cachedStorageCheck = result.ok ? { ok: true } : { ok: false }\r\n cachedAt = now\r\n }\r\n\r\n const storageOk = staticStorage ? Boolean(cachedStorageCheck?.ok) : true\r\n const body: Record<string, unknown> = {\r\n status: configOk && storageOk ? 'ok' : 'degraded',\r\n checks: {\r\n config: configOk ? 'ok' : 'incomplete',\r\n storage: !staticStorage ? 'skipped' : storageOk ? 'ok' : 'error',\r\n },\r\n // Non-secret operational summary — labels/flags/counts only, never any\r\n // secret VALUE. Lets an operator eyeball how an instance is configured\r\n // (which storage backend, whether anonymous access is open, how many\r\n // drive providers are wired, the upload-token lifetime) from the same\r\n // unauthenticated probe.\r\n summary: {\r\n storageType: staticStorage ? staticStorage.type : 'dynamic',\r\n anonymousUploads: Boolean(config.allowAnonymousUploads),\r\n anonymousDrives: Boolean(config.allowAnonymous),\r\n driveProviders: config.providers\r\n ? Object.keys(config.providers).length\r\n : 0,\r\n uploadTokenTtlSeconds: DEFAULT_UPLOAD_TOKEN_TTL_SECONDS,\r\n },\r\n }\r\n\r\n if (config.health?.exposeSecretFingerprint && config.uploadTokenSecret) {\r\n body.uploadTokenFingerprint = (\r\n await sha256Hex(config.uploadTokenSecret)\r\n ).slice(0, 8)\r\n }\r\n\r\n // Always 200: this is a liveness-friendly endpoint (deploy/orchestration\r\n // probes should not 5xx a container just because S3 blipped); the `status`\r\n // field carries the actual health signal for humans/dashboards.\r\n return res.json(body)\r\n}\r\n","// packages/server/src/respond.ts\r\n//\r\n// The single home for forming an HTTP response in @useupup/server (F-108). A\r\n// per-request Responder closes over the request's CORS headers AND an\r\n// x-upup-request-id, so every route returns through `res.json`/`html`/\r\n// `redirect`/`noContent`/`fail` and can NEVER forget the headers — omission is\r\n// structurally impossible (there is no header parameter to leave off). After\r\n// this module, a route that composes its own `new Response(...)` is a defect.\r\n\r\nimport type { UpupServerConfig } from './config'\r\nimport { reportServerError, toSafeError } from './observability'\r\nimport { UpupErrorCode } from '@useupup/core'\r\n\r\nexport type ResponseHeaders = Record<string, string>\r\n\r\nexport function corsHeaders(\r\n req: Request,\r\n config: UpupServerConfig,\r\n): ResponseHeaders {\r\n const cors = config.cors\r\n if (!cors) return {}\r\n\r\n const origin = req.headers.get('origin') ?? ''\r\n const allowsWildcard = cors.allowedOrigins.includes('*')\r\n const allowsOrigin = origin && cors.allowedOrigins.includes(origin)\r\n if (!allowsWildcard && !allowsOrigin) return {}\r\n\r\n // Never send a literal '*' to a browser (Origin present): reflect the matched\r\n // origin so no route (incl. /files/*, /presign) exposes a bare wildcard. '*'\r\n // is emitted only for origin-less (non-browser) requests (audit S3).\r\n const allowOrigin = origin ? origin : '*'\r\n const headers: ResponseHeaders = {\r\n 'Access-Control-Allow-Origin': allowOrigin,\r\n 'Access-Control-Allow-Methods': (\r\n cors.allowedMethods ?? ['GET', 'POST', 'OPTIONS']\r\n ).join(', '),\r\n 'Access-Control-Allow-Headers': (\r\n cors.allowedHeaders ?? ['Content-Type', 'Authorization']\r\n ).join(', '),\r\n 'Access-Control-Max-Age': String(cors.maxAgeSeconds ?? 600),\r\n Vary: 'Origin',\r\n }\r\n // Credentialed CORS is gated on a CONCRETE allowlist match only — NEVER a\r\n // wildcard-only match. Reflecting an arbitrary origin (allowed solely via '*')\r\n // together with credentials would let any site make credentialed cross-origin\r\n // reads, so a '*'-configured server gets public, NON-credentialed CORS. To use\r\n // the server-mode drive client (`credentials: 'include'`), operators must\r\n // enumerate their app origin(s) in `allowedOrigins` (audit S3 / CORS review).\r\n if (allowsOrigin && allowOrigin !== '*') {\r\n headers['Access-Control-Allow-Credentials'] = 'true'\r\n }\r\n return headers\r\n}\r\n\r\nexport type Responder = {\r\n /** This request's correlation id — also emitted as the\r\n * x-upup-request-id header on every response. */\r\n requestId: string\r\n json(data: unknown, status?: number): Response\r\n html(body: string, status?: number): Response\r\n redirect(location: string, status?: number): Response\r\n noContent(status?: number): Response\r\n /** One home for every logged failure: reports via the onError seam (redacted),\r\n * then returns the uniform `{ error, code }` body. The real cause goes to the\r\n * logger, never the client. */\r\n fail(\r\n route: string,\r\n method: string,\r\n status: number,\r\n code: string,\r\n message: string,\r\n error: unknown,\r\n ): Response\r\n}\r\n\r\nexport function createResponder(\r\n req: Request,\r\n config: UpupServerConfig,\r\n): Responder {\r\n const requestId = crypto.randomUUID()\r\n const headers: ResponseHeaders = {\r\n ...corsHeaders(req, config),\r\n 'x-upup-request-id': requestId,\r\n }\r\n const json = (data: unknown, status = 200): Response =>\r\n new Response(JSON.stringify(data), {\r\n status,\r\n headers: { 'Content-Type': 'application/json', ...headers },\r\n })\r\n return {\r\n requestId,\r\n json,\r\n html: (body, status = 200) =>\r\n new Response(body, {\r\n status,\r\n headers: {\r\n 'Content-Type': 'text/html; charset=utf-8',\r\n ...headers,\r\n },\r\n }),\r\n redirect: (location, status = 302) =>\r\n new Response(null, {\r\n status,\r\n headers: { ...headers, Location: location },\r\n }),\r\n noContent: (status = 204) => new Response(null, { status, headers }),\r\n fail: (route, method, status, code, message, error) => {\r\n reportServerError(config.onError, {\r\n route,\r\n method,\r\n status,\r\n code,\r\n message,\r\n requestId,\r\n error: toSafeError(error),\r\n })\r\n return json({ error: message, code }, status)\r\n },\r\n }\r\n}\r\n\r\n/** Parse a JSON request body, returning a 400 Response (not an unhandled throw)\r\n * on malformed JSON. Collapses the two divergent body-parse sites. */\r\nexport async function parseJsonBody(\r\n req: Request,\r\n res: Responder,\r\n): Promise<{ ok: true; value: unknown } | { ok: false; response: Response }> {\r\n try {\r\n const value: unknown = await req.json()\r\n return { ok: true, value }\r\n } catch {\r\n // upup-catch: malformed JSON becomes a 400 Result the caller returns —\r\n // the parse failure is surfaced to the client, not swallowed.\r\n return {\r\n ok: false,\r\n response: res.json(\r\n { error: 'Invalid JSON body', code: UpupErrorCode.BAD_REQUEST },\r\n 400,\r\n ),\r\n }\r\n }\r\n}\r\n","// packages/server/src/upload-routes.ts\r\n//\r\n// The HMAC upload trust boundary in isolation: upload-metadata validation, the\r\n// S3 presign route, and the full multipart lifecycle (init/sign-part/complete/\r\n// abort) with its token issue + verify + owner-binding + signed size-envelope\r\n// enforcement. Extracted verbatim from handler.ts (F-101/F-505) so a reviewer\r\n// can read the trust core without wading through OAuth or drive-provider code.\r\n// The HMAC/token/envelope logic is UNCHANGED — this is a move, not a rewrite.\r\n\r\nimport {\r\n UpupErrorCode,\r\n UpupError,\r\n type MultipartResumeResponse,\r\n} from '@useupup/core'\r\nimport type {\r\n UpupServerConfig,\r\n FileMetadata,\r\n UploadedFile,\r\n PresignResponseBody,\r\n PresignResponseContext,\r\n} from './config'\r\nimport {\r\n generatePresignedUrl,\r\n initiateMultipartUpload,\r\n generatePresignedPartUrl,\r\n completeMultipartUpload,\r\n abortMultipartUpload,\r\n getMultipartUploadedSize,\r\n listMultipartParts,\r\n} from './providers/aws'\r\nimport {\r\n assertUploadTokenSecret,\r\n signUploadToken,\r\n verifyUploadToken,\r\n UploadTokenError,\r\n DEFAULT_UPLOAD_TOKEN_TTL_SECONDS,\r\n type UploadTokenPayload,\r\n} from './uploadToken'\r\nimport { resolveUserId, DEFAULT_USER_ID } from './tokenStore'\r\nimport { defaultKeyStrategy } from './key'\r\nimport { reportServerError, toSafeError } from './observability'\r\nimport { parseJsonBody, type Responder } from './respond'\r\nimport {\r\n isStorageResolver,\r\n resolveStorage,\r\n resolveBoundStorage,\r\n storageIdentity,\r\n StorageBindingError,\r\n} from './resolve-storage'\r\nimport type { UpupStorageConfig, StorageResolverContext } from './config'\r\n\r\n/** Secure-by-default gate for the capability-granting upload routes (/presign,\r\n * /multipart/init): reject an unauthenticated, unidentified caller unless the\r\n * integrator explicitly opts into the shared anonymous namespace. With\r\n * `config.auth` set, an unauthorized caller is already 401'd by the global\r\n * gate; with `config.getUserId` set, an unauthenticated caller is already\r\n * 401'd inside the route (resolveUserId returns null) — this check only bites\r\n * when NEITHER is configured, which previously fell through to the non-null\r\n * DEFAULT_USER_ID and let the upload proceed (F-110). */\r\nfunction requireUploadAuthorization(\r\n config: UpupServerConfig,\r\n res: Responder,\r\n route: string,\r\n method: string,\r\n): Response | null {\r\n if (!config.auth && !config.getUserId && !config.allowAnonymousUploads) {\r\n return res.fail(\r\n route,\r\n method,\r\n 403,\r\n UpupErrorCode.AUTH_REQUIRED,\r\n 'Anonymous uploads are disabled. Set allowAnonymousUploads:true, or configure auth/getUserId.',\r\n new Error('anonymous upload rejected'),\r\n )\r\n }\r\n return null\r\n}\r\n\r\n/** How long after the original init an upload stays resumable, when the\r\n * integrator sets no `multipartResumeWindowSeconds`. 24h — the client's\r\n * localStorage session TTL, so neither side outlives the other. */\r\nexport const DEFAULT_MULTIPART_RESUME_WINDOW_SECONDS = 86_400\r\n\r\n/** Verify an upload token, or return the 403 Response to send as-is. Collapses\r\n * the three duplicated inner try/catch blocks in sign-part/complete/abort into\r\n * one call site, and surfaces the token's own malformed/bad_signature/expired\r\n * code at the HTTP boundary (previously all three collapsed into one 403).\r\n * `allowExpired` is set by /multipart/resume ALONE — see the note on\r\n * VerifyUploadTokenOptions; every other caller keeps today's expiry rejection. */\r\nasync function verifyTokenOrRespond(\r\n config: UpupServerConfig,\r\n token: string,\r\n res: Responder,\r\n route: string,\r\n method: string,\r\n allowExpired = false,\r\n): Promise<UploadTokenPayload | Response> {\r\n assertUploadTokenSecret(config.uploadTokenSecret)\r\n try {\r\n return await verifyUploadToken(\r\n config.uploadTokenSecret,\r\n token,\r\n Date.now(),\r\n { allowExpired },\r\n )\r\n } catch (e) {\r\n if (e instanceof UploadTokenError) {\r\n reportServerError(config.onError, {\r\n route,\r\n method,\r\n status: 403,\r\n code: e.code,\r\n message: 'Invalid upload token',\r\n requestId: res.requestId,\r\n error: toSafeError(e),\r\n })\r\n return res.json(\r\n { error: 'Invalid upload token', code: e.code },\r\n 403,\r\n )\r\n }\r\n throw e\r\n }\r\n}\r\n\r\n/** Re-check the caller's resolved identity against a verified token's bound\r\n * uid on the multipart continuation routes (sign-part/complete/abort). The\r\n * token itself only proves possession, not who currently holds it — so a\r\n * leaked token could otherwise be replayed by a different authenticated user\r\n * (F-106). Skipped entirely when no `getUserId` resolver exists: `payload.uid`\r\n * is then always null (init never had an identity to bind), so token\r\n * possession remains the intentional model, documented in the README (F-107). */\r\nasync function enforceTokenOwner(\r\n config: UpupServerConfig,\r\n req: Request,\r\n payload: UploadTokenPayload,\r\n res: Responder,\r\n route: string,\r\n method: string,\r\n): Promise<Response | null> {\r\n if (!config.getUserId) return null\r\n const currentUserId = await resolveUserId(config, req)\r\n const currentOwner =\r\n currentUserId === DEFAULT_USER_ID ? null : currentUserId\r\n if (currentOwner !== payload.uid) {\r\n return res.fail(\r\n route,\r\n method,\r\n 403,\r\n UpupErrorCode.AUTH_DENIED,\r\n 'Upload token does not belong to the current user',\r\n new Error('upload-token uid mismatch'),\r\n )\r\n }\r\n return null\r\n}\r\n\r\n/** The single allowed-types policy, shared by the presign/multipart path and\r\n * the drive-transfer path (F-743). No allowlist -> everything passes; an\r\n * `image/*` entry honours the wildcard; an absent/empty type does NOT match a\r\n * non-empty allowlist (callers pass `mimeType ?? ''` so a missing type is\r\n * rejected identically on both paths, never bypassed). */\r\nexport function matchesAllowedType(\r\n type: string,\r\n allowedTypes?: string[],\r\n): boolean {\r\n if (!allowedTypes?.length) return true\r\n return allowedTypes.some(allowed => {\r\n if (allowed === type) return true\r\n if (allowed.endsWith('/*')) {\r\n return type.startsWith(`${allowed.slice(0, -2)}/`)\r\n }\r\n return false\r\n })\r\n}\r\n\r\nasync function validateUploadMetadata(\r\n req: Request,\r\n config: UpupServerConfig,\r\n body: FileMetadata,\r\n res: Responder,\r\n): Promise<Response | null> {\r\n if (\r\n typeof body.name !== 'string' ||\r\n body.name.length === 0 ||\r\n typeof body.type !== 'string' ||\r\n typeof body.size !== 'number' ||\r\n !Number.isFinite(body.size) ||\r\n body.size < 0\r\n ) {\r\n return res.json(\r\n { error: 'Invalid file metadata', code: UpupErrorCode.BAD_REQUEST },\r\n 400,\r\n )\r\n }\r\n\r\n if (config.maxFileSize && body.size > config.maxFileSize) {\r\n return res.json({ error: 'File too large' }, 413)\r\n }\r\n\r\n if (!matchesAllowedType(body.type, config.allowedTypes)) {\r\n return res.json({ error: 'File type not allowed' }, 415)\r\n }\r\n\r\n if (config.hooks?.onBeforeUpload) {\r\n let allowed: boolean\r\n try {\r\n allowed = await config.hooks.onBeforeUpload(body, req)\r\n } catch (error) {\r\n // upup-catch: an UpupError is the integrator DELIBERATELY speaking to\r\n // the client — serialize its message + code into the 403 so a quota\r\n // check can explain itself (#338). Anything else is an unexpected\r\n // failure and is re-thrown, so it surfaces as a generic 500 with the\r\n // real cause going only to onError. Never leak internal error text.\r\n if (error instanceof UpupError) {\r\n return res.json({ error: error.message, code: error.code }, 403)\r\n }\r\n throw error\r\n }\r\n if (!allowed) {\r\n return res.json({ error: 'Upload rejected' }, 403)\r\n }\r\n }\r\n\r\n return null\r\n}\r\n\r\n/** Resolve the storage for a fresh upload (#337). A resolver that returns\r\n * something unusable is a server misconfiguration, so it fails the request as\r\n * a logged 500 — the real cause goes to onError, the client gets a fixed\r\n * message. Never fall back to a default bucket: a misrouted write is worse\r\n * than a failed one. */\r\nasync function resolveStorageOrFail(\r\n config: UpupServerConfig,\r\n res: Responder,\r\n route: string,\r\n method: string,\r\n ctx: StorageResolverContext,\r\n): Promise<UpupStorageConfig | Response> {\r\n try {\r\n return await resolveStorage(config, ctx)\r\n } catch (error) {\r\n return res.fail(\r\n route,\r\n method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Storage configuration error',\r\n error,\r\n )\r\n }\r\n}\r\n\r\n/** Resolve the storage for a multipart CONTINUATION, pinned to the identity the\r\n * init bound into the token. A binding that cannot be honoured is a 403, not a\r\n * 500 — the request is not authorized for that storage. */\r\nasync function resolveBoundStorageOrFail(\r\n config: UpupServerConfig,\r\n res: Responder,\r\n route: string,\r\n method: string,\r\n ctx: StorageResolverContext,\r\n boundId: string | undefined,\r\n): Promise<UpupStorageConfig | Response> {\r\n try {\r\n return await resolveBoundStorage(config, ctx, boundId)\r\n } catch (error) {\r\n if (error instanceof StorageBindingError) {\r\n return res.fail(\r\n route,\r\n method,\r\n 403,\r\n UpupErrorCode.AUTH_DENIED,\r\n 'Upload token is not valid for the resolved storage',\r\n error,\r\n )\r\n }\r\n return res.fail(\r\n route,\r\n method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Storage configuration error',\r\n error,\r\n )\r\n }\r\n}\r\n\r\n/** Give `hooks.onPresignResponse` the last look at a presign-side payload\r\n * (#338). Runs after every auth/policy/token check, so it can rewrite where\r\n * the browser sends bytes but can never widen what the caller was allowed to\r\n * do. Returning nothing keeps the payload untouched. */\r\nasync function applyPresignResponseHook(\r\n config: UpupServerConfig,\r\n response: PresignResponseBody,\r\n ctx: PresignResponseContext,\r\n): Promise<PresignResponseBody> {\r\n const hook = config.hooks?.onPresignResponse\r\n if (!hook) return response\r\n return (await hook(response, ctx)) ?? response\r\n}\r\n\r\n/** Run integrator post-completion hooks (onFileUploaded/onUploadComplete) AFTER\r\n * the object is durably written, in their OWN try so a throwing hook is logged\r\n * via onError and swallowed — never re-coded as a 500 that would tell the\r\n * client to retry an already-stored object (F-745). Shared by the multipart-\r\n * complete and drive-transfer completion paths so both behave identically. */\r\nexport async function runPostCompletionHooks(\r\n config: UpupServerConfig,\r\n res: Responder,\r\n route: string,\r\n method: string,\r\n run: () => Promise<void>,\r\n): Promise<void> {\r\n if (!config.hooks) return\r\n try {\r\n await run()\r\n } catch (error) {\r\n // upup-catch: a post-completion hook failure is REPORTED (below) and\r\n // deliberately swallowed — the upload already durably succeeded, so it\r\n // must not propagate and re-code the committed write as a 500 (F-745).\r\n reportServerError(config.onError, {\r\n route,\r\n method,\r\n status: 200,\r\n // No dedicated HOOK_ERROR code exists in @useupup/core; the message\r\n // carries the real meaning — the upload itself succeeded.\r\n code: UpupErrorCode.STORAGE_ERROR,\r\n message:\r\n 'Post-completion hook threw after a durably-completed upload; ' +\r\n 'the upload succeeded and the client was returned 200 (F-745).',\r\n requestId: res.requestId,\r\n error: toSafeError(error),\r\n })\r\n }\r\n}\r\n\r\nexport async function handlePresign(\r\n req: Request,\r\n config: UpupServerConfig,\r\n res: Responder,\r\n): Promise<Response> {\r\n const gate = requireUploadAuthorization(config, res, 'presign', req.method)\r\n if (gate) return gate\r\n\r\n const parsed = await parseJsonBody(req, res)\r\n if (!parsed.ok) return parsed.response\r\n const body = parsed.value as FileMetadata\r\n\r\n const validationError = await validateUploadMetadata(req, config, body, res)\r\n if (validationError) return validationError\r\n\r\n const userId = await resolveUserId(config, req)\r\n if (userId === null) return res.json({ error: 'Unauthenticated' }, 401)\r\n const owner = userId === DEFAULT_USER_ID ? null : userId\r\n\r\n const key = (config.keyStrategy ?? defaultKeyStrategy)({\r\n userId: owner,\r\n fileName: body.name,\r\n contentType: body.type,\r\n size: body.size,\r\n ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),\r\n req,\r\n })\r\n\r\n const storage = await resolveStorageOrFail(\r\n config,\r\n res,\r\n 'presign',\r\n req.method,\r\n {\r\n req,\r\n phase: 'presign',\r\n userId: owner,\r\n ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),\r\n fileName: body.name,\r\n contentType: body.type,\r\n size: body.size,\r\n },\r\n )\r\n if (storage instanceof Response) return storage\r\n\r\n try {\r\n const result = await generatePresignedUrl(\r\n storage,\r\n key,\r\n body.type,\r\n body.size,\r\n undefined,\r\n config.downloadUrlExpiresIn,\r\n )\r\n const payload = await applyPresignResponseHook(config, result, {\r\n req,\r\n phase: 'presign',\r\n // Always the key that is IN the payload, on every phase.\r\n key: result.key,\r\n file: body,\r\n ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),\r\n userId: owner,\r\n })\r\n return res.json(payload, 200)\r\n } catch (error) {\r\n return res.fail(\r\n 'presign',\r\n req.method,\r\n 500,\r\n UpupErrorCode.PRESIGN_FAILED,\r\n 'Presign failed',\r\n error,\r\n )\r\n }\r\n}\r\n\r\nexport async function handleMultipartInit(\r\n req: Request,\r\n config: UpupServerConfig,\r\n res: Responder,\r\n): Promise<Response> {\r\n const gate = requireUploadAuthorization(\r\n config,\r\n res,\r\n 'multipart/init',\r\n req.method,\r\n )\r\n if (gate) return gate\r\n\r\n const parsed = await parseJsonBody(req, res)\r\n if (!parsed.ok) return parsed.response\r\n const body = parsed.value as FileMetadata & { chunkSizeBytes?: number }\r\n\r\n try {\r\n const validationError = await validateUploadMetadata(\r\n req,\r\n config,\r\n body,\r\n res,\r\n )\r\n if (validationError) return validationError\r\n\r\n const userId = await resolveUserId(config, req)\r\n if (userId === null) return res.json({ error: 'Unauthenticated' }, 401)\r\n const owner = userId === DEFAULT_USER_ID ? null : userId\r\n\r\n const key = (config.keyStrategy ?? defaultKeyStrategy)({\r\n userId: owner,\r\n fileName: body.name,\r\n contentType: body.type,\r\n size: body.size,\r\n ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),\r\n req,\r\n })\r\n\r\n const storage = await resolveStorageOrFail(\r\n config,\r\n res,\r\n 'multipart/init',\r\n req.method,\r\n {\r\n req,\r\n phase: 'multipart-init',\r\n userId: owner,\r\n ...(body.metadata !== undefined\r\n ? { metadata: body.metadata }\r\n : {}),\r\n fileName: body.name,\r\n contentType: body.type,\r\n size: body.size,\r\n },\r\n )\r\n if (storage instanceof Response) return storage\r\n\r\n const result = await initiateMultipartUpload(\r\n storage,\r\n key,\r\n body.type,\r\n body.size,\r\n undefined,\r\n body.chunkSizeBytes,\r\n )\r\n assertUploadTokenSecret(config.uploadTokenSecret)\r\n // Bind the resolved destination into the SIGNED token so every\r\n // continuation provably lands in this bucket (#337). Omitted entirely\r\n // for a static config — there is only one destination, and omitting it\r\n // keeps the static token byte-identical to before.\r\n const sid = isStorageResolver(config.storage)\r\n ? await storageIdentity(storage)\r\n : undefined\r\n // `iat` stamps the moment this upload was born. /multipart/resume anchors\r\n // its window here and carries the value forward untouched, so no chain of\r\n // resumes can outlive the window the original init started.\r\n const issuedAt = Math.floor(Date.now() / 1000)\r\n const token = await signUploadToken(config.uploadTokenSecret, {\r\n k: result.key,\r\n u: result.uploadId,\r\n uid: owner,\r\n smin: 0,\r\n smax: body.size,\r\n ...(sid !== undefined ? { sid } : {}),\r\n exp: issuedAt + DEFAULT_UPLOAD_TOKEN_TTL_SECONDS,\r\n iat: issuedAt,\r\n })\r\n const payload = await applyPresignResponseHook(\r\n config,\r\n { ...result, token },\r\n {\r\n req,\r\n phase: 'multipart-init',\r\n key: result.key,\r\n file: body,\r\n ...(body.metadata !== undefined\r\n ? { metadata: body.metadata }\r\n : {}),\r\n userId: owner,\r\n },\r\n )\r\n return res.json(payload, 200)\r\n } catch (error) {\r\n return res.fail(\r\n 'multipart/init',\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Multipart init failed',\r\n error,\r\n )\r\n }\r\n}\r\n\r\n/** S3/MinIO's \"that multipart upload is gone\" signal — it completed, was\r\n * aborted, or a lifecycle rule reaped it. Matched on the modelled error name /\r\n * wire Code only, NOT on a bare 404 status: a missing BUCKET also 404s, and\r\n * mistaking that for a dead upload would tell the client its session is stale\r\n * when the deployment is actually misconfigured. */\r\nfunction isNoSuchUpload(error: unknown): boolean {\r\n if (typeof error !== 'object' || error === null) return false\r\n const shape = error as { name?: unknown; Code?: unknown }\r\n return shape.name === 'NoSuchUpload' || shape.Code === 'NoSuchUpload'\r\n}\r\n\r\n/**\r\n * Re-attach to a multipart upload a previous page-load left in flight: hand\r\n * back the parts the provider already holds and a fresh token, so the client\r\n * can finish the file instead of re-uploading it from byte zero.\r\n *\r\n * Trust posture is deliberately identical to sign-part/complete/abort — the\r\n * token is the ONLY carrier. The client sends nothing but the token: no key, no\r\n * uploadId, and none comes back either (S2 stays closed). Sits after the global\r\n * `config.auth` gate like the other continuation routes, and owner-binds via\r\n * `enforceTokenOwner` whenever `getUserId` exists.\r\n *\r\n * The ONE relaxation: an expired `exp` is accepted, because handing an expired\r\n * token back as a fresh one is the entire point (an upload can easily outlive\r\n * the 1h TTL). A tighter bound replaces it — the resume window, measured from\r\n * the ORIGINAL init's `iat` and carried forward on every re-issue, so this is a\r\n * fixed-length extension of a token's life, never an open-ended renewal.\r\n */\r\nexport async function handleMultipartResume(\r\n req: Request,\r\n config: UpupServerConfig,\r\n res: Responder,\r\n): Promise<Response> {\r\n const parsed = await parseJsonBody(req, res)\r\n if (!parsed.ok) return parsed.response\r\n const body = parsed.value as { token: string }\r\n try {\r\n const payload = await verifyTokenOrRespond(\r\n config,\r\n body.token,\r\n res,\r\n 'multipart/resume',\r\n req.method,\r\n true, // allowExpired — the resume window below is the real bound\r\n )\r\n if (payload instanceof Response) return payload\r\n\r\n const nowSeconds = Math.floor(Date.now() / 1000)\r\n // Tokens minted before `iat` existed still resume: their init time is\r\n // recoverable from the expiry, since init is the only issuer and always\r\n // used the same TTL.\r\n const issuedAt =\r\n payload.iat ?? payload.exp - DEFAULT_UPLOAD_TOKEN_TTL_SECONDS\r\n const windowSeconds =\r\n config.multipartResumeWindowSeconds ??\r\n DEFAULT_MULTIPART_RESUME_WINDOW_SECONDS\r\n if (nowSeconds > issuedAt + windowSeconds) {\r\n // Code stays the literal 'expired' the token layer uses: the client\r\n // keys its mid-flight token refresh on `403 + code:'expired'`, and a\r\n // second spelling here would silently break that path.\r\n return res.fail(\r\n 'multipart/resume',\r\n req.method,\r\n 403,\r\n 'expired',\r\n 'Upload resume window has expired',\r\n new Error('resume window elapsed'),\r\n )\r\n }\r\n\r\n const owned = await enforceTokenOwner(\r\n config,\r\n req,\r\n payload,\r\n res,\r\n 'multipart/resume',\r\n req.method,\r\n )\r\n if (owned) return owned\r\n\r\n const storage = await resolveBoundStorageOrFail(\r\n config,\r\n res,\r\n 'multipart/resume',\r\n req.method,\r\n { req, phase: 'multipart-resume', userId: payload.uid },\r\n payload.sid,\r\n )\r\n if (storage instanceof Response) return storage\r\n\r\n let listed\r\n try {\r\n listed = await listMultipartParts(storage, payload.k, payload.u)\r\n } catch (error) {\r\n // upup-catch: a dead upload is a 4xx, never a 5xx. The client drops\r\n // its session and starts fresh on any 4xx; a 5xx would read as\r\n // \"try again later\" and retry an upload that can never come back.\r\n if (isNoSuchUpload(error)) {\r\n return res.fail(\r\n 'multipart/resume',\r\n req.method,\r\n 404,\r\n UpupErrorCode.NOT_FOUND,\r\n 'Multipart upload no longer exists',\r\n error,\r\n )\r\n }\r\n throw error\r\n }\r\n\r\n assertUploadTokenSecret(config.uploadTokenSecret)\r\n const token = await signUploadToken(config.uploadTokenSecret, {\r\n // Every binding is copied from the VERIFIED payload, unchanged: same\r\n // key, same uploadId, same owner, same size envelope, same storage\r\n // identity. Only `exp` moves — `iat` is the original, so the window\r\n // does not roll.\r\n k: payload.k,\r\n u: payload.u,\r\n uid: payload.uid,\r\n smin: payload.smin,\r\n smax: payload.smax,\r\n ...(payload.sid !== undefined ? { sid: payload.sid } : {}),\r\n // Clamp to the resume window, not a fresh full TTL. The window is\r\n // the operator's stated cap on how long a leaked token stays\r\n // usable (multipartResumeWindowSeconds); a re-issued token that\r\n // outlived it by up to a full TTL would make that cap — and the\r\n // documented leak mitigation — a lie.\r\n exp: Math.min(\r\n nowSeconds + DEFAULT_UPLOAD_TOKEN_TTL_SECONDS,\r\n issuedAt + windowSeconds,\r\n ),\r\n iat: issuedAt,\r\n })\r\n\r\n const response: MultipartResumeResponse = {\r\n key: payload.k,\r\n token,\r\n parts: listed.parts,\r\n }\r\n return res.json(response, 200)\r\n } catch (error) {\r\n return res.fail(\r\n 'multipart/resume',\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Multipart resume failed',\r\n error,\r\n )\r\n }\r\n}\r\n\r\nexport async function handleMultipartSignPart(\r\n req: Request,\r\n config: UpupServerConfig,\r\n res: Responder,\r\n): Promise<Response> {\r\n const parsed = await parseJsonBody(req, res)\r\n if (!parsed.ok) return parsed.response\r\n const body = parsed.value as { token: string; partNumber: number }\r\n try {\r\n const payload = await verifyTokenOrRespond(\r\n config,\r\n body.token,\r\n res,\r\n 'multipart/sign-part',\r\n req.method,\r\n )\r\n if (payload instanceof Response) return payload\r\n const owned = await enforceTokenOwner(\r\n config,\r\n req,\r\n payload,\r\n res,\r\n 'multipart/sign-part',\r\n req.method,\r\n )\r\n if (owned) return owned\r\n const storage = await resolveBoundStorageOrFail(\r\n config,\r\n res,\r\n 'multipart/sign-part',\r\n req.method,\r\n {\r\n req,\r\n phase: 'multipart-sign-part',\r\n userId: payload.uid,\r\n },\r\n payload.sid,\r\n )\r\n if (storage instanceof Response) return storage\r\n const result = await generatePresignedPartUrl(\r\n storage,\r\n payload.k,\r\n payload.u,\r\n body.partNumber,\r\n )\r\n const rewritten = await applyPresignResponseHook(config, result, {\r\n req,\r\n phase: 'multipart-sign-part',\r\n // From the VERIFIED token — sign-part never sees a client-asserted\r\n // key, and has no file metadata to report.\r\n key: payload.k,\r\n userId: payload.uid,\r\n })\r\n return res.json(rewritten, 200)\r\n } catch (error) {\r\n return res.fail(\r\n 'multipart/sign-part',\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Multipart sign failed',\r\n error,\r\n )\r\n }\r\n}\r\n\r\nexport async function handleMultipartComplete(\r\n req: Request,\r\n config: UpupServerConfig,\r\n res: Responder,\r\n): Promise<Response> {\r\n const parsed = await parseJsonBody(req, res)\r\n if (!parsed.ok) return parsed.response\r\n const body = parsed.value as {\r\n token: string\r\n parts: Array<{ partNumber: number; eTag: string }>\r\n }\r\n try {\r\n const payload = await verifyTokenOrRespond(\r\n config,\r\n body.token,\r\n res,\r\n 'multipart/complete',\r\n req.method,\r\n )\r\n if (payload instanceof Response) return payload\r\n const owned = await enforceTokenOwner(\r\n config,\r\n req,\r\n payload,\r\n res,\r\n 'multipart/complete',\r\n req.method,\r\n )\r\n if (owned) return owned\r\n\r\n const storage = await resolveBoundStorageOrFail(\r\n config,\r\n res,\r\n 'multipart/complete',\r\n req.method,\r\n { req, phase: 'multipart-complete', userId: payload.uid },\r\n payload.sid,\r\n )\r\n if (storage instanceof Response) return storage\r\n\r\n // S1 (multipart): smin/smax are SIGNED at init but must be ENFORCED here —\r\n // otherwise a client can init with a tiny declared size (tiny smax) and\r\n // upload arbitrarily large real parts, since sign-part/PUT never sees the\r\n // client-declared size. Sum the bytes S3 actually received (ListParts) and\r\n // reject + abort if outside the signed envelope.\r\n const uploadedSize = await getMultipartUploadedSize(\r\n storage,\r\n payload.k,\r\n payload.u,\r\n )\r\n if (uploadedSize < payload.smin || uploadedSize > payload.smax) {\r\n await abortMultipartUpload(storage, payload.k, payload.u)\r\n return res.json(\r\n { error: 'Upload size outside signed envelope' },\r\n 403,\r\n )\r\n }\r\n\r\n const result = await completeMultipartUpload(\r\n storage,\r\n payload.k,\r\n payload.u,\r\n body.parts,\r\n config.downloadUrlExpiresIn,\r\n )\r\n\r\n const uploaded: UploadedFile = {\r\n key: result.key,\r\n name: result.key.split('/').pop() ?? result.key,\r\n size: uploadedSize, // already computed above for the envelope check\r\n type: '', // not retained server-side on the multipart path\r\n url: result.downloadUrl ?? '',\r\n }\r\n\r\n // Post-commit: the object is durably in S3. A throwing hook is logged +\r\n // swallowed here, never bubbling to the catch below as a 500 (F-745).\r\n await runPostCompletionHooks(\r\n config,\r\n res,\r\n 'multipart/complete',\r\n req.method,\r\n async () => {\r\n if (config.hooks?.onFileUploaded)\r\n await config.hooks.onFileUploaded(uploaded, req)\r\n if (config.hooks?.onUploadComplete)\r\n await config.hooks.onUploadComplete([uploaded], req)\r\n },\r\n )\r\n\r\n return res.json(result, 200)\r\n } catch (error) {\r\n return res.fail(\r\n 'multipart/complete',\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Multipart complete failed',\r\n error,\r\n )\r\n }\r\n}\r\n\r\nexport async function handleMultipartAbort(\r\n req: Request,\r\n config: UpupServerConfig,\r\n res: Responder,\r\n): Promise<Response> {\r\n const parsed = await parseJsonBody(req, res)\r\n if (!parsed.ok) return parsed.response\r\n const body = parsed.value as { token: string }\r\n try {\r\n const payload = await verifyTokenOrRespond(\r\n config,\r\n body.token,\r\n res,\r\n 'multipart/abort',\r\n req.method,\r\n )\r\n if (payload instanceof Response) return payload\r\n const owned = await enforceTokenOwner(\r\n config,\r\n req,\r\n payload,\r\n res,\r\n 'multipart/abort',\r\n req.method,\r\n )\r\n if (owned) return owned\r\n const storage = await resolveBoundStorageOrFail(\r\n config,\r\n res,\r\n 'multipart/abort',\r\n req.method,\r\n { req, phase: 'multipart-abort', userId: payload.uid },\r\n payload.sid,\r\n )\r\n if (storage instanceof Response) return storage\r\n const result = await abortMultipartUpload(storage, payload.k, payload.u)\r\n return res.json(result, 200)\r\n } catch (error) {\r\n return res.fail(\r\n 'multipart/abort',\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Multipart abort failed',\r\n error,\r\n )\r\n }\r\n}\r\n","import type {\r\n TokenStore,\r\n DriveTokens,\r\n OAuthState,\r\n UpupServerConfig,\r\n} from './config'\r\n\r\n/**\r\n * Zero-dependency reference implementation. Fine for demos, single-process\r\n * dev, and serverless runtimes where the function is warm. NOT suitable\r\n * for production: tokens are lost on restart, state isn't shared across\r\n * workers. Ship a Redis / KV / DB-backed store for real deployments.\r\n */\r\nexport class InMemoryTokenStore implements TokenStore {\r\n private store = new Map<\r\n string,\r\n { value: string; expiresAt: number | null }\r\n >()\r\n\r\n get(key: string): Promise<string | null> {\r\n const entry = this.store.get(key)\r\n if (!entry) return Promise.resolve(null)\r\n if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {\r\n this.store.delete(key)\r\n return Promise.resolve(null)\r\n }\r\n return Promise.resolve(entry.value)\r\n }\r\n\r\n set(key: string, value: string, ttlSeconds?: number): Promise<void> {\r\n // ttlSeconds === 0 means \"already expired\", NOT \"no expiry\". Only an\r\n // omitted ttl (undefined) stores without expiry.\r\n const expiresAt =\r\n ttlSeconds != null ? Date.now() + ttlSeconds * 1000 : null\r\n this.store.set(key, { value, expiresAt })\r\n return Promise.resolve()\r\n }\r\n\r\n delete(key: string): Promise<void> {\r\n this.store.delete(key)\r\n return Promise.resolve()\r\n }\r\n}\r\n\r\nconst OAUTH_STATE_TTL_SECONDS = 600\r\n\r\nconst tokensKey = (userId: string, provider: string) =>\r\n `upup:tokens:${userId}:${provider}`\r\nconst oauthStateKey = (state: string) => `upup:oauth-state:${state}`\r\n\r\nexport async function getTokens(\r\n store: TokenStore,\r\n userId: string,\r\n provider: string,\r\n): Promise<DriveTokens | null> {\r\n const raw = await store.get(tokensKey(userId, provider))\r\n if (!raw) return null\r\n try {\r\n return JSON.parse(raw) as DriveTokens\r\n } catch {\r\n // upup-catch: a corrupt/legacy token blob is treated as absent — the\r\n // caller re-authenticates rather than crashing on bad stored JSON.\r\n return null\r\n }\r\n}\r\n\r\nexport async function setTokens(\r\n store: TokenStore,\r\n userId: string,\r\n provider: string,\r\n tokens: DriveTokens,\r\n): Promise<void> {\r\n // Refresh-aware TTL (audit S8): when a refresh token exists the blob must\r\n // outlive the short-lived access token so it can be refreshed, so store it\r\n // without expiry. Only a dead, non-refreshable access token self-evicts.\r\n let ttlSeconds: number | undefined\r\n if (!tokens.refreshToken && tokens.expiresAt) {\r\n ttlSeconds = Math.max(\r\n 0,\r\n Math.ceil((tokens.expiresAt - Date.now()) / 1000),\r\n )\r\n }\r\n await store.set(\r\n tokensKey(userId, provider),\r\n JSON.stringify(tokens),\r\n ttlSeconds,\r\n )\r\n}\r\n\r\nexport async function deleteTokens(\r\n store: TokenStore,\r\n userId: string,\r\n provider: string,\r\n): Promise<void> {\r\n await store.delete(tokensKey(userId, provider))\r\n}\r\n\r\nexport async function saveOAuthState(\r\n store: TokenStore,\r\n state: string,\r\n payload: OAuthState,\r\n): Promise<void> {\r\n await store.set(\r\n oauthStateKey(state),\r\n JSON.stringify(payload),\r\n OAUTH_STATE_TTL_SECONDS,\r\n )\r\n}\r\n\r\nexport async function consumeOAuthState(\r\n store: TokenStore,\r\n state: string,\r\n): Promise<OAuthState | null> {\r\n const raw = await store.get(oauthStateKey(state))\r\n if (!raw) return null\r\n await store.delete(oauthStateKey(state))\r\n try {\r\n return JSON.parse(raw) as OAuthState\r\n } catch {\r\n // upup-catch: a corrupt oauth-state blob is treated as absent — the OAuth\r\n // flow restarts rather than crashing on bad stored JSON.\r\n return null\r\n }\r\n}\r\n\r\nexport function generateOAuthState(): string {\r\n const bytes = new Uint8Array(32)\r\n crypto.getRandomValues(bytes)\r\n return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')\r\n}\r\n\r\nexport const DEFAULT_USER_ID = 'default'\r\n\r\n/**\r\n * Resolve the authenticated userId for this request, honouring the\r\n * consumer's getUserId hook. Returns null if the hook says unauthenticated.\r\n */\r\nexport async function resolveUserId(\r\n config: UpupServerConfig,\r\n req: Request,\r\n): Promise<string | null> {\r\n if (config.getUserId) return config.getUserId(req)\r\n return DEFAULT_USER_ID\r\n}\r\n","// packages/server/src/key.ts\r\nimport type { KeyStrategyContext } from './config'\r\n\r\n/**\r\n * Make a user-supplied filename safe to embed in an object key: drop directory\r\n * separators and anything outside [A-Za-z0-9._-], collapse runs to '_', strip\r\n * leading dots/underscores (no '.'/'..'), bound length. Never empty.\r\n */\r\nexport function sanitizeFilename(name: string): string {\r\n const cleaned = name\r\n .replace(/[^A-Za-z0-9._-]+/g, '_')\r\n .replace(/^[._]+/, '')\r\n .slice(0, 128)\r\n return cleaned || 'file'\r\n}\r\n\r\n/**\r\n * Default object-key layout: `<userId|anon>/<uuid>/<sanitized-filename>`. The\r\n * leading owner segment namespaces every upload so one caller can never address\r\n * or overwrite another's object; the uuid guarantees uniqueness even anonymously.\r\n */\r\nexport function defaultKeyStrategy(ctx: KeyStrategyContext): string {\r\n const owner = ctx.userId ?? 'anon'\r\n return `${owner}/${crypto.randomUUID()}/${sanitizeFilename(ctx.fileName)}`\r\n}\r\n","// packages/server/src/oauth.ts\r\n//\r\n// The cloud-drive OAuth flow in isolation (F-101/F-505): provider identity +\r\n// metadata, the redirect/callback handlers, the success-page builder, returnTo\r\n// validation, and the access-token refresh. Extracted verbatim from handler.ts.\r\n// The provider identity (VALID_PROVIDERS/OAuthProvider/isValidProvider) lives\r\n// here — auth is where \"provider\" originates — and is imported by the two drive\r\n// modules. This module imports NOTHING from the drive modules (acyclic DAG).\r\n\r\nimport { UpupErrorCode } from '@useupup/core'\r\nimport type { UpupServerConfig, DriveTokens } from './config'\r\nimport {\r\n generateOAuthState,\r\n saveOAuthState,\r\n consumeOAuthState,\r\n setTokens,\r\n deleteTokens,\r\n resolveUserId,\r\n} from './tokenStore'\r\nimport { reportServerError, toSafeError } from './observability'\r\nimport { type Responder } from './respond'\r\n\r\nexport const VALID_PROVIDERS = [\r\n 'google-drive',\r\n 'one-drive',\r\n 'dropbox',\r\n 'box',\r\n] as const\r\nexport type OAuthProvider = (typeof VALID_PROVIDERS)[number]\r\n\r\nexport function isValidProvider(p: string): p is OAuthProvider {\r\n return (VALID_PROVIDERS as readonly string[]).includes(p)\r\n}\r\n\r\ntype ProviderOAuthMeta = {\r\n authUrl: string\r\n tokenUrl: string\r\n scope: string\r\n clientId: string\r\n clientSecret: string\r\n extra?: Record<string, string>\r\n}\r\n\r\nfunction getProviderMeta(\r\n config: UpupServerConfig,\r\n provider: OAuthProvider,\r\n): ProviderOAuthMeta | { error: string; status: number } {\r\n const providers = config.providers\r\n if (!providers)\r\n return { error: 'No OAuth providers configured', status: 500 }\r\n\r\n switch (provider) {\r\n case 'google-drive': {\r\n const gc = providers.googleDrive\r\n if (!gc)\r\n return { error: 'Google Drive not configured', status: 400 }\r\n return {\r\n authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',\r\n tokenUrl: 'https://oauth2.googleapis.com/token',\r\n scope: 'https://www.googleapis.com/auth/drive.readonly',\r\n clientId: gc.clientId,\r\n clientSecret: gc.clientSecret,\r\n extra: { access_type: 'offline', prompt: 'consent' },\r\n }\r\n }\r\n case 'one-drive': {\r\n const oc = providers.oneDrive\r\n if (!oc) return { error: 'OneDrive not configured', status: 400 }\r\n const tenant = oc.tenantId ?? 'common'\r\n return {\r\n authUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,\r\n tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,\r\n scope: 'Files.Read.All offline_access',\r\n clientId: oc.clientId,\r\n clientSecret: oc.clientSecret,\r\n }\r\n }\r\n case 'dropbox': {\r\n const dc = providers.dropbox\r\n if (!dc) return { error: 'Dropbox not configured', status: 400 }\r\n return {\r\n authUrl: 'https://www.dropbox.com/oauth2/authorize',\r\n tokenUrl: 'https://api.dropboxapi.com/oauth2/token',\r\n scope: 'files.content.read files.metadata.read',\r\n clientId: dc.appKey,\r\n clientSecret: dc.appSecret,\r\n extra: { token_access_type: 'online' },\r\n }\r\n }\r\n case 'box': {\r\n const bc = providers.box\r\n if (!bc) return { error: 'Box not configured', status: 400 }\r\n return {\r\n authUrl: 'https://account.box.com/api/oauth2/authorize',\r\n tokenUrl: 'https://api.box.com/oauth2/token',\r\n scope: 'root_readonly',\r\n clientId: bc.clientId,\r\n clientSecret: bc.clientSecret,\r\n }\r\n }\r\n }\r\n}\r\n\r\nfunction callbackUrlFor(req: Request, provider: string): string {\r\n const url = new URL(req.url)\r\n // Strip the route suffix so this is idempotent across BOTH the auth request\r\n // (/auth/:provider) and the callback (/auth/:provider/cb) — otherwise the\r\n // token-exchange redirect_uri doubles the path and Google rejects it with\r\n // redirect_uri_mismatch. Mirrors the router's own /auth match (line ~128).\r\n // The optional trailing slash mirrors the router's slash-stripped matching\r\n // (Next trailingSlash:true 308s /auth/x to /auth/x/) — without it the\r\n // stray slash survives into base and the derived redirect_uri.\r\n const base = `${url.origin}${url.pathname.replace(/\\/auth\\/[\\w-]+(?:\\/cb)?\\/?$/, '')}`\r\n return `${base}/auth/${provider}/cb`\r\n}\r\n\r\nexport async function handleOAuthRedirect(\r\n req: Request,\r\n config: UpupServerConfig,\r\n provider: string,\r\n res: Responder,\r\n): Promise<Response> {\r\n if (!isValidProvider(provider)) {\r\n return res.json({ error: `Unknown provider: ${provider}` }, 400)\r\n }\r\n\r\n if (!config.tokenStore) {\r\n return res.json(\r\n { error: 'tokenStore is required for OAuth flows' },\r\n 500,\r\n )\r\n }\r\n\r\n const userId = await resolveUserId(config, req)\r\n if (!userId) return res.json({ error: 'Unauthenticated' }, 401)\r\n\r\n const meta = getProviderMeta(config, provider)\r\n if ('error' in meta) return res.json({ error: meta.error }, meta.status)\r\n\r\n const state = generateOAuthState()\r\n const returnTo = new URL(req.url).searchParams.get('returnTo') ?? undefined\r\n await saveOAuthState(config.tokenStore, state, {\r\n userId,\r\n provider,\r\n returnTo,\r\n })\r\n\r\n const params = new URLSearchParams({\r\n client_id: meta.clientId,\r\n redirect_uri: callbackUrlFor(req, provider),\r\n response_type: 'code',\r\n scope: meta.scope,\r\n state,\r\n ...(meta.extra ?? {}),\r\n })\r\n\r\n return res.redirect(`${meta.authUrl}?${params.toString()}`)\r\n}\r\n\r\nexport async function handleOAuthCallback(\r\n req: Request,\r\n config: UpupServerConfig,\r\n provider: string,\r\n res: Responder,\r\n): Promise<Response> {\r\n if (!isValidProvider(provider)) {\r\n return res.json({ error: `Unknown provider: ${provider}` }, 400)\r\n }\r\n if (!config.tokenStore) {\r\n return res.json({ error: 'tokenStore is required' }, 500)\r\n }\r\n\r\n const url = new URL(req.url)\r\n const code = url.searchParams.get('code')\r\n const state = url.searchParams.get('state')\r\n const error = url.searchParams.get('error')\r\n\r\n if (error) return res.json({ error: `OAuth error: ${error}` }, 400)\r\n if (!code || !state)\r\n return res.json({ error: 'Missing code or state' }, 400)\r\n\r\n const stateData = await consumeOAuthState(config.tokenStore, state)\r\n if (!stateData || stateData.provider !== provider) {\r\n return res.json({ error: 'Invalid or expired state' }, 400)\r\n }\r\n\r\n const meta = getProviderMeta(config, provider)\r\n if ('error' in meta) return res.json({ error: meta.error }, meta.status)\r\n\r\n const tokenRes = await fetch(meta.tokenUrl, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({\r\n code,\r\n client_id: meta.clientId,\r\n client_secret: meta.clientSecret,\r\n redirect_uri: callbackUrlFor(req, provider),\r\n grant_type: 'authorization_code',\r\n }),\r\n })\r\n\r\n if (!tokenRes.ok) {\r\n const body = await tokenRes.text()\r\n reportServerError(config.onError, {\r\n route: `auth/${provider}/cb`,\r\n method: req.method,\r\n status: 502,\r\n code: UpupErrorCode.AUTH_PROVIDER_ERROR,\r\n message: 'Token exchange failed',\r\n error: toSafeError(new Error(body.slice(0, 500))),\r\n })\r\n return res.json(\r\n {\r\n error: 'Token exchange failed',\r\n code: UpupErrorCode.AUTH_PROVIDER_ERROR,\r\n },\r\n 502,\r\n )\r\n }\r\n\r\n const payload = (await tokenRes.json()) as {\r\n access_token: string\r\n expires_in?: number\r\n refresh_token?: string\r\n scope?: string\r\n token_type?: string\r\n }\r\n\r\n const tokens: DriveTokens = {\r\n accessToken: payload.access_token,\r\n expiresAt: payload.expires_in\r\n ? Date.now() + payload.expires_in * 1000\r\n : undefined,\r\n scope: payload.scope,\r\n tokenType: payload.token_type,\r\n refreshToken: payload.refresh_token,\r\n }\r\n await setTokens(config.tokenStore, stateData.userId, provider, tokens)\r\n\r\n const validatedReturn = validateReturnTo(\r\n stateData.returnTo,\r\n req,\r\n config.cors,\r\n )\r\n const targetOrigins = concreteAllowedOrigins(config.cors)\r\n return res.html(\r\n buildOAuthSuccessPage(provider, {\r\n targetOrigins,\r\n ...(validatedReturn !== undefined\r\n ? { returnTo: validatedReturn }\r\n : {}),\r\n }),\r\n )\r\n}\r\n\r\n/** Validate an OAuth returnTo against same-origin + the CORS allowlist (audit S7).\r\n * Returns the resolved absolute URL string if allowed, else undefined.\r\n * A wildcard '*' in allowedOrigins does NOT authorize an arbitrary cross-origin returnTo.\r\n * Proxy caveat: a RELATIVE returnTo resolves against `new URL(req.url).origin` and is\r\n * returned ABSOLUTE. Behind an origin-rewriting reverse proxy (internal req.url origin ≠\r\n * public origin) it becomes absolute to the INTERNAL origin — this fails closed (never an\r\n * open redirect); to keep relative returnTo working there, add the public origin to\r\n * cors.allowedOrigins. */\r\nexport function validateReturnTo(\r\n returnTo: string | undefined,\r\n req: Request,\r\n cors: { allowedOrigins: string[] } | undefined,\r\n): string | undefined {\r\n if (!returnTo) return undefined\r\n const serverOrigin = new URL(req.url).origin\r\n let resolved: URL\r\n try {\r\n resolved = new URL(returnTo, serverOrigin) // relative returnTo resolves to same-origin\r\n } catch {\r\n // upup-catch: a malformed returnTo is rejected (fail-closed) so it can\r\n // never become an open redirect — absence of a valid URL means \"deny\".\r\n return undefined\r\n }\r\n if (resolved.origin === serverOrigin) return resolved.toString()\r\n const concrete = (cors?.allowedOrigins ?? []).filter(o => o !== '*')\r\n if (concrete.includes(resolved.origin)) return resolved.toString()\r\n return undefined\r\n}\r\n\r\n/** Concrete (non-wildcard) origins to target postMessage at (audit S7). */\r\nexport function concreteAllowedOrigins(\r\n cors: { allowedOrigins: string[] } | undefined,\r\n): string[] {\r\n return (cors?.allowedOrigins ?? []).filter(o => o !== '*')\r\n}\r\n\r\nexport function buildOAuthSuccessPage(\r\n provider: string,\r\n opts: { returnTo?: string; targetOrigins: string[] },\r\n): string {\r\n const safeProvider = provider.replace(/[^a-z0-9-]/gi, '')\r\n const { returnTo, targetOrigins } = opts\r\n\r\n let postMessageScript: string\r\n if (targetOrigins.length > 0) {\r\n postMessageScript = targetOrigins\r\n .map(\r\n origin =>\r\n `window.opener.postMessage({ type: 'upup:oauth-success', provider: ${JSON.stringify(safeProvider)} }, ${JSON.stringify(origin)});`,\r\n )\r\n .join('\\n ')\r\n } else {\r\n // '*' is acceptable here because the payload is token-free ({type, provider}, no secret)\r\n postMessageScript = `window.opener.postMessage({ type: 'upup:oauth-success', provider: ${JSON.stringify(safeProvider)} }, '*' /* token-free payload */);`\r\n }\r\n\r\n const elseBody = returnTo\r\n ? `window.location.replace(${JSON.stringify(returnTo)});`\r\n : `document.body.textContent = 'Connected to ' + ${JSON.stringify(safeProvider)} + '. You may close this window.';`\r\n\r\n return `<!doctype html>\r\n<html><head><title>Connected</title></head><body>\r\n<script>\r\n try {\r\n if (window.opener) {\r\n ${postMessageScript}\r\n window.close();\r\n } else if (${JSON.stringify(returnTo ?? '')}) {\r\n ${elseBody}\r\n } else {\r\n document.body.textContent = 'Connected to ' + ${JSON.stringify(safeProvider)} + '. You may close this window.';\r\n }\r\n } catch (e) {\r\n document.body.textContent = 'Connected. You may close this window.';\r\n }\r\n</script>\r\n</body></html>`\r\n}\r\n\r\nexport async function refreshAccessToken(\r\n config: UpupServerConfig,\r\n provider: OAuthProvider,\r\n userId: string,\r\n tokens: DriveTokens,\r\n): Promise<DriveTokens | null> {\r\n if (!tokens.refreshToken || !config.tokenStore) return null\r\n const meta = getProviderMeta(config, provider)\r\n if ('error' in meta) return null\r\n const res = await fetch(meta.tokenUrl, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\r\n body: new URLSearchParams({\r\n grant_type: 'refresh_token',\r\n refresh_token: tokens.refreshToken,\r\n client_id: meta.clientId,\r\n client_secret: meta.clientSecret,\r\n }),\r\n })\r\n if (!res.ok) {\r\n // Refresh token is dead/revoked -> force a clean re-auth.\r\n const body = await res.text().catch(() => '')\r\n reportServerError(config.onError, {\r\n route: `auth/${provider}/refresh`,\r\n method: 'POST',\r\n status: res.status,\r\n code: UpupErrorCode.AUTH_EXPIRED,\r\n message: 'Drive token refresh failed',\r\n error: toSafeError(new Error(body.slice(0, 500) || res.statusText)),\r\n })\r\n await deleteTokens(config.tokenStore, userId, provider)\r\n return null\r\n }\r\n const payload = (await res.json()) as {\r\n access_token: string\r\n expires_in?: number\r\n refresh_token?: string\r\n scope?: string\r\n token_type?: string\r\n }\r\n const next: DriveTokens = {\r\n accessToken: payload.access_token,\r\n expiresAt: payload.expires_in\r\n ? Date.now() + payload.expires_in * 1000\r\n : undefined,\r\n scope: payload.scope ?? tokens.scope,\r\n tokenType: payload.token_type ?? tokens.tokenType,\r\n // Some providers omit refresh_token on refresh -> keep the existing one.\r\n refreshToken: payload.refresh_token ?? tokens.refreshToken,\r\n }\r\n await setTokens(config.tokenStore, userId, provider, next)\r\n return next\r\n}\r\n","// packages/server/src/drive-routes.ts\r\n//\r\n// The authenticated cloud-drive HTTP routes (F-101/F-505): list a provider's\r\n// files, and transfer a chosen file server-side into S3. Both resolve/refresh/\r\n// reauth the caller's per-user drive tokens, then delegate the actual API calls\r\n// to ./drive-clients. Extracted verbatim from handler.ts. The dynamic\r\n// `import('./transfer')` keeps its path (same src/ dir). No behavior change.\r\n\r\nimport { UpupErrorCode, UpupNetworkError } from '@useupup/core'\r\nimport type { UpupServerConfig } from './config'\r\nimport { getTokens, deleteTokens, resolveUserId } from './tokenStore'\r\nimport { isValidProvider, refreshAccessToken } from './oauth'\r\nimport { getDriveClient } from './drive-clients'\r\nimport { matchesAllowedType, runPostCompletionHooks } from './upload-routes'\r\nimport { type Responder } from './respond'\r\nimport { resolveStorage } from './resolve-storage'\r\nimport type { UpupClientMetadata } from './config'\r\n\r\nexport async function handleListFiles(\r\n req: Request,\r\n config: UpupServerConfig,\r\n provider: string,\r\n res: Responder,\r\n): Promise<Response> {\r\n if (!isValidProvider(provider)) {\r\n return res.json({ error: `Unknown provider: ${provider}` }, 400)\r\n }\r\n if (!config.tokenStore)\r\n return res.json({ error: 'tokenStore is required' }, 500)\r\n\r\n const userId = await resolveUserId(config, req)\r\n if (!userId) return res.json({ error: 'Unauthenticated' }, 401)\r\n\r\n let tokens = await getTokens(config.tokenStore, userId, provider)\r\n if (!tokens) {\r\n return res.json({ reauth: true, provider }, 401)\r\n }\r\n if (\r\n tokens.refreshToken &&\r\n tokens.expiresAt &&\r\n Date.now() > tokens.expiresAt - 30_000\r\n ) {\r\n const refreshed = await refreshAccessToken(\r\n config,\r\n provider,\r\n userId,\r\n tokens,\r\n )\r\n if (!refreshed) {\r\n return res.json({ reauth: true, provider }, 401)\r\n }\r\n tokens = refreshed\r\n }\r\n\r\n const url = new URL(req.url)\r\n const folderId = url.searchParams.get('folderId') ?? undefined\r\n const search = url.searchParams.get('search') ?? undefined\r\n\r\n try {\r\n const files = await getDriveClient(provider).listFiles(\r\n tokens.accessToken,\r\n { folderId, search },\r\n )\r\n return res.json({ provider, files }, 200)\r\n } catch (err) {\r\n if (err instanceof UpupNetworkError && err.status === 401) {\r\n await deleteTokens(config.tokenStore, userId, provider)\r\n return res.json({ reauth: true, provider }, 401)\r\n }\r\n return res.fail(\r\n `files/${provider}`,\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Drive request failed',\r\n err,\r\n )\r\n }\r\n}\r\n\r\nexport async function handleFileTransfer(\r\n req: Request,\r\n config: UpupServerConfig,\r\n provider: string,\r\n res: Responder,\r\n): Promise<Response> {\r\n if (!isValidProvider(provider)) {\r\n return res.json({ error: `Unknown provider: ${provider}` }, 400)\r\n }\r\n if (!config.tokenStore)\r\n return res.json({ error: 'tokenStore is required' }, 500)\r\n\r\n const userId = await resolveUserId(config, req)\r\n if (!userId) return res.json({ error: 'Unauthenticated' }, 401)\r\n\r\n let tokens = await getTokens(config.tokenStore, userId, provider)\r\n if (!tokens) return res.json({ reauth: true, provider }, 401)\r\n if (\r\n tokens.refreshToken &&\r\n tokens.expiresAt &&\r\n Date.now() > tokens.expiresAt - 30_000\r\n ) {\r\n const refreshed = await refreshAccessToken(\r\n config,\r\n provider,\r\n userId,\r\n tokens,\r\n )\r\n if (!refreshed) {\r\n return res.json({ reauth: true, provider }, 401)\r\n }\r\n tokens = refreshed\r\n }\r\n\r\n let body: {\r\n fileId: string\r\n fileName?: string\r\n size?: number\r\n mimeType?: string\r\n metadata?: UpupClientMetadata\r\n }\r\n try {\r\n body = (await req.json()) as typeof body\r\n } catch {\r\n // upup-catch: a malformed JSON body is surfaced to the client as 400 —\r\n // returned, not swallowed.\r\n return res.json({ error: 'Invalid JSON body' }, 400)\r\n }\r\n if (!body.fileId) return res.json({ error: 'Missing fileId' }, 400)\r\n\r\n // Fast client-declared-size reject (cheap early-out before we open the\r\n // drive stream); the AUTHORITATIVE cap is enforced against actual streamed\r\n // bytes inside transferDriveFileToS3 via `maxBytes` below (F-743).\r\n if (\r\n config.maxFileSize &&\r\n typeof body.size === 'number' &&\r\n body.size > config.maxFileSize\r\n ) {\r\n return res.json({ error: 'File too large' }, 413)\r\n }\r\n // One shared allowed-types policy with the upload path (F-743): honour the\r\n // `image/*` wildcard, and treat an ABSENT mimeType as a non-match against a\r\n // non-empty allowlist (`?? ''`) rather than silently bypassing the check.\r\n if (!matchesAllowedType(body.mimeType ?? '', config.allowedTypes)) {\r\n return res.json({ error: 'File type not allowed' }, 415)\r\n }\r\n\r\n try {\r\n const { stream, size, fileName, mimeType } = await getDriveClient(\r\n provider,\r\n ).fetchFile(tokens.accessToken, body)\r\n // Resolved AFTER the drive reports the real name/type/size, so a\r\n // resolver can route on what is actually being transferred rather than\r\n // on the client's claim (#337).\r\n const storage = await resolveStorage(config, {\r\n req,\r\n phase: 'drive-transfer',\r\n userId,\r\n ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),\r\n fileName,\r\n contentType: mimeType,\r\n size,\r\n })\r\n const { transferDriveFileToS3 } = await import('./transfer')\r\n const result = await transferDriveFileToS3({\r\n stream,\r\n size,\r\n fileName,\r\n mimeType,\r\n storage,\r\n // Enforce maxFileSize against the ACTUAL streamed bytes, and route\r\n // an abort-cleanup failure through onError instead of swallowing it\r\n // (F-743 / F-744).\r\n maxBytes: config.maxFileSize,\r\n onError: config.onError,\r\n requestId: res.requestId,\r\n downloadUrlExpiresIn: config.downloadUrlExpiresIn,\r\n })\r\n // Post-commit: object durably in S3. A throwing onFileUploaded hook is\r\n // logged + swallowed by runPostCompletionHooks, never bubbling to the\r\n // catch below as a 500 (F-745).\r\n await runPostCompletionHooks(\r\n config,\r\n res,\r\n `files/${provider}/transfer`,\r\n req.method,\r\n async () => {\r\n if (config.hooks?.onFileUploaded)\r\n await config.hooks.onFileUploaded(result, req)\r\n },\r\n )\r\n return res.json({ provider, ...result }, 200)\r\n } catch (err) {\r\n if (err instanceof UpupNetworkError && err.status === 401) {\r\n await deleteTokens(config.tokenStore, userId, provider)\r\n return res.json({ reauth: true, provider }, 401)\r\n }\r\n return res.fail(\r\n `files/${provider}/transfer`,\r\n req.method,\r\n 500,\r\n UpupErrorCode.STORAGE_ERROR,\r\n 'Drive request failed',\r\n err,\r\n )\r\n }\r\n}\r\n","// packages/server/src/drive-clients.ts\r\n//\r\n// Per-provider cloud-drive API calls (list + fetch) behind a small internal\r\n// surface, plus the shared driveFetch (401→reauth signal) and the query-value\r\n// escaping helpers. Extracted verbatim from handler.ts (F-101/F-505). The two\r\n// parallel dispatch switches (listDriveFiles/fetchDriveFile) are collapsed into\r\n// a provider registry in C6 (F-407). Imports only the provider identity type\r\n// from ./oauth (acyclic DAG).\r\n\r\nimport { UpupNetworkError } from '@useupup/core'\r\nimport { type OAuthProvider } from './oauth'\r\n\r\nexport type DriveFile = {\r\n id: string\r\n name: string\r\n size?: number | undefined\r\n mimeType?: string | undefined\r\n thumbnailUrl?: string | undefined\r\n isFolder: boolean\r\n modifiedAt?: string | undefined\r\n}\r\n\r\n/** One provider's drive surface: list a folder/search, and fetch a file's bytes.\r\n * Replaces the two parallel per-provider dispatch switches (F-407) — adding a\r\n * provider is now one client-fn pair + one DRIVE_CLIENTS row. */\r\nexport type DriveClient = {\r\n listFiles(\r\n accessToken: string,\r\n opts: { folderId?: string | undefined; search?: string | undefined },\r\n ): Promise<DriveFile[]>\r\n fetchFile(\r\n accessToken: string,\r\n body: {\r\n fileId: string\r\n fileName?: string\r\n size?: number\r\n mimeType?: string\r\n },\r\n ): Promise<{\r\n stream: ReadableStream<Uint8Array>\r\n size: number\r\n fileName: string\r\n mimeType: string\r\n }>\r\n}\r\n\r\nconst DRIVE_CLIENTS: Record<OAuthProvider, DriveClient> = {\r\n 'google-drive': {\r\n listFiles: listGoogleDriveFiles,\r\n fetchFile: fetchGoogleDriveFile,\r\n },\r\n 'one-drive': { listFiles: listOneDriveFiles, fetchFile: fetchOneDriveFile },\r\n dropbox: { listFiles: listDropboxFiles, fetchFile: fetchDropboxFile },\r\n box: { listFiles: listBoxFiles, fetchFile: fetchBoxFile },\r\n}\r\n\r\nexport function getDriveClient(provider: OAuthProvider): DriveClient {\r\n return DRIVE_CLIENTS[provider]\r\n}\r\n\r\nasync function driveFetch(\r\n url: string,\r\n init: RequestInit = {},\r\n): Promise<Response> {\r\n const res = await fetch(url, init)\r\n if (res.status === 401) {\r\n // Typed 401 sentinel: UpupNetworkError already carries `.status`, so the\r\n // reauth branch in drive-routes can `instanceof`-check it instead of\r\n // duck-typing an ad-hoc untyped property (F-747).\r\n throw new UpupNetworkError('Drive API 401', 401)\r\n }\r\n if (!res.ok) {\r\n const text = await res.text().catch(() => '')\r\n throw new UpupNetworkError(\r\n `Drive API ${res.status}: ${text.slice(0, 200) || res.statusText}`,\r\n res.status,\r\n )\r\n }\r\n return res\r\n}\r\n\r\n/** Escape a value for use inside a Google Drive API query string literal (single-quoted).\r\n * Backslashes must be escaped before quotes to prevent query injection (audit S5).\r\n * Twin: scripts/drive-sandbox/seed.mjs escapeGDriveQueryValue — keep in sync. */\r\nexport function escapeDriveQueryValue(value: string): string {\r\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")\r\n}\r\n\r\n/**\r\n * Escape a user value for an OData string literal (Microsoft Graph). OData\r\n * escapes a single quote by doubling it. Applied before encodeURIComponent so\r\n * the doubled quote survives URL-decoding on Graph's side (defense-in-depth\r\n * parity with Google Drive's escapeDriveQueryValue).\r\n */\r\nexport function escapeODataSearchValue(value: string): string {\r\n return value.replace(/'/g, \"''\")\r\n}\r\n\r\n/**\r\n * Dropbox-API-Arg travels as an HTTP HEADER, and HTTP headers are latin-1: a\r\n * unicode filename (Dropbox file ids ARE path strings) is either rejected by\r\n * undici outright (code points > 0xFF) or sent mis-encoded as latin-1 bytes\r\n * that Dropbox cannot match to its UTF-8 path (a 409 path/not_found) — the\r\n * download breaks either way. Dropbox prescribes \"HTTP header safe JSON\":\r\n * escape every char >= 0x7F as \\uXXXX. (The list/search calls put their JSON in\r\n * the request BODY — UTF-8 is fine there.)\r\n * Twin: scripts/drive-sandbox/seed.mjs headerSafeJson — keep in sync.\r\n */\r\nexport function httpHeaderSafeJson(value: unknown): string {\r\n return JSON.stringify(value).replace(\r\n /[\\u007f-\\uffff]/g,\r\n c => '\\\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'),\r\n )\r\n}\r\n\r\nasync function listGoogleDriveFiles(\r\n accessToken: string,\r\n opts: { folderId?: string; search?: string },\r\n): Promise<DriveFile[]> {\r\n const parent = opts.folderId ?? 'root'\r\n const q = opts.search\r\n ? `name contains '${escapeDriveQueryValue(opts.search)}' and trashed = false`\r\n : `'${parent}' in parents and trashed = false`\r\n const params = new URLSearchParams({\r\n q,\r\n fields: 'files(id,name,size,mimeType,thumbnailLink,modifiedTime,iconLink)',\r\n pageSize: '200',\r\n })\r\n const res = await driveFetch(\r\n `https://www.googleapis.com/drive/v3/files?${params.toString()}`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n const data = (await res.json()) as {\r\n files: Array<{\r\n id: string\r\n name: string\r\n size?: string\r\n mimeType: string\r\n thumbnailLink?: string\r\n modifiedTime?: string\r\n }>\r\n }\r\n return data.files.map(f => ({\r\n id: f.id,\r\n name: f.name,\r\n size: f.size ? Number(f.size) : undefined,\r\n mimeType: f.mimeType,\r\n thumbnailUrl: f.thumbnailLink,\r\n isFolder: f.mimeType === 'application/vnd.google-apps.folder',\r\n modifiedAt: f.modifiedTime,\r\n }))\r\n}\r\n\r\nasync function fetchGoogleDriveFile(\r\n accessToken: string,\r\n body: {\r\n fileId: string\r\n fileName?: string\r\n size?: number\r\n mimeType?: string\r\n },\r\n) {\r\n const metaRes = await driveFetch(\r\n `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(\r\n body.fileId,\r\n )}?fields=name,size,mimeType`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n const meta = (await metaRes.json()) as {\r\n name: string\r\n size?: string\r\n mimeType?: string\r\n }\r\n const dlRes = await driveFetch(\r\n `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(\r\n body.fileId,\r\n )}?alt=media`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n if (!dlRes.body) throw new UpupNetworkError('Empty download body')\r\n return {\r\n stream: dlRes.body,\r\n size: Number(meta.size ?? body.size ?? 0),\r\n fileName: body.fileName ?? meta.name,\r\n mimeType: body.mimeType ?? meta.mimeType ?? 'application/octet-stream',\r\n }\r\n}\r\n\r\nasync function listOneDriveFiles(\r\n accessToken: string,\r\n opts: { folderId?: string; search?: string },\r\n): Promise<DriveFile[]> {\r\n const path = opts.search\r\n ? `/me/drive/root/search(q='${encodeURIComponent(escapeODataSearchValue(opts.search))}')`\r\n : opts.folderId\r\n ? `/me/drive/items/${encodeURIComponent(opts.folderId)}/children`\r\n : '/me/drive/root/children'\r\n const res = await driveFetch(`https://graph.microsoft.com/v1.0${path}`, {\r\n headers: { Authorization: `Bearer ${accessToken}` },\r\n })\r\n const data = (await res.json()) as {\r\n value: Array<{\r\n id: string\r\n name: string\r\n size?: number\r\n file?: { mimeType?: string }\r\n folder?: unknown\r\n lastModifiedDateTime?: string\r\n }>\r\n }\r\n return data.value.map(f => ({\r\n id: f.id,\r\n name: f.name,\r\n size: f.size,\r\n mimeType: f.file?.mimeType,\r\n isFolder: !!f.folder,\r\n modifiedAt: f.lastModifiedDateTime,\r\n }))\r\n}\r\n\r\nasync function fetchOneDriveFile(\r\n accessToken: string,\r\n body: {\r\n fileId: string\r\n fileName?: string\r\n size?: number\r\n mimeType?: string\r\n },\r\n) {\r\n const metaRes = await driveFetch(\r\n `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(\r\n body.fileId,\r\n )}`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n const meta = (await metaRes.json()) as {\r\n name: string\r\n size?: number\r\n file?: { mimeType?: string }\r\n }\r\n const dlRes = await driveFetch(\r\n `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(\r\n body.fileId,\r\n )}/content`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n if (!dlRes.body) throw new UpupNetworkError('Empty download body')\r\n return {\r\n stream: dlRes.body,\r\n size: meta.size ?? body.size ?? 0,\r\n fileName: body.fileName ?? meta.name,\r\n mimeType:\r\n body.mimeType ?? meta.file?.mimeType ?? 'application/octet-stream',\r\n }\r\n}\r\n\r\nasync function listDropboxFiles(\r\n accessToken: string,\r\n opts: { folderId?: string; search?: string },\r\n): Promise<DriveFile[]> {\r\n const endpoint = opts.search\r\n ? 'https://api.dropboxapi.com/2/files/search_v2'\r\n : 'https://api.dropboxapi.com/2/files/list_folder'\r\n const body = opts.search\r\n ? { query: opts.search }\r\n : { path: opts.folderId ?? '', recursive: false }\r\n const res = await driveFetch(endpoint, {\r\n method: 'POST',\r\n headers: {\r\n Authorization: `Bearer ${accessToken}`,\r\n 'Content-Type': 'application/json',\r\n },\r\n body: JSON.stringify(body),\r\n })\r\n const data = (await res.json()) as\r\n | {\r\n entries: Array<{\r\n '.tag': 'file' | 'folder' | 'deleted'\r\n id: string\r\n name: string\r\n path_lower?: string\r\n size?: number\r\n server_modified?: string\r\n }>\r\n }\r\n | {\r\n matches: Array<{\r\n metadata: {\r\n metadata: {\r\n '.tag': 'file' | 'folder'\r\n id: string\r\n name: string\r\n path_lower?: string\r\n size?: number\r\n }\r\n }\r\n }>\r\n }\r\n const entries =\r\n 'entries' in data\r\n ? data.entries\r\n : data.matches.map(m => m.metadata.metadata)\r\n return entries\r\n .filter(e => e['.tag'] !== 'deleted')\r\n .map(e => ({\r\n id: e.path_lower ?? e.id,\r\n name: e.name,\r\n size: 'size' in e ? e.size : undefined,\r\n isFolder: e['.tag'] === 'folder',\r\n modifiedAt: 'server_modified' in e ? e.server_modified : undefined,\r\n }))\r\n}\r\n\r\nasync function fetchDropboxFile(\r\n accessToken: string,\r\n body: {\r\n fileId: string\r\n fileName?: string\r\n size?: number\r\n mimeType?: string\r\n },\r\n) {\r\n const dlRes = await driveFetch(\r\n 'https://content.dropboxapi.com/2/files/download',\r\n {\r\n method: 'POST',\r\n headers: {\r\n Authorization: `Bearer ${accessToken}`,\r\n 'Dropbox-API-Arg': httpHeaderSafeJson({ path: body.fileId }),\r\n },\r\n },\r\n )\r\n if (!dlRes.body) throw new UpupNetworkError('Empty download body')\r\n const apiResult = dlRes.headers.get('Dropbox-API-Result')\r\n let name = body.fileName ?? 'download'\r\n let size = body.size ?? 0\r\n if (apiResult) {\r\n try {\r\n const parsed = JSON.parse(apiResult) as {\r\n name?: string\r\n size?: number\r\n }\r\n name = body.fileName ?? parsed.name ?? name\r\n size = parsed.size ?? size\r\n } catch {\r\n // upup-catch: the Dropbox-API-Result header is best-effort metadata; a\r\n // malformed value just falls back to the request body / defaults.\r\n }\r\n }\r\n return {\r\n stream: dlRes.body,\r\n size,\r\n fileName: name,\r\n mimeType:\r\n body.mimeType ??\r\n dlRes.headers.get('Content-Type') ??\r\n 'application/octet-stream',\r\n }\r\n}\r\n\r\nasync function listBoxFiles(\r\n accessToken: string,\r\n opts: { folderId?: string; search?: string },\r\n): Promise<DriveFile[]> {\r\n if (opts.search) {\r\n const params = new URLSearchParams({\r\n query: opts.search,\r\n limit: '200',\r\n })\r\n const res = await driveFetch(\r\n `https://api.box.com/2.0/search?${params.toString()}`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n const data = (await res.json()) as {\r\n entries: Array<{\r\n type: 'file' | 'folder'\r\n id: string\r\n name: string\r\n size?: number\r\n modified_at?: string\r\n }>\r\n }\r\n return data.entries.map(e => ({\r\n id: e.id,\r\n name: e.name,\r\n size: e.size,\r\n isFolder: e.type === 'folder',\r\n modifiedAt: e.modified_at,\r\n }))\r\n }\r\n const folderId = opts.folderId ?? '0'\r\n const res = await driveFetch(\r\n `https://api.box.com/2.0/folders/${encodeURIComponent(folderId)}/items?limit=200&fields=id,name,size,type,modified_at`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n const data = (await res.json()) as {\r\n entries: Array<{\r\n type: 'file' | 'folder'\r\n id: string\r\n name: string\r\n size?: number\r\n modified_at?: string\r\n }>\r\n }\r\n return data.entries.map(e => ({\r\n id: e.id,\r\n name: e.name,\r\n size: e.size,\r\n isFolder: e.type === 'folder',\r\n modifiedAt: e.modified_at,\r\n }))\r\n}\r\n\r\nasync function fetchBoxFile(\r\n accessToken: string,\r\n body: {\r\n fileId: string\r\n fileName?: string\r\n size?: number\r\n mimeType?: string\r\n },\r\n) {\r\n const metaRes = await driveFetch(\r\n `https://api.box.com/2.0/files/${encodeURIComponent(body.fileId)}`,\r\n { headers: { Authorization: `Bearer ${accessToken}` } },\r\n )\r\n const meta = (await metaRes.json()) as { name: string; size?: number }\r\n const dlRes = await driveFetch(\r\n `https://api.box.com/2.0/files/${encodeURIComponent(body.fileId)}/content`,\r\n {\r\n headers: { Authorization: `Bearer ${accessToken}` },\r\n redirect: 'follow',\r\n },\r\n )\r\n if (!dlRes.body) throw new UpupNetworkError('Empty download body')\r\n return {\r\n stream: dlRes.body,\r\n size: meta.size ?? body.size ?? 0,\r\n fileName: body.fileName ?? meta.name,\r\n mimeType:\r\n body.mimeType ??\r\n dlRes.headers.get('Content-Type') ??\r\n 'application/octet-stream',\r\n }\r\n}\r\n","/**\r\n * The ONE Node-request -> Web-`Request` and Web-`Response` -> Node-response bridge\r\n * for @useupup/server's Node adapters (F-651). express.ts, fastify.ts, and\r\n * @useupup/next's pages-handler.ts each hand-rolled this conversion and drifted:\r\n * two copied ALL response headers (incl. content-length) and sent a `.text()`\r\n * string; one correctly skipped content-length and sent a lossless Buffer. This\r\n * module pins the correct behavior for every future Node adapter to import\r\n * rather than re-derive.\r\n *\r\n * hono.ts / next.ts (App Router) are web-native (they receive/return a Web\r\n * Request/Response directly) and do not need this bridge.\r\n */\r\n\r\nexport function nodeHeadersToWeb(\r\n h: Record<string, string | string[] | undefined>,\r\n): Headers {\r\n const headers = new Headers()\r\n for (const [k, v] of Object.entries(h)) {\r\n if (v === undefined) continue\r\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\r\n else headers.set(k, v)\r\n }\r\n return headers\r\n}\r\n\r\nexport function toWebRequest(input: {\r\n url: string\r\n method: string\r\n headers: Record<string, string | string[] | undefined>\r\n // RequestInit['body'] (not the bare BodyInit alias): this tsconfig's\r\n // lib:[\"ES2020\"] + @types/node resolve the composite RequestInit interface\r\n // globally (used elsewhere in this package, e.g. normalize-origin.ts) but\r\n // not every constituent type alias as its own global name.\r\n body?: RequestInit['body']\r\n}): Request {\r\n const hasBody = input.method !== 'GET' && input.method !== 'HEAD'\r\n return new Request(input.url, {\r\n method: input.method,\r\n headers: nodeHeadersToWeb(input.headers),\r\n ...(hasBody && input.body != null ? { body: input.body } : {}),\r\n })\r\n}\r\n\r\n/** Minimal Node-response sink. Express's res, Fastify's reply (via a thin\r\n * method-name adapter), and Next's NextApiResponse all satisfy this shape. */\r\nexport interface NodeResponseSink {\r\n status(code: number): void\r\n setHeader(name: string, value: string): void\r\n send(body: Buffer): void\r\n}\r\n\r\nexport async function writeWebResponse(\r\n sink: NodeResponseSink,\r\n webRes: Response,\r\n): Promise<void> {\r\n sink.status(webRes.status)\r\n webRes.headers.forEach((value, key) => {\r\n // Node recomputes content-length from the payload; copying it risks a mismatch.\r\n if (key.toLowerCase() === 'content-length') return\r\n sink.setHeader(key, value)\r\n })\r\n sink.send(Buffer.from(await webRes.arrayBuffer()))\r\n}\r\n"],"mappings":"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","names":["import_client_s3","import_core","import_client_s3","import_core","import_core","import_core","import_core","import_core","import_core","import_core","import_core","import_core","import_core","res","data","transferDriveFileToS3"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { U as UpupServerConfig } from './config-3b434689.cjs';
|
|
2
|
+
import '@useupup/core';
|
|
3
|
+
|
|
4
|
+
interface ExpressReq {
|
|
5
|
+
protocol: string;
|
|
6
|
+
get(name: string): string | undefined;
|
|
7
|
+
originalUrl: string;
|
|
8
|
+
method: string;
|
|
9
|
+
headers: Record<string, string | string[] | undefined>;
|
|
10
|
+
body: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface ExpressRes {
|
|
13
|
+
status(code: number): ExpressRes;
|
|
14
|
+
setHeader(name: string, value: string): ExpressRes;
|
|
15
|
+
send(body: string | Buffer): void;
|
|
16
|
+
}
|
|
17
|
+
declare function createUpupMiddleware(config: UpupServerConfig): (req: ExpressReq, res: ExpressRes, _next: () => void) => Promise<void>;
|
|
18
|
+
|
|
19
|
+
export { UpupServerConfig, createUpupMiddleware };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { U as UpupServerConfig } from './config-3b434689.js';
|
|
2
|
+
import '@useupup/core';
|
|
3
|
+
|
|
4
|
+
interface ExpressReq {
|
|
5
|
+
protocol: string;
|
|
6
|
+
get(name: string): string | undefined;
|
|
7
|
+
originalUrl: string;
|
|
8
|
+
method: string;
|
|
9
|
+
headers: Record<string, string | string[] | undefined>;
|
|
10
|
+
body: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface ExpressRes {
|
|
13
|
+
status(code: number): ExpressRes;
|
|
14
|
+
setHeader(name: string, value: string): ExpressRes;
|
|
15
|
+
send(body: string | Buffer): void;
|
|
16
|
+
}
|
|
17
|
+
declare function createUpupMiddleware(config: UpupServerConfig): (req: ExpressReq, res: ExpressRes, _next: () => void) => Promise<void>;
|
|
18
|
+
|
|
19
|
+
export { UpupServerConfig, createUpupMiddleware };
|