uploadthing
Version:
Learn more: [docs.uploadthing.com](https://docs.uploadthing.com)
900 lines (882 loc) • 45.7 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var node_stream = require('node:stream');
var Effect = require('effect/Effect');
var express = require('express');
var platform = require('@effect/platform');
var Config = require('effect/Config');
var Context = require('effect/Context');
var Match = require('effect/Match');
var Redacted = require('effect/Redacted');
var S = require('effect/Schema');
var shared = require('@uploadthing/shared');
var ConfigProvider = require('effect/ConfigProvider');
var Stream = require('effect/Stream');
var Layer = require('effect/Layer');
var Logger = require('effect/Logger');
var LogLevel = require('effect/LogLevel');
var Cause = require('effect/Cause');
var Data = require('effect/Data');
var Runtime = require('effect/Runtime');
var FiberRef = require('effect/FiberRef');
var ManagedRuntime = require('effect/ManagedRuntime');
var types_cjs = require('../internal/types.cjs');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return n;
}
var Effect__namespace = /*#__PURE__*/_interopNamespace(Effect);
var Config__namespace = /*#__PURE__*/_interopNamespace(Config);
var Context__namespace = /*#__PURE__*/_interopNamespace(Context);
var Match__namespace = /*#__PURE__*/_interopNamespace(Match);
var Redacted__namespace = /*#__PURE__*/_interopNamespace(Redacted);
var S__namespace = /*#__PURE__*/_interopNamespace(S);
var ConfigProvider__namespace = /*#__PURE__*/_interopNamespace(ConfigProvider);
var Stream__namespace = /*#__PURE__*/_interopNamespace(Stream);
var Layer__namespace = /*#__PURE__*/_interopNamespace(Layer);
var Logger__namespace = /*#__PURE__*/_interopNamespace(Logger);
var LogLevel__namespace = /*#__PURE__*/_interopNamespace(LogLevel);
var Cause__namespace = /*#__PURE__*/_interopNamespace(Cause);
var Data__namespace = /*#__PURE__*/_interopNamespace(Data);
var Runtime__namespace = /*#__PURE__*/_interopNamespace(Runtime);
var FiberRef__namespace = /*#__PURE__*/_interopNamespace(FiberRef);
var ManagedRuntime__namespace = /*#__PURE__*/_interopNamespace(ManagedRuntime);
var version = "7.4.4";
S__namespace.Literal(...shared.ValidContentDispositions);
S__namespace.Literal(...shared.ValidACLs);
/**
* Valid options for the `?actionType` query param
*/ const ActionType = S__namespace.Literal("upload");
/**
* Valid options for the `uploadthing-hook` header
* for requests coming from UT server
*/ const UploadThingHook = S__namespace.Literal("callback", "error");
/**
* =============================================================================
* =========================== Configuration ===================================
* =============================================================================
*/ const DecodeString = S__namespace.transform(S__namespace.Uint8ArrayFromSelf, S__namespace.String, {
decode: (data)=>new TextDecoder().decode(data),
encode: (data)=>new TextEncoder().encode(data)
});
const ParsedToken = S__namespace.Struct({
apiKey: S__namespace.Redacted(S__namespace.String.pipe(S__namespace.startsWith("sk_"))),
appId: S__namespace.String,
regions: S__namespace.NonEmptyArray(S__namespace.String),
ingestHost: S__namespace.String.pipe(S__namespace.optionalWith({
default: ()=>"ingest.uploadthing.com"
}))
});
const UploadThingToken = S__namespace.Uint8ArrayFromBase64.pipe(S__namespace.compose(DecodeString), S__namespace.compose(S__namespace.parseJson(ParsedToken)));
/**
* =============================================================================
* ======================== File Type Hierarchy ===============================
* =============================================================================
*/ /**
* Properties from the web File object, this is what the client sends when initiating an upload
*/ class FileUploadData extends S__namespace.Class("FileUploadData")({
name: S__namespace.String,
size: S__namespace.Number,
type: S__namespace.String,
lastModified: S__namespace.Number.pipe(S__namespace.optional)
}) {
}
/**
* `.middleware()` can add a customId to the incoming file data
*/ class FileUploadDataWithCustomId extends FileUploadData.extend("FileUploadDataWithCustomId")({
customId: S__namespace.NullOr(S__namespace.String)
}) {
}
/**
* When files are uploaded, we get back
* - a key
* - a direct URL for the file
* - an app-specific URL for the file (useful for scoping eg. for optimization allowed origins)
* - the hash (md5-hex) of the uploaded file's contents
*/ class UploadedFileData extends FileUploadDataWithCustomId.extend("UploadedFileData")({
key: S__namespace.String,
url: S__namespace.String,
appUrl: S__namespace.String,
fileHash: S__namespace.String
}) {
}
/**
* =============================================================================
* ======================== Server Response Schemas ============================
* =============================================================================
*/ class NewPresignedUrl extends S__namespace.Class("NewPresignedUrl")({
url: S__namespace.String,
key: S__namespace.String,
customId: S__namespace.NullOr(S__namespace.String),
name: S__namespace.String
}) {
}
class MetadataFetchStreamPart extends S__namespace.Class("MetadataFetchStreamPart")({
payload: S__namespace.String,
signature: S__namespace.String,
hook: UploadThingHook
}) {
}
class MetadataFetchResponse extends S__namespace.Class("MetadataFetchResponse")({
ok: S__namespace.Boolean
}) {
}
class CallbackResultResponse extends S__namespace.Class("CallbackResultResponse")({
ok: S__namespace.Boolean
}) {
}
/**
* =============================================================================
* ======================== Client Action Payloads ============================
* =============================================================================
*/ class UploadActionPayload extends S__namespace.Class("UploadActionPayload")({
files: S__namespace.Array(FileUploadData),
input: S__namespace.Unknown
}) {
}
/**
* Merge in `import.meta.env` to the built-in `process.env` provider
* Prefix keys with `UPLOADTHING_` so we can reference just the name.
* @example
* process.env.UPLOADTHING_TOKEN = "foo"
* Config.string("token"); // Config<"foo">
*/ const envProvider = ConfigProvider__namespace.fromEnv().pipe(ConfigProvider__namespace.orElse(()=>ConfigProvider__namespace.fromMap(new Map(Object.entries(shared.filterDefinedObjectValues(// fuck this I give up. import.meta is a mistake, someone else can fix it
undefined ?? {}))), {
pathDelim: "_"
})), ConfigProvider__namespace.nested("uploadthing"), ConfigProvider__namespace.constantCase);
/**
* Config provider that merges the options from the object
* and environment variables prefixed with `UPLOADTHING_`.
* @remarks Options take precedence over environment variables.
*/ const configProvider = (options)=>ConfigProvider__namespace.fromJson(options ?? {}).pipe(ConfigProvider__namespace.orElse(()=>envProvider));
const IsDevelopment = Config__namespace.boolean("isDev").pipe(Config__namespace.orElse(()=>Config__namespace.succeed(typeof process !== "undefined" ? process.env.NODE_ENV : undefined).pipe(Config__namespace.map((_)=>_ === "development"))), Config__namespace.withDefault(false));
const UTToken = S__namespace.Config("token", UploadThingToken).pipe(Effect__namespace.catchTags({
ConfigError: (e)=>new shared.UploadThingError({
code: e._op === "InvalidData" ? "INVALID_SERVER_CONFIG" : "MISSING_ENV",
message: e._op === "InvalidData" ? "Invalid token. A token is a base64 encoded JSON object matching { apiKey: string, appId: string, regions: string[] }." : "Missing token. Please set the `UPLOADTHING_TOKEN` environment variable or provide a token manually through config.",
cause: e
})
}));
Config__namespace.string("apiUrl").pipe(Config__namespace.withDefault("https://api.uploadthing.com"), Config__namespace.mapAttempt((_)=>new URL(_)), Config__namespace.map((url)=>url.href.replace(/\/$/, "")));
const IngestUrl = Effect__namespace.gen(function*() {
const { regions, ingestHost } = yield* UTToken;
const region = regions[0]; // Currently only support 1 region per app
return yield* Config__namespace.string("ingestUrl").pipe(Config__namespace.withDefault(`https://${region}.${ingestHost}`), Config__namespace.mapAttempt((_)=>new URL(_)), Config__namespace.map((url)=>url.href.replace(/\/$/, "")));
});
function defaultErrorFormatter(error) {
return {
message: error.message
};
}
function formatError(error, router) {
const errorFormatter = router[Object.keys(router)[0]]?.errorFormatter ?? defaultErrorFormatter;
return errorFormatter(error);
}
const handleJsonLineStream = (schema, onChunk)=>(stream)=>{
let buf = "";
return stream.pipe(Stream__namespace.decodeText(), Stream__namespace.mapEffect((chunk)=>Effect__namespace.gen(function*() {
buf += chunk;
// Scan buffer for newlines
const parts = buf.split("\n");
const validChunks = [];
for (const part of parts){
try {
// Attempt to parse chunk as JSON
validChunks.push(JSON.parse(part));
// Advance buffer if parsing succeeded
buf = buf.slice(part.length + 1);
} catch {
//
}
}
yield* Effect__namespace.logDebug("Received chunks").pipe(Effect__namespace.annotateLogs("chunk", chunk), Effect__namespace.annotateLogs("parsedChunks", validChunks), Effect__namespace.annotateLogs("buf", buf));
return validChunks;
})), Stream__namespace.mapEffect(S__namespace.decodeUnknown(S__namespace.Array(schema))), Stream__namespace.mapEffect(Effect__namespace.forEach((part)=>onChunk(part))), Stream__namespace.runDrain, Effect__namespace.withLogSpan("handleJsonLineStream"));
};
const withMinimalLogLevel = Config__namespace.logLevel("logLevel").pipe(Config__namespace.withDefault(LogLevel__namespace.Info), Effect__namespace.andThen((level)=>Logger__namespace.minimumLogLevel(level)), Effect__namespace.tapError((e)=>Effect__namespace.logError("Invalid log level").pipe(Effect__namespace.annotateLogs("error", e))), Effect__namespace.catchTag("ConfigError", (e)=>new shared.UploadThingError({
code: "INVALID_SERVER_CONFIG",
message: "Invalid server configuration",
cause: e
})), Layer__namespace.unwrapEffect);
const LogFormat = Config__namespace.literal("json", "logFmt", "structured", "pretty")("logFormat");
const withLogFormat = Effect__namespace.gen(function*() {
const isDev = yield* IsDevelopment;
const logFormat = yield* LogFormat.pipe(Config__namespace.withDefault(isDev ? "pretty" : "json"));
return Logger__namespace[logFormat];
}).pipe(Effect__namespace.catchTag("ConfigError", (e)=>new shared.UploadThingError({
code: "INVALID_SERVER_CONFIG",
message: "Invalid server configuration",
cause: e
})), Layer__namespace.unwrapEffect);
const logHttpClientResponse = (message, opts)=>{
const mixin = opts?.mixin ?? "json";
const level = LogLevel__namespace.fromLiteral(opts?.level ?? "Debug");
return (response)=>Effect__namespace.flatMap(mixin !== "None" ? response[mixin] : Effect__namespace.void, ()=>Effect__namespace.logWithLevel(level, `${message} (${response.status})`).pipe(Effect__namespace.annotateLogs("response", response)));
};
const logHttpClientError = (message)=>(err)=>err._tag === "ResponseError" ? logHttpClientResponse(message, {
level: "Error"
})(err.response) : Effect__namespace.logError(message).pipe(Effect__namespace.annotateLogs("error", err));
class ParserError extends Data__namespace.TaggedError("ParserError") {
constructor(...args){
super(...args), this.message = "Input validation failed. The original error with it's validation issues is in the error cause.";
}
}
function getParseFn(parser) {
if ("~standard" in parser) {
/**
* Standard Schema
*/ return async (value)=>{
const result = await parser["~standard"].validate(value);
if (result.issues) {
throw new ParserError({
cause: result.issues
});
}
return result.value;
};
}
if ("parseAsync" in parser && typeof parser.parseAsync === "function") {
/**
* Zod
* TODO (next major): Consider wrapping ZodError in ParserError
*/ return parser.parseAsync;
}
if (S__namespace.isSchema(parser)) {
/**
* Effect Schema
*/ return (value)=>S__namespace.decodeUnknownPromise(parser)(value).catch((error)=>{
throw new ParserError({
cause: Cause__namespace.squash(error[Runtime__namespace.FiberFailureCauseId])
});
});
}
throw new Error("Invalid parser");
}
class FileSizeMismatch extends Data__namespace.Error {
constructor(type, max, actual){
const reason = `You uploaded a ${type} file that was ${shared.bytesToFileSize(actual)}, but the limit for that type is ${max}`;
super({
reason
}), this._tag = "FileSizeMismatch", this.name = "FileSizeMismatchError";
}
}
class FileCountMismatch extends Data__namespace.Error {
constructor(type, boundtype, bound, actual){
const reason = `You uploaded ${actual} file(s) of type '${type}', but the ${boundtype} for that type is ${bound}`;
super({
reason
}), this._tag = "FileCountMismatch", this.name = "FileCountMismatchError";
}
}
// Verify that the uploaded files doesn't violate the route config,
// e.g. uploading more videos than allowed, or a file that is larger than allowed.
// This is double-checked on infra side, but we want to fail early to avoid network latency.
const assertFilesMeetConfig = (files, routeConfig)=>Effect__namespace.gen(function*() {
const counts = {};
for (const file of files){
const type = yield* shared.matchFileType(file, shared.objectKeys(routeConfig));
counts[type] = (counts[type] ?? 0) + 1;
const sizeLimit = routeConfig[type]?.maxFileSize;
if (!sizeLimit) {
return yield* new shared.InvalidRouteConfigError(type, "maxFileSize");
}
const sizeLimitBytes = yield* shared.fileSizeToBytes(sizeLimit);
if (file.size > sizeLimitBytes) {
return yield* new FileSizeMismatch(type, sizeLimit, file.size);
}
}
for(const _key in counts){
const key = _key;
const config = routeConfig[key];
if (!config) return yield* new shared.InvalidRouteConfigError(key);
const count = counts[key];
const min = config.minFileCount;
const max = config.maxFileCount;
if (min > max) {
return yield* new shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid config during file count - minFileCount > maxFileCount",
cause: `minFileCount must be less than maxFileCount for key ${key}. got: ${min} > ${max}`
});
}
if (count < min) {
return yield* new FileCountMismatch(key, "minimum", min, count);
}
if (count > max) {
return yield* new FileCountMismatch(key, "maximum", max, count);
}
}
return null;
});
const extractRouterConfig = (router)=>Effect__namespace.forEach(shared.objectKeys(router), (slug)=>Effect__namespace.map(shared.fillInputRouteConfig(router[slug].routerConfig), (config)=>({
slug,
config
})));
const makeRuntime = (fetch, config)=>{
const fetchHttpClient = Layer__namespace.provideMerge(platform.FetchHttpClient.layer, Layer__namespace.succeed(platform.FetchHttpClient.Fetch, fetch));
const withRedactedHeaders = Layer__namespace.effectDiscard(FiberRef__namespace.update(platform.Headers.currentRedactedNames, (_)=>_.concat([
"x-uploadthing-api-key"
])));
const layer = Layer__namespace.provide(Layer__namespace.mergeAll(withLogFormat, withMinimalLogLevel, fetchHttpClient, withRedactedHeaders), Layer__namespace.setConfigProvider(configProvider(config)));
return ManagedRuntime__namespace.make(layer);
};
class AdapterArguments extends Context__namespace.Tag("uploadthing/AdapterArguments")() {
}
const makeAdapterHandler = (makeAdapterArgs, toRequest, opts, beAdapter)=>{
const managed = makeRuntime(opts.config?.fetch, opts.config);
const handle = Effect__namespace.promise(()=>managed.runtime().then(platform.HttpApp.toWebHandlerRuntime));
const app = (...args)=>Effect__namespace.map(Effect__namespace.promise(()=>managed.runPromise(createRequestHandler(opts, beAdapter))), Effect__namespace.provideServiceEffect(AdapterArguments, makeAdapterArgs(...args)));
return async (...args)=>{
const result = await handle.pipe(Effect__namespace.ap(app(...args)), Effect__namespace.ap(toRequest(...args)), Effect__namespace.withLogSpan("requestHandler"), managed.runPromise);
return result;
};
};
const createRequestHandler = (opts, beAdapter)=>Effect__namespace.gen(function*() {
const isDevelopment = yield* IsDevelopment;
const routerConfig = yield* extractRouterConfig(opts.router);
const handleDaemon = (()=>{
if (opts.config?.handleDaemonPromise) {
return opts.config.handleDaemonPromise;
}
return isDevelopment ? "void" : "await";
})();
if (isDevelopment && handleDaemon === "await") {
return yield* new shared.UploadThingError({
code: "INVALID_SERVER_CONFIG",
message: 'handleDaemonPromise: "await" is forbidden in development.'
});
}
const GET = Effect__namespace.gen(function*() {
return yield* platform.HttpServerResponse.json(routerConfig);
});
const POST = Effect__namespace.gen(function*() {
const { "uploadthing-hook": uploadthingHook, "x-uploadthing-package": fePackage, "x-uploadthing-version": clientVersion } = yield* platform.HttpServerRequest.schemaHeaders(S__namespace.Struct({
"uploadthing-hook": UploadThingHook.pipe(S__namespace.optional),
"x-uploadthing-package": S__namespace.String.pipe(S__namespace.optionalWith({
default: ()=>"unknown"
})),
"x-uploadthing-version": S__namespace.String.pipe(S__namespace.optionalWith({
default: ()=>version
}))
}));
if (clientVersion !== version) {
const serverVersion = version;
yield* Effect__namespace.logWarning("Client version mismatch. Things may not work as expected, please sync your versions to ensure compatibility.").pipe(Effect__namespace.annotateLogs({
clientVersion,
serverVersion
}));
}
const { slug, actionType } = yield* platform.HttpRouter.schemaParams(S__namespace.Struct({
actionType: ActionType.pipe(S__namespace.optional),
slug: S__namespace.String
}));
const uploadable = opts.router[slug];
if (!uploadable) {
const msg = `No file route found for slug ${slug}`;
yield* Effect__namespace.logError(msg);
return yield* new shared.UploadThingError({
code: "NOT_FOUND",
message: msg
});
}
const { body, fiber } = yield* Match__namespace.value({
actionType,
uploadthingHook
}).pipe(Match__namespace.when({
actionType: "upload",
uploadthingHook: undefined
}, ()=>handleUploadAction({
uploadable,
fePackage,
beAdapter,
slug
})), Match__namespace.when({
actionType: undefined,
uploadthingHook: "callback"
}, ()=>handleCallbackRequest({
uploadable,
fePackage,
beAdapter
})), Match__namespace.when({
actionType: undefined,
uploadthingHook: "error"
}, ()=>handleErrorRequest({
uploadable
})), Match__namespace.orElse(()=>Effect__namespace.succeed({
body: null,
fiber: null
})));
if (fiber) {
yield* Effect__namespace.logDebug("Running fiber as daemon").pipe(Effect__namespace.annotateLogs("handleDaemon", handleDaemon));
if (handleDaemon === "void") ; else if (handleDaemon === "await") {
yield* fiber.await;
} else if (typeof handleDaemon === "function") {
handleDaemon(Effect__namespace.runPromise(fiber.await));
}
}
yield* Effect__namespace.logDebug("Sending response").pipe(Effect__namespace.annotateLogs("body", body));
return yield* platform.HttpServerResponse.json(body);
}).pipe(Effect__namespace.catchTags({
ParseError: (e)=>platform.HttpServerResponse.json(formatError(new shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid input",
cause: e.message
}), opts.router), {
status: 400
}),
UploadThingError: (e)=>// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
platform.HttpServerResponse.json(formatError(e, opts.router), {
status: shared.getStatusCodeFromError(e)
})
}));
const appendResponseHeaders = Effect__namespace.map(platform.HttpServerResponse.setHeader("x-uploadthing-version", version));
return platform.HttpRouter.empty.pipe(platform.HttpRouter.get("*", GET), platform.HttpRouter.post("*", POST), platform.HttpRouter.use(appendResponseHeaders));
}).pipe(Effect__namespace.withLogSpan("createRequestHandler"));
const handleErrorRequest = (opts)=>Effect__namespace.gen(function*() {
const { uploadable } = opts;
const request = yield* platform.HttpServerRequest.HttpServerRequest;
const { apiKey } = yield* UTToken;
const verified = yield* shared.verifySignature((yield* request.text), request.headers["x-uploadthing-signature"], apiKey);
yield* Effect__namespace.logDebug(`Signature verified: ${verified}`);
if (!verified) {
yield* Effect__namespace.logError("Invalid signature");
return yield* new shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid signature"
});
}
const requestInput = yield* platform.HttpServerRequest.schemaBodyJson(S__namespace.Struct({
fileKey: S__namespace.String,
error: S__namespace.String
}));
yield* Effect__namespace.logDebug("Handling error callback request with input:").pipe(Effect__namespace.annotateLogs("json", requestInput));
const adapterArgs = yield* AdapterArguments;
const fiber = yield* Effect__namespace.tryPromise({
try: async ()=>uploadable.onUploadError({
...adapterArgs,
error: new shared.UploadThingError({
code: "UPLOAD_FAILED",
message: `Upload failed for ${requestInput.fileKey}: ${requestInput.error}`
}),
fileKey: requestInput.fileKey
}),
catch: (error)=>new shared.UploadThingError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to run onUploadError",
cause: error
})
}).pipe(Effect__namespace.tapError((error)=>Effect__namespace.logError("Failed to run onUploadError. You probably shouldn't be throwing errors here.").pipe(Effect__namespace.annotateLogs("error", error)))).pipe(Effect__namespace.ignoreLogged, Effect__namespace.forkDaemon);
return {
body: null,
fiber
};
}).pipe(Effect__namespace.withLogSpan("handleErrorRequest"));
const handleCallbackRequest = (opts)=>Effect__namespace.gen(function*() {
const { uploadable, fePackage, beAdapter } = opts;
const request = yield* platform.HttpServerRequest.HttpServerRequest;
const { apiKey } = yield* UTToken;
const verified = yield* shared.verifySignature((yield* request.text), request.headers["x-uploadthing-signature"], apiKey);
yield* Effect__namespace.logDebug(`Signature verified: ${verified}`);
if (!verified) {
yield* Effect__namespace.logError("Invalid signature");
return yield* new shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid signature"
});
}
const requestInput = yield* platform.HttpServerRequest.schemaBodyJson(S__namespace.Struct({
status: S__namespace.String,
file: UploadedFileData,
metadata: S__namespace.Record({
key: S__namespace.String,
value: S__namespace.Unknown
})
}));
yield* Effect__namespace.logDebug("Handling callback request with input:").pipe(Effect__namespace.annotateLogs("json", requestInput));
/**
* Run `.onUploadComplete` as a daemon to prevent the
* request from UT to potentially timeout.
*/ const fiber = yield* Effect__namespace.gen(function*() {
const adapterArgs = yield* AdapterArguments;
const serverData = yield* Effect__namespace.tryPromise({
try: async ()=>uploadable.onUploadComplete({
...adapterArgs,
file: requestInput.file,
metadata: requestInput.metadata
}),
catch: (error)=>new shared.UploadThingError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to run onUploadComplete. You probably shouldn't be throwing errors here.",
cause: error
})
});
const payload = {
fileKey: requestInput.file.key,
callbackData: serverData ?? null
};
yield* Effect__namespace.logDebug("'onUploadComplete' callback finished. Sending response to UploadThing:").pipe(Effect__namespace.annotateLogs("callbackData", payload));
const baseUrl = yield* IngestUrl;
const httpClient = (yield* platform.HttpClient.HttpClient).pipe(platform.HttpClient.filterStatusOk);
yield* platform.HttpClientRequest.post(`/callback-result`).pipe(platform.HttpClientRequest.prependUrl(baseUrl), platform.HttpClientRequest.setHeaders({
"x-uploadthing-api-key": Redacted__namespace.value(apiKey),
"x-uploadthing-version": version,
"x-uploadthing-be-adapter": beAdapter,
"x-uploadthing-fe-package": fePackage
}), platform.HttpClientRequest.bodyJson(payload), Effect__namespace.flatMap(httpClient.execute), Effect__namespace.tapError(logHttpClientError("Failed to register callback result")), Effect__namespace.flatMap(platform.HttpClientResponse.schemaBodyJson(CallbackResultResponse)), Effect__namespace.tap(Effect__namespace.log("Sent callback result to UploadThing")), Effect__namespace.scoped);
}).pipe(Effect__namespace.ignoreLogged, Effect__namespace.forkDaemon);
return {
body: null,
fiber
};
}).pipe(Effect__namespace.withLogSpan("handleCallbackRequest"));
const runRouteMiddleware = (opts)=>Effect__namespace.gen(function*() {
const { json: { files, input }, uploadable } = opts;
yield* Effect__namespace.logDebug("Running middleware");
const adapterArgs = yield* AdapterArguments;
const metadata = yield* Effect__namespace.tryPromise({
try: async ()=>uploadable.middleware({
...adapterArgs,
input,
files
}),
catch: (error)=>error instanceof shared.UploadThingError ? error : new shared.UploadThingError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to run middleware",
cause: error
})
});
if (metadata[types_cjs.UTFiles] && metadata[types_cjs.UTFiles].length !== files.length) {
const msg = `Expected files override to have the same length as original files, got ${metadata[types_cjs.UTFiles].length} but expected ${files.length}`;
yield* Effect__namespace.logError(msg);
return yield* new shared.UploadThingError({
code: "BAD_REQUEST",
message: "Files override must have the same length as files",
cause: msg
});
}
// Attach customIds from middleware to the files
const filesWithCustomIds = yield* Effect__namespace.forEach(files, (file, idx)=>Effect__namespace.gen(function*() {
const theirs = metadata[types_cjs.UTFiles]?.[idx];
if (theirs && theirs.size !== file.size) {
yield* Effect__namespace.logWarning("File size mismatch. Reverting to original size");
}
return {
name: theirs?.name ?? file.name,
size: file.size,
type: file.type,
customId: theirs?.customId,
lastModified: theirs?.lastModified ?? Date.now()
};
}));
return {
metadata,
filesWithCustomIds
};
}).pipe(Effect__namespace.withLogSpan("runRouteMiddleware"));
const handleUploadAction = (opts)=>Effect__namespace.gen(function*() {
const httpClient = (yield* platform.HttpClient.HttpClient).pipe(platform.HttpClient.filterStatusOk);
const { uploadable, fePackage, beAdapter, slug } = opts;
const json = yield* platform.HttpServerRequest.schemaBodyJson(UploadActionPayload);
yield* Effect__namespace.logDebug("Handling upload request").pipe(Effect__namespace.annotateLogs("json", json));
// validate the input
yield* Effect__namespace.logDebug("Parsing user input");
const parsedInput = yield* Effect__namespace.tryPromise({
try: ()=>getParseFn(uploadable.inputParser)(json.input),
catch: (error)=>new shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid input",
cause: error
})
});
yield* Effect__namespace.logDebug("Input parsed successfully").pipe(Effect__namespace.annotateLogs("input", parsedInput));
const { metadata, filesWithCustomIds } = yield* runRouteMiddleware({
json: {
input: parsedInput,
files: json.files
},
uploadable
});
yield* Effect__namespace.logDebug("Parsing route config").pipe(Effect__namespace.annotateLogs("routerConfig", uploadable.routerConfig));
const parsedConfig = yield* shared.fillInputRouteConfig(uploadable.routerConfig).pipe(Effect__namespace.catchTag("InvalidRouteConfig", (err)=>new shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid route config",
cause: err
})));
yield* Effect__namespace.logDebug("Route config parsed successfully").pipe(Effect__namespace.annotateLogs("routeConfig", parsedConfig));
yield* Effect__namespace.logDebug("Validating files meet the config requirements").pipe(Effect__namespace.annotateLogs("files", json.files));
yield* assertFilesMeetConfig(json.files, parsedConfig).pipe(Effect__namespace.mapError((e)=>new shared.UploadThingError({
code: "BAD_REQUEST",
message: `Invalid config: ${e._tag}`,
cause: "reason" in e ? e.reason : e.message
})));
yield* Effect__namespace.logDebug("Files validated.");
const fileUploadRequests = yield* Effect__namespace.forEach(filesWithCustomIds, (file)=>Effect__namespace.map(shared.matchFileType(file, shared.objectKeys(parsedConfig)), (type)=>({
name: file.name,
size: file.size,
type: file.type || type,
lastModified: file.lastModified,
customId: file.customId,
contentDisposition: parsedConfig[type]?.contentDisposition ?? "inline",
acl: parsedConfig[type]?.acl
}))).pipe(Effect__namespace.catchTags({
/** Shouldn't happen since config is validated above so just dying is fine I think */ InvalidFileType: (e)=>Effect__namespace.die(e),
UnknownFileType: (e)=>Effect__namespace.die(e)
}));
const routeOptions = uploadable.routeOptions;
const { apiKey, appId } = yield* UTToken;
const ingestUrl = yield* IngestUrl;
const isDev = yield* IsDevelopment;
yield* Effect__namespace.logDebug("Generating presigned URLs").pipe(Effect__namespace.annotateLogs("fileUploadRequests", fileUploadRequests), Effect__namespace.annotateLogs("ingestUrl", ingestUrl));
const presignedUrls = yield* Effect__namespace.forEach(fileUploadRequests, (file)=>Effect__namespace.gen(function*() {
const key = yield* shared.generateKey(file, appId, routeOptions.getFileHashParts);
const url = yield* shared.generateSignedURL(`${ingestUrl}/${key}`, apiKey, {
ttlInSeconds: routeOptions.presignedURLTTL,
data: {
"x-ut-identifier": appId,
"x-ut-file-name": file.name,
"x-ut-file-size": file.size,
"x-ut-file-type": file.type,
"x-ut-slug": slug,
"x-ut-custom-id": file.customId,
"x-ut-content-disposition": file.contentDisposition,
"x-ut-acl": file.acl
}
});
return {
url,
key
};
}), {
concurrency: "unbounded"
});
const serverReq = yield* platform.HttpServerRequest.HttpServerRequest;
const requestUrl = yield* platform.HttpServerRequest.toURL(serverReq);
const devHookRequest = yield* Config__namespace.string("callbackUrl").pipe(Config__namespace.withDefault(requestUrl.origin + requestUrl.pathname), Effect__namespace.map((url)=>platform.HttpClientRequest.post(url).pipe(platform.HttpClientRequest.appendUrlParam("slug", slug))));
const metadataRequest = platform.HttpClientRequest.post("/route-metadata").pipe(platform.HttpClientRequest.prependUrl(ingestUrl), platform.HttpClientRequest.setHeaders({
"x-uploadthing-api-key": Redacted__namespace.value(apiKey),
"x-uploadthing-version": version,
"x-uploadthing-be-adapter": beAdapter,
"x-uploadthing-fe-package": fePackage
}), platform.HttpClientRequest.bodyJson({
fileKeys: presignedUrls.map(({ key })=>key),
metadata: metadata,
isDev,
callbackUrl: devHookRequest.url,
callbackSlug: slug,
awaitServerData: routeOptions.awaitServerData ?? true
}), Effect__namespace.flatMap(httpClient.execute));
// Send metadata to UT server (non blocking as a daemon)
// In dev, keep the stream open and simulate the callback requests as
// files complete uploading
const fiber = yield* Effect__namespace.if(isDev, {
onTrue: ()=>metadataRequest.pipe(Effect__namespace.tapBoth({
onSuccess: logHttpClientResponse("Registered metadata", {
mixin: "None"
}),
onFailure: logHttpClientError("Failed to register metadata")
}), platform.HttpClientResponse.stream, handleJsonLineStream(MetadataFetchStreamPart, (chunk)=>devHookRequest.pipe(platform.HttpClientRequest.setHeaders({
"uploadthing-hook": chunk.hook,
"x-uploadthing-signature": chunk.signature
}), platform.HttpClientRequest.setBody(platform.HttpBody.text(chunk.payload, "application/json")), httpClient.execute, Effect__namespace.tapBoth({
onSuccess: logHttpClientResponse("Successfully forwarded callback request from dev stream"),
onFailure: logHttpClientError("Failed to forward callback request from dev stream")
}), Effect__namespace.annotateLogs(chunk), Effect__namespace.asVoid, Effect__namespace.ignoreLogged, Effect__namespace.scoped))),
onFalse: ()=>metadataRequest.pipe(Effect__namespace.tapBoth({
onSuccess: logHttpClientResponse("Registered metadata"),
onFailure: logHttpClientError("Failed to register metadata")
}), Effect__namespace.flatMap(platform.HttpClientResponse.schemaBodyJson(MetadataFetchResponse)), Effect__namespace.scoped)
}).pipe(Effect__namespace.forkDaemon);
const presigneds = presignedUrls.map((p, i)=>({
url: p.url,
key: p.key,
name: fileUploadRequests[i].name,
customId: fileUploadRequests[i].customId ?? null
}));
yield* Effect__namespace.logInfo("Sending presigned URLs to client").pipe(Effect__namespace.annotateLogs("presignedUrls", presigneds));
return {
body: presigneds,
fiber
};
}).pipe(Effect__namespace.withLogSpan("handleUploadAction"));
class InvalidURL extends Data__namespace.Error {
constructor(attemptedUrl, base){
Effect__namespace.runSync(Effect__namespace.logError(`Failed to parse URL from request. '${attemptedUrl}' is not a valid URL with base '${base}'.`));
super({
reason: `Failed to parse URL from request. '${attemptedUrl}' is not a valid URL with base '${base}'.`
}), this._tag = "InvalidURL", this.name = "InvalidURLError";
}
}
const parseURL = (req)=>{
const headers = req.headers;
let relativeUrl = req.url ?? "/";
if ("baseUrl" in req && typeof req.baseUrl === "string") {
relativeUrl = req.baseUrl + relativeUrl;
}
const proto = headers?.["x-forwarded-proto"] ?? "http";
const host = headers?.["x-forwarded-host"] ?? headers?.host;
const baseUrl = Config__namespace.string("url").pipe(Config__namespace.withDefault(`${proto.toString()}://${host?.toString()}`));
return Effect__namespace.flatMap(baseUrl, (baseUrl)=>Effect__namespace.try({
try: ()=>new URL(relativeUrl, baseUrl),
catch: ()=>new InvalidURL(relativeUrl, baseUrl)
})).pipe(Effect__namespace.catchTag("ConfigError", ()=>Effect__namespace.fail(new InvalidURL(relativeUrl))));
};
const isBodyAllowed = (method)=>[
"POST",
"PUT",
"PATCH"
].includes(method);
const getPostBody = (opts)=>Effect__namespace.async((resume)=>{
const { req } = opts;
if (!req.method || !isBodyAllowed(req.method)) {
return resume(Effect__namespace.succeed(undefined));
}
const contentType = req.headers?.["content-type"];
if ("body" in req) {
if (contentType !== "application/json") {
Effect__namespace.runSync(Effect__namespace.logError("Expected JSON content type, got:", contentType));
return resume(new shared.UploadThingError({
code: "BAD_REQUEST",
message: "INVALID_CONTENT_TYPE"
}));
}
if (typeof req.body !== "object") {
Effect__namespace.runSync(Effect__namespace.logError("Expected body to be of type 'object', got:", typeof req.body));
return resume(new shared.UploadThingError({
code: "BAD_REQUEST",
message: "INVALID_BODY"
}));
}
Effect__namespace.runSync(Effect__namespace.logDebug("Body parsed successfully.", req.body));
return resume(Effect__namespace.succeed(req.body));
}
let body = "";
req.on("data", (data)=>body += data);
req.on("end", ()=>{
const parsedBody = Effect__namespace.try({
try: ()=>JSON.parse(body),
catch: (err)=>new shared.UploadThingError({
code: "BAD_REQUEST",
message: "INVALID_JSON",
cause: err
})
});
return resume(parsedBody);
});
});
const toWebRequest = (req, body)=>{
body ??= req.body;
const bodyStr = typeof body === "string" ? body : JSON.stringify(body);
const method = req.method ?? "GET";
const allowsBody = isBodyAllowed(method);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers ?? [])){
if (typeof value === "string") headers.set(key, value);
if (Array.isArray(value)) headers.set(key, value.join(","));
}
return parseURL(req).pipe(Effect__namespace.catchTag("InvalidURL", (e)=>Effect__namespace.die(e)), Effect__namespace.andThen((url)=>new Request(url, {
method,
headers,
...allowsBody ? {
body: bodyStr
} : {}
})));
};
function internalCreateBuilder(initDef = {}) {
const _def = {
$types: {},
// Default router config
routerConfig: {
image: {
maxFileSize: "4MB"
}
},
routeOptions: {
awaitServerData: true
},
inputParser: {
parseAsync: ()=>Promise.resolve(undefined),
_input: undefined,
_output: undefined
},
middleware: ()=>({}),
onUploadError: ()=>{
// noop
},
onUploadComplete: ()=>undefined,
errorFormatter: initDef.errorFormatter ?? defaultErrorFormatter,
// Overload with properties passed in
...initDef
};
return {
input (userParser) {
return internalCreateBuilder({
..._def,
inputParser: userParser
});
},
middleware (userMiddleware) {
return internalCreateBuilder({
..._def,
middleware: userMiddleware
});
},
onUploadComplete (userUploadComplete) {
return {
..._def,
onUploadComplete: userUploadComplete
};
},
onUploadError (userOnUploadError) {
return internalCreateBuilder({
..._def,
onUploadError: userOnUploadError
});
}
};
}
function createBuilder(opts) {
return (input, config)=>{
return internalCreateBuilder({
routerConfig: input,
routeOptions: config ?? {},
...opts
});
};
}
const createUploadthing = (opts)=>createBuilder(opts);
const createRouteHandler = (opts)=>{
const handler = makeAdapterHandler((req, res)=>Effect__namespace.succeed({
req,
res,
event: undefined
}), (req)=>Effect__namespace.flatMap(getPostBody({
req
}), (body)=>toWebRequest(req, body)).pipe(Effect__namespace.orDie), opts, "express");
return express.Router().all("/", // eslint-disable-next-line @typescript-eslint/no-misused-promises
async (req, res)=>{
const response = await handler(req, res);
res.writeHead(response.status, Object.fromEntries(response.headers));
if (!response.body) return res.end();
// Slight type mismatch in `node:stream.ReadableStream` and Fetch's `ReadableStream`.
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return node_stream.Readable.fromWeb(response.body).pipe(res);
});
};
Object.defineProperty(exports, "UTFiles", {
enumerable: true,
get: function () { return types_cjs.UTFiles; }
});
exports.createRouteHandler = createRouteHandler;
exports.createUploadthing = createUploadthing;