uploadthing
Version:
Learn more: [docs.uploadthing.com](https://docs.uploadthing.com)
794 lines (783 loc) • 37.6 kB
JavaScript
const require_chunk = require('./chunk-CUT6urMc.cjs');
const require_package = require('./package-CKF1Vr61.cjs');
const require_deprecations = require('./deprecations-DPGpmqha.cjs');
const require_shared_schemas = require('./shared-schemas-CG9VaBtT.cjs');
const __uploadthing_shared = require_chunk.__toESM(require("@uploadthing/shared"));
const effect_Effect = require_chunk.__toESM(require("effect/Effect"));
const __effect_platform_HttpApp = require_chunk.__toESM(require("@effect/platform/HttpApp"));
const __effect_platform_HttpBody = require_chunk.__toESM(require("@effect/platform/HttpBody"));
const __effect_platform_HttpClient = require_chunk.__toESM(require("@effect/platform/HttpClient"));
const __effect_platform_HttpClientRequest = require_chunk.__toESM(require("@effect/platform/HttpClientRequest"));
const __effect_platform_HttpClientResponse = require_chunk.__toESM(require("@effect/platform/HttpClientResponse"));
const __effect_platform_HttpRouter = require_chunk.__toESM(require("@effect/platform/HttpRouter"));
const __effect_platform_HttpServerRequest = require_chunk.__toESM(require("@effect/platform/HttpServerRequest"));
const __effect_platform_HttpServerResponse = require_chunk.__toESM(require("@effect/platform/HttpServerResponse"));
const effect_Config = require_chunk.__toESM(require("effect/Config"));
const effect_Context = require_chunk.__toESM(require("effect/Context"));
const effect_Match = require_chunk.__toESM(require("effect/Match"));
const effect_Redacted = require_chunk.__toESM(require("effect/Redacted"));
const effect_Schema = require_chunk.__toESM(require("effect/Schema"));
const effect_ConfigProvider = require_chunk.__toESM(require("effect/ConfigProvider"));
const effect_Stream = require_chunk.__toESM(require("effect/Stream"));
const effect_ConfigError = require_chunk.__toESM(require("effect/ConfigError"));
const effect_Either = require_chunk.__toESM(require("effect/Either"));
const effect_Layer = require_chunk.__toESM(require("effect/Layer"));
const effect_Logger = require_chunk.__toESM(require("effect/Logger"));
const effect_LogLevel = require_chunk.__toESM(require("effect/LogLevel"));
const effect_Cause = require_chunk.__toESM(require("effect/Cause"));
const effect_Data = require_chunk.__toESM(require("effect/Data"));
const effect_Runtime = require_chunk.__toESM(require("effect/Runtime"));
const __effect_platform_FetchHttpClient = require_chunk.__toESM(require("@effect/platform/FetchHttpClient"));
const __effect_platform_Headers = require_chunk.__toESM(require("@effect/platform/Headers"));
const effect_FiberRef = require_chunk.__toESM(require("effect/FiberRef"));
const effect_ManagedRuntime = require_chunk.__toESM(require("effect/ManagedRuntime"));
//#region src/_internal/config.ts
/**
* 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 = effect_ConfigProvider.fromEnv().pipe(effect_ConfigProvider.orElse(() => effect_ConfigProvider.fromMap(new Map(Object.entries((0, __uploadthing_shared.filterDefinedObjectValues)(import.meta?.env ?? {}))), { pathDelim: "_" })), effect_ConfigProvider.nested("uploadthing"), effect_ConfigProvider.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) => effect_ConfigProvider.fromJson(options ?? {}).pipe(effect_ConfigProvider.orElse(() => envProvider));
const IsDevelopment = effect_Config.boolean("isDev").pipe(effect_Config.orElse(() => effect_Config.succeed(typeof process !== "undefined" ? process.env.NODE_ENV : void 0).pipe(effect_Config.map((_) => _ === "development"))), effect_Config.withDefault(false));
const UTToken = effect_Schema.Config("token", require_shared_schemas.UploadThingToken).pipe(effect_Effect.catchTags({ ConfigError: (e) => new __uploadthing_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
}) }));
const ApiUrl = effect_Config.string("apiUrl").pipe(effect_Config.withDefault("https://api.uploadthing.com"), effect_Config.mapAttempt((_) => new URL(_)), effect_Config.map((url) => url.href.replace(/\/$/, "")));
const IngestUrl = effect_Effect.fn(function* (preferredRegion) {
const { regions, ingestHost } = yield* UTToken;
const region = preferredRegion ? regions.find((r) => r === preferredRegion) ?? regions[0] : regions[0];
return yield* effect_Config.string("ingestUrl").pipe(effect_Config.withDefault(`https://${region}.${ingestHost}`), effect_Config.mapAttempt((_) => new URL(_)), effect_Config.map((url) => url.href.replace(/\/$/, "")));
});
const UtfsHost = effect_Config.string("utfsHost").pipe(effect_Config.withDefault("utfs.io"));
const UfsHost = effect_Config.string("ufsHost").pipe(effect_Config.withDefault("ufs.sh"));
const UfsAppIdLocation = effect_Config.literal("subdomain", "path")("ufsAppIdLocation").pipe(effect_Config.withDefault("subdomain"));
//#endregion
//#region src/_internal/error-formatter.ts
function defaultErrorFormatter(error) {
return { message: error.message };
}
function formatError(error, router) {
const firstSlug = Object.keys(router)[0];
const errorFormatter = firstSlug ? router[firstSlug]?.errorFormatter ?? defaultErrorFormatter : defaultErrorFormatter;
return errorFormatter(error);
}
//#endregion
//#region src/_internal/jsonl.ts
const handleJsonLineStream = (schema, onChunk) => (stream) => {
let buf = "";
return stream.pipe(effect_Stream.decodeText(), effect_Stream.mapEffect((chunk) => effect_Effect.gen(function* () {
buf += chunk;
const parts = buf.split("\n");
const validChunks = [];
for (const part of parts) try {
validChunks.push(JSON.parse(part));
buf = buf.slice(part.length + 1);
} catch {}
yield* effect_Effect.logDebug("Received chunks").pipe(effect_Effect.annotateLogs("chunk", chunk), effect_Effect.annotateLogs("parsedChunks", validChunks), effect_Effect.annotateLogs("buf", buf));
return validChunks;
})), effect_Stream.mapEffect(effect_Schema.decodeUnknown(effect_Schema.Array(schema))), effect_Stream.mapEffect(effect_Effect.forEach((part) => onChunk(part))), effect_Stream.runDrain, effect_Effect.withLogSpan("handleJsonLineStream"));
};
//#endregion
//#region src/_internal/logger.ts
/**
* Config.logLevel counter-intuitively accepts LogLevel["label"]
* instead of a literal, ripping it and changing to accept literal
* Effect 4.0 will change this to accept a literal and then we can
* remove this and go back to the built-in validator.
*/
const ConfigLogLevel = (name) => {
const config = effect_Config.mapOrFail(effect_Config.string(), (literal) => {
const level = effect_LogLevel.allLevels.find((level$1) => level$1._tag === literal);
return level === void 0 ? effect_Either.left(effect_ConfigError.InvalidData([], `Expected a log level but received ${literal}`)) : effect_Either.right(level);
});
return name === void 0 ? config : effect_Config.nested(config, name);
};
const withMinimalLogLevel = ConfigLogLevel("logLevel").pipe(effect_Config.withDefault(effect_LogLevel.Info), effect_Effect.andThen((level) => effect_Logger.minimumLogLevel(level)), effect_Effect.tapError((e) => effect_Effect.logError("Invalid log level").pipe(effect_Effect.annotateLogs("error", e))), effect_Effect.catchTag("ConfigError", (e) => new __uploadthing_shared.UploadThingError({
code: "INVALID_SERVER_CONFIG",
message: "Invalid server configuration",
cause: e
})), effect_Layer.unwrapEffect);
const LogFormat = effect_Config.literal("json", "logFmt", "structured", "pretty")("logFormat");
const withLogFormat = effect_Effect.gen(function* () {
const isDev = yield* IsDevelopment;
const logFormat = yield* LogFormat.pipe(effect_Config.withDefault(isDev ? "pretty" : "json"));
return effect_Logger[logFormat];
}).pipe(effect_Effect.catchTag("ConfigError", (e) => new __uploadthing_shared.UploadThingError({
code: "INVALID_SERVER_CONFIG",
message: "Invalid server configuration",
cause: e
})), effect_Layer.unwrapEffect);
const logHttpClientResponse = (message, opts) => {
const mixin = opts?.mixin ?? "json";
const level = effect_LogLevel.fromLiteral(opts?.level ?? "Debug");
return (response) => effect_Effect.flatMap(mixin !== "None" ? response[mixin] : effect_Effect.void, () => effect_Effect.logWithLevel(level, `${message} (${response.status})`).pipe(effect_Effect.annotateLogs("response", response)));
};
const logHttpClientError = (message) => (err) => err._tag === "ResponseError" ? logHttpClientResponse(message, { level: "Error" })(err.response) : effect_Effect.logError(message).pipe(effect_Effect.annotateLogs("error", err));
//#endregion
//#region src/_internal/parser.ts
var ParserError = class extends effect_Data.TaggedError("ParserError") {
message = "Input validation failed. The original error with it's validation issues is in the error cause.";
};
function getParseFn(parser) {
if ("parseAsync" in parser && typeof parser.parseAsync === "function")
/**
* Zod
* TODO (next major): Consider wrapping ZodError in ParserError
*/
return parser.parseAsync;
if (effect_Schema.isSchema(parser))
/**
* Effect Schema
*/
return (value) => effect_Schema.decodeUnknownPromise(parser)(value).catch((error) => {
throw new ParserError({ cause: effect_Cause.squash(error[effect_Runtime.FiberFailureCauseId]) });
});
if ("~standard" in parser)
/**
* Standard Schema
* TODO (next major): Consider moving this to the top of the function
*/
return async (value) => {
const result = await parser["~standard"].validate(value);
if (result.issues) throw new ParserError({ cause: result.issues });
return result.value;
};
throw new Error("Invalid parser");
}
//#endregion
//#region src/_internal/route-config.ts
var FileSizeMismatch = class extends effect_Data.Error {
_tag = "FileSizeMismatch";
name = "FileSizeMismatchError";
constructor(type, max, actual) {
const reason = `You uploaded a ${type} file that was ${(0, __uploadthing_shared.bytesToFileSize)(actual)}, but the limit for that type is ${max}`;
super({ reason });
}
};
var FileCountMismatch = class extends effect_Data.Error {
_tag = "FileCountMismatch";
name = "FileCountMismatchError";
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 });
}
};
const assertFilesMeetConfig = (files, routeConfig) => effect_Effect.gen(function* () {
const counts = {};
for (const file of files) {
const type = yield* (0, __uploadthing_shared.matchFileType)(file, (0, __uploadthing_shared.objectKeys)(routeConfig));
counts[type] = (counts[type] ?? 0) + 1;
const sizeLimit = routeConfig[type]?.maxFileSize;
if (!sizeLimit) return yield* new __uploadthing_shared.InvalidRouteConfigError(type, "maxFileSize");
const sizeLimitBytes = yield* (0, __uploadthing_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 __uploadthing_shared.InvalidRouteConfigError(key);
const count = counts[key];
const min = config.minFileCount;
const max = config.maxFileCount;
if (min > max) return yield* new __uploadthing_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 != null && count < min) return yield* new FileCountMismatch(key, "minimum", min, count);
if (count != null && count > max) return yield* new FileCountMismatch(key, "maximum", max, count);
}
return null;
});
const extractRouterConfig = (router) => effect_Effect.forEach((0, __uploadthing_shared.objectKeys)(router), (slug) => effect_Effect.map((0, __uploadthing_shared.fillInputRouteConfig)(router[slug].routerConfig), (config) => ({
slug,
config
})));
//#endregion
//#region src/_internal/runtime.ts
const makeRuntime = (fetch, config) => {
const fetchHttpClient = effect_Layer.provideMerge(__effect_platform_FetchHttpClient.layer, effect_Layer.succeed(__effect_platform_FetchHttpClient.Fetch, fetch));
const withRedactedHeaders = effect_Layer.effectDiscard(effect_FiberRef.update(__effect_platform_Headers.currentRedactedNames, (_) => _.concat(["x-uploadthing-api-key"])));
const layer = effect_Layer.provide(effect_Layer.mergeAll(withLogFormat, withMinimalLogLevel, fetchHttpClient, withRedactedHeaders), effect_Layer.setConfigProvider(configProvider(config)));
return effect_ManagedRuntime.make(layer);
};
//#endregion
//#region src/_internal/types.ts
/**
* Marker used to select the region based on the incoming request
*/
const UTRegion = Symbol("uploadthing-region-symbol");
/**
* Marker used to append a `customId` to the incoming file data in `.middleware()`
* @example
* ```ts
* .middleware((opts) => {
* return {
* [UTFiles]: opts.files.map((file) => ({
* ...file,
* customId: generateId(),
* }))
* };
* })
* ```
*/
const UTFiles = Symbol("uploadthing-custom-id-symbol");
//#endregion
//#region src/_internal/handler.ts
var AdapterArguments = class extends effect_Context.Tag("uploadthing/AdapterArguments")() {};
/**
* Create a request handler adapter for any framework or server library.
* Refer to the existing adapters for examples on how to use this function.
* @public
*
* @param makeAdapterArgs - Function that takes the args from your framework and returns an Effect that resolves to the adapter args.
* These args are passed to the `.middleware`, `.onUploadComplete`, and `.onUploadError` hooks.
* @param toRequest - Function that takes the args from your framework and returns an Effect that resolves to a web Request object.
* @param opts - The router config and other options that are normally passed to `createRequestHandler` of official adapters
* @param beAdapter - [Optional] The adapter name of the adapter, used for telemetry purposes
* @returns A function that takes the args from your framework and returns a promise that resolves to a Response object.
*/
const makeAdapterHandler = (makeAdapterArgs, toRequest, opts, beAdapter) => {
const managed = makeRuntime(opts.config?.fetch, opts.config);
const handle = effect_Effect.promise(() => managed.runtime().then(__effect_platform_HttpApp.toWebHandlerRuntime));
const app = (...args) => effect_Effect.map(effect_Effect.promise(() => managed.runPromise(createRequestHandler(opts, beAdapter ?? "custom"))), effect_Effect.provideServiceEffect(AdapterArguments, makeAdapterArgs(...args)));
return async (...args) => {
const result = await handle.pipe(effect_Effect.ap(app(...args)), effect_Effect.ap(toRequest(...args)), effect_Effect.withLogSpan("requestHandler"), managed.runPromise);
return result;
};
};
const createRequestHandler = (opts, beAdapter) => effect_Effect.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 __uploadthing_shared.UploadThingError({
code: "INVALID_SERVER_CONFIG",
message: "handleDaemonPromise: \"await\" is forbidden in development."
});
const GET = effect_Effect.gen(function* () {
return yield* __effect_platform_HttpServerResponse.json(routerConfig);
});
const POST = effect_Effect.gen(function* () {
const { "uploadthing-hook": uploadthingHook, "x-uploadthing-package": fePackage, "x-uploadthing-version": clientVersion } = yield* __effect_platform_HttpServerRequest.schemaHeaders(effect_Schema.Struct({
"uploadthing-hook": require_shared_schemas.UploadThingHook.pipe(effect_Schema.optional),
"x-uploadthing-package": effect_Schema.String.pipe(effect_Schema.optionalWith({ default: () => "unknown" })),
"x-uploadthing-version": effect_Schema.String.pipe(effect_Schema.optionalWith({ default: () => require_package.version }))
}));
if (clientVersion !== require_package.version) {
const serverVersion = require_package.version;
yield* effect_Effect.logWarning("Client version mismatch. Things may not work as expected, please sync your versions to ensure compatibility.").pipe(effect_Effect.annotateLogs({
clientVersion,
serverVersion
}));
}
const { slug, actionType } = yield* __effect_platform_HttpRouter.schemaParams(effect_Schema.Struct({
actionType: require_shared_schemas.ActionType.pipe(effect_Schema.optional),
slug: effect_Schema.String
}));
const uploadable = opts.router[slug];
if (!uploadable) {
const msg = `No file route found for slug ${slug}`;
yield* effect_Effect.logError(msg);
return yield* new __uploadthing_shared.UploadThingError({
code: "NOT_FOUND",
message: msg
});
}
const { body, fiber } = yield* effect_Match.value({
actionType,
uploadthingHook
}).pipe(effect_Match.when({
actionType: "upload",
uploadthingHook: void 0
}, () => handleUploadAction({
uploadable,
fePackage,
beAdapter,
slug
})), effect_Match.when({
actionType: void 0,
uploadthingHook: "callback"
}, () => handleCallbackRequest({
uploadable,
fePackage,
beAdapter
})), effect_Match.when({
actionType: void 0,
uploadthingHook: "error"
}, () => handleErrorRequest({ uploadable })), effect_Match.orElse(() => effect_Effect.succeed({
body: null,
fiber: null
})));
if (fiber) {
yield* effect_Effect.logDebug("Running fiber as daemon").pipe(effect_Effect.annotateLogs("handleDaemon", handleDaemon));
if (handleDaemon === "void") {} else if (handleDaemon === "await") yield* fiber.await;
else if (typeof handleDaemon === "function") handleDaemon(effect_Effect.runPromise(fiber.await));
}
yield* effect_Effect.logDebug("Sending response").pipe(effect_Effect.annotateLogs("body", body));
return yield* __effect_platform_HttpServerResponse.json(body);
}).pipe(effect_Effect.catchTags({
ParseError: (e) => __effect_platform_HttpServerResponse.json(formatError(new __uploadthing_shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid input",
cause: e.message
}), opts.router), { status: 400 }),
UploadThingError: (e) => __effect_platform_HttpServerResponse.json(formatError(e, opts.router), { status: (0, __uploadthing_shared.getStatusCodeFromError)(e) })
}));
const appendResponseHeaders = effect_Effect.map(__effect_platform_HttpServerResponse.setHeader("x-uploadthing-version", require_package.version));
return __effect_platform_HttpRouter.empty.pipe(__effect_platform_HttpRouter.get("*", GET), __effect_platform_HttpRouter.post("*", POST), __effect_platform_HttpRouter.use(appendResponseHeaders));
}).pipe(effect_Effect.withLogSpan("createRequestHandler"));
const handleErrorRequest = (opts) => effect_Effect.gen(function* () {
const { uploadable } = opts;
const request = yield* __effect_platform_HttpServerRequest.HttpServerRequest;
const { apiKey } = yield* UTToken;
const verified = yield* (0, __uploadthing_shared.verifySignature)(yield* request.text, request.headers["x-uploadthing-signature"] ?? null, apiKey);
yield* effect_Effect.logDebug(`Signature verified: ${verified}`);
if (!verified) {
yield* effect_Effect.logError("Invalid signature");
return yield* new __uploadthing_shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid signature"
});
}
const requestInput = yield* __effect_platform_HttpServerRequest.schemaBodyJson(effect_Schema.Struct({
fileKey: effect_Schema.String,
error: effect_Schema.String
}));
yield* effect_Effect.logDebug("Handling error callback request with input:").pipe(effect_Effect.annotateLogs("json", requestInput));
const adapterArgs = yield* AdapterArguments;
const fiber = yield* effect_Effect.tryPromise({
try: async () => uploadable.onUploadError({
...adapterArgs,
error: new __uploadthing_shared.UploadThingError({
code: "UPLOAD_FAILED",
message: `Upload failed for ${requestInput.fileKey}: ${requestInput.error}`
}),
fileKey: requestInput.fileKey
}),
catch: (error) => new __uploadthing_shared.UploadThingError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to run onUploadError",
cause: error
})
}).pipe(effect_Effect.tapError((error) => effect_Effect.logError("Failed to run onUploadError. You probably shouldn't be throwing errors here.").pipe(effect_Effect.annotateLogs("error", error)))).pipe(effect_Effect.ignoreLogged, effect_Effect.forkDaemon);
return {
body: null,
fiber
};
}).pipe(effect_Effect.withLogSpan("handleErrorRequest"));
const handleCallbackRequest = (opts) => effect_Effect.gen(function* () {
const { uploadable, fePackage, beAdapter } = opts;
const request = yield* __effect_platform_HttpServerRequest.HttpServerRequest;
const { apiKey } = yield* UTToken;
const verified = yield* (0, __uploadthing_shared.verifySignature)(yield* request.text, request.headers["x-uploadthing-signature"] ?? null, apiKey);
yield* effect_Effect.logDebug(`Signature verified: ${verified}`);
if (!verified) {
yield* effect_Effect.logError("Invalid signature");
return yield* new __uploadthing_shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid signature"
});
}
const requestInput = yield* __effect_platform_HttpServerRequest.schemaBodyJson(effect_Schema.Struct({
status: effect_Schema.String,
file: require_shared_schemas.UploadedFileData,
origin: effect_Schema.String,
metadata: effect_Schema.Record({
key: effect_Schema.String,
value: effect_Schema.Unknown
})
}));
yield* effect_Effect.logDebug("Handling callback request with input:").pipe(effect_Effect.annotateLogs("json", requestInput));
/**
* Run `.onUploadComplete` as a daemon to prevent the
* request from UT to potentially timeout.
*/
const fiber = yield* effect_Effect.gen(function* () {
const adapterArgs = yield* AdapterArguments;
const serverData = yield* effect_Effect.tryPromise({
try: async () => uploadable.onUploadComplete({
...adapterArgs,
file: {
...requestInput.file,
get url() {
require_deprecations.logDeprecationWarning("`file.url` is deprecated and will be removed in uploadthing v9. Use `file.ufsUrl` instead.");
return requestInput.file.url;
},
get appUrl() {
require_deprecations.logDeprecationWarning("`file.appUrl` is deprecated and will be removed in uploadthing v9. Use `file.ufsUrl` instead.");
return requestInput.file.appUrl;
}
},
metadata: requestInput.metadata
}),
catch: (error) => new __uploadthing_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_Effect.logDebug("'onUploadComplete' callback finished. Sending response to UploadThing:").pipe(effect_Effect.annotateLogs("callbackData", payload));
const httpClient = (yield* __effect_platform_HttpClient.HttpClient).pipe(__effect_platform_HttpClient.filterStatusOk);
yield* __effect_platform_HttpClientRequest.post(`/callback-result`).pipe(__effect_platform_HttpClientRequest.prependUrl(requestInput.origin), __effect_platform_HttpClientRequest.setHeaders({
"x-uploadthing-api-key": effect_Redacted.value(apiKey),
"x-uploadthing-version": require_package.version,
"x-uploadthing-be-adapter": beAdapter,
"x-uploadthing-fe-package": fePackage
}), __effect_platform_HttpClientRequest.bodyJson(payload), effect_Effect.flatMap(httpClient.execute), effect_Effect.tapError(logHttpClientError("Failed to register callback result")), effect_Effect.flatMap(__effect_platform_HttpClientResponse.schemaBodyJson(require_shared_schemas.CallbackResultResponse)), effect_Effect.tap(effect_Effect.log("Sent callback result to UploadThing")), effect_Effect.scoped);
}).pipe(effect_Effect.ignoreLogged, effect_Effect.forkDaemon);
return {
body: null,
fiber
};
}).pipe(effect_Effect.withLogSpan("handleCallbackRequest"));
const runRouteMiddleware = (opts) => effect_Effect.gen(function* () {
const { json: { files, input }, uploadable } = opts;
yield* effect_Effect.logDebug("Running middleware");
const adapterArgs = yield* AdapterArguments;
const metadata = yield* effect_Effect.tryPromise({
try: async () => uploadable.middleware({
...adapterArgs,
input,
files
}),
catch: (error) => error instanceof __uploadthing_shared.UploadThingError ? error : new __uploadthing_shared.UploadThingError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to run middleware",
cause: error
})
});
if (metadata[UTFiles] && metadata[UTFiles].length !== files.length) {
const msg = `Expected files override to have the same length as original files, got ${metadata[UTFiles].length} but expected ${files.length}`;
yield* effect_Effect.logError(msg);
return yield* new __uploadthing_shared.UploadThingError({
code: "BAD_REQUEST",
message: "Files override must have the same length as files",
cause: msg
});
}
const filesWithCustomIds = yield* effect_Effect.forEach(files, (file, idx) => effect_Effect.gen(function* () {
const theirs = metadata[UTFiles]?.[idx];
if (theirs && theirs.size !== file.size) yield* effect_Effect.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,
preferredRegion: metadata[UTRegion]
};
}).pipe(effect_Effect.withLogSpan("runRouteMiddleware"));
const handleUploadAction = (opts) => effect_Effect.gen(function* () {
const httpClient = (yield* __effect_platform_HttpClient.HttpClient).pipe(__effect_platform_HttpClient.filterStatusOk);
const { uploadable, fePackage, beAdapter, slug } = opts;
const json = yield* __effect_platform_HttpServerRequest.schemaBodyJson(require_shared_schemas.UploadActionPayload);
yield* effect_Effect.logDebug("Handling upload request").pipe(effect_Effect.annotateLogs("json", json));
yield* effect_Effect.logDebug("Parsing user input");
const parsedInput = yield* effect_Effect.tryPromise({
try: () => getParseFn(uploadable.inputParser)(json.input),
catch: (error) => new __uploadthing_shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid input",
cause: error
})
});
yield* effect_Effect.logDebug("Input parsed successfully").pipe(effect_Effect.annotateLogs("input", parsedInput));
const { metadata, filesWithCustomIds, preferredRegion } = yield* runRouteMiddleware({
json: {
input: parsedInput,
files: json.files
},
uploadable
});
yield* effect_Effect.logDebug("Parsing route config").pipe(effect_Effect.annotateLogs("routerConfig", uploadable.routerConfig));
const parsedConfig = yield* (0, __uploadthing_shared.fillInputRouteConfig)(uploadable.routerConfig).pipe(effect_Effect.catchTag("InvalidRouteConfig", (err) => new __uploadthing_shared.UploadThingError({
code: "BAD_REQUEST",
message: "Invalid route config",
cause: err
})));
yield* effect_Effect.logDebug("Route config parsed successfully").pipe(effect_Effect.annotateLogs("routeConfig", parsedConfig));
yield* effect_Effect.logDebug("Validating files meet the config requirements").pipe(effect_Effect.annotateLogs("files", json.files));
yield* assertFilesMeetConfig(json.files, parsedConfig).pipe(effect_Effect.mapError((e) => new __uploadthing_shared.UploadThingError({
code: "BAD_REQUEST",
message: `Invalid config: ${e._tag}`,
cause: "reason" in e ? e.reason : e.message
})));
yield* effect_Effect.logDebug("Files validated.");
const fileUploadRequests = yield* effect_Effect.forEach(filesWithCustomIds, (file) => effect_Effect.map((0, __uploadthing_shared.matchFileType)(file, (0, __uploadthing_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_Effect.catchTags({
InvalidFileType: (e) => effect_Effect.die(e),
UnknownFileType: (e) => effect_Effect.die(e)
}));
const routeOptions = uploadable.routeOptions;
const { apiKey, appId } = yield* UTToken;
const ingestUrl = yield* IngestUrl(preferredRegion);
const isDev = yield* IsDevelopment;
yield* effect_Effect.logDebug("Generating presigned URLs").pipe(effect_Effect.annotateLogs("fileUploadRequests", fileUploadRequests), effect_Effect.annotateLogs("ingestUrl", ingestUrl));
const presignedUrls = yield* effect_Effect.forEach(fileUploadRequests, (file) => effect_Effect.gen(function* () {
const key = yield* (0, __uploadthing_shared.generateKey)(file, appId, routeOptions.getFileHashParts);
const url = yield* (0, __uploadthing_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* __effect_platform_HttpServerRequest.HttpServerRequest;
const requestUrl = yield* __effect_platform_HttpServerRequest.toURL(serverReq);
const devHookRequest = yield* effect_Config.string("callbackUrl").pipe(effect_Config.withDefault(requestUrl.origin + requestUrl.pathname), effect_Effect.map((url) => __effect_platform_HttpClientRequest.post(url).pipe(__effect_platform_HttpClientRequest.appendUrlParam("slug", slug))));
const metadataRequest = __effect_platform_HttpClientRequest.post("/route-metadata").pipe(__effect_platform_HttpClientRequest.prependUrl(ingestUrl), __effect_platform_HttpClientRequest.setHeaders({
"x-uploadthing-api-key": effect_Redacted.value(apiKey),
"x-uploadthing-version": require_package.version,
"x-uploadthing-be-adapter": beAdapter,
"x-uploadthing-fe-package": fePackage
}), __effect_platform_HttpClientRequest.bodyJson({
fileKeys: presignedUrls.map(({ key }) => key),
metadata,
isDev,
callbackUrl: devHookRequest.url,
callbackSlug: slug,
awaitServerData: routeOptions.awaitServerData ?? true
}), effect_Effect.flatMap(httpClient.execute));
const handleDevStreamError = effect_Effect.fn("handleDevStreamError")(function* (err, chunk) {
const schema = effect_Schema.parseJson(effect_Schema.Struct({ file: require_shared_schemas.UploadedFileData }));
const parsedChunk = yield* effect_Schema.decodeUnknown(schema)(chunk);
const key = parsedChunk.file.key;
yield* effect_Effect.logError("Failed to forward callback request from dev stream").pipe(effect_Effect.annotateLogs({
fileKey: key,
error: err.message
}));
const httpResponse = yield* __effect_platform_HttpClientRequest.post("/callback-result").pipe(__effect_platform_HttpClientRequest.prependUrl(ingestUrl), __effect_platform_HttpClientRequest.setHeaders({
"x-uploadthing-api-key": effect_Redacted.value(apiKey),
"x-uploadthing-version": require_package.version,
"x-uploadthing-be-adapter": beAdapter,
"x-uploadthing-fe-package": fePackage
}), __effect_platform_HttpClientRequest.bodyJson({
fileKey: key,
error: `Failed to forward callback request from dev stream: ${err.message}`
}), effect_Effect.flatMap(httpClient.execute));
yield* logHttpClientResponse("Reported callback error to UploadThing")(httpResponse);
});
const fiber = yield* effect_Effect.if(isDev, {
onTrue: () => metadataRequest.pipe(effect_Effect.tapBoth({
onSuccess: logHttpClientResponse("Registered metadata", { mixin: "None" }),
onFailure: logHttpClientError("Failed to register metadata")
}), __effect_platform_HttpClientResponse.stream, handleJsonLineStream(require_shared_schemas.MetadataFetchStreamPart, (chunk) => devHookRequest.pipe(__effect_platform_HttpClientRequest.setHeaders({
"uploadthing-hook": chunk.hook,
"x-uploadthing-signature": chunk.signature
}), __effect_platform_HttpClientRequest.setBody(__effect_platform_HttpBody.text(chunk.payload, "application/json")), httpClient.execute, effect_Effect.tap(logHttpClientResponse("Successfully forwarded callback request from dev stream")), effect_Effect.catchTag("ResponseError", (err) => handleDevStreamError(err, chunk.payload)), effect_Effect.annotateLogs(chunk), effect_Effect.asVoid, effect_Effect.ignoreLogged, effect_Effect.scoped))),
onFalse: () => metadataRequest.pipe(effect_Effect.tapBoth({
onSuccess: logHttpClientResponse("Registered metadata"),
onFailure: logHttpClientError("Failed to register metadata")
}), effect_Effect.flatMap(__effect_platform_HttpClientResponse.schemaBodyJson(require_shared_schemas.MetadataFetchResponse)), effect_Effect.scoped)
}).pipe(effect_Effect.forkDaemon);
const presigneds = presignedUrls.map((p, i) => ({
url: p.url,
key: p.key,
name: fileUploadRequests[i].name,
customId: fileUploadRequests[i].customId ?? null
}));
yield* effect_Effect.logInfo("Sending presigned URLs to client").pipe(effect_Effect.annotateLogs("presignedUrls", presigneds));
return {
body: presigneds,
fiber
};
}).pipe(effect_Effect.withLogSpan("handleUploadAction"));
//#endregion
//#region src/_internal/upload-builder.ts
function internalCreateBuilder(initDef = {}) {
const _def = {
$types: {},
routerConfig: { image: { maxFileSize: "4MB" } },
routeOptions: { awaitServerData: true },
inputParser: {
parseAsync: () => Promise.resolve(void 0),
_input: void 0,
_output: void 0
},
middleware: () => ({}),
onUploadError: () => {},
onUploadComplete: () => void 0,
errorFormatter: initDef.errorFormatter ?? defaultErrorFormatter,
...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
});
}
};
}
/**
* Create a builder for your backend adapter.
* Refer to the existing adapters for examples on how to use this function.
* @public
*
* @param opts - Options for the builder
* @returns A file route builder for making UploadThing file routes
*/
function createBuilder(opts) {
return (input, config) => {
return internalCreateBuilder({
routerConfig: input,
routeOptions: config ?? {},
...opts
});
};
}
//#endregion
Object.defineProperty(exports, 'AdapterArguments', {
enumerable: true,
get: function () {
return AdapterArguments;
}
});
Object.defineProperty(exports, 'ApiUrl', {
enumerable: true,
get: function () {
return ApiUrl;
}
});
Object.defineProperty(exports, 'IngestUrl', {
enumerable: true,
get: function () {
return IngestUrl;
}
});
Object.defineProperty(exports, 'UTFiles', {
enumerable: true,
get: function () {
return UTFiles;
}
});
Object.defineProperty(exports, 'UTRegion', {
enumerable: true,
get: function () {
return UTRegion;
}
});
Object.defineProperty(exports, 'UTToken', {
enumerable: true,
get: function () {
return UTToken;
}
});
Object.defineProperty(exports, 'UfsAppIdLocation', {
enumerable: true,
get: function () {
return UfsAppIdLocation;
}
});
Object.defineProperty(exports, 'UfsHost', {
enumerable: true,
get: function () {
return UfsHost;
}
});
Object.defineProperty(exports, 'configProvider', {
enumerable: true,
get: function () {
return configProvider;
}
});
Object.defineProperty(exports, 'createBuilder', {
enumerable: true,
get: function () {
return createBuilder;
}
});
Object.defineProperty(exports, 'createRequestHandler', {
enumerable: true,
get: function () {
return createRequestHandler;
}
});
Object.defineProperty(exports, 'extractRouterConfig', {
enumerable: true,
get: function () {
return extractRouterConfig;
}
});
Object.defineProperty(exports, 'logHttpClientError', {
enumerable: true,
get: function () {
return logHttpClientError;
}
});
Object.defineProperty(exports, 'logHttpClientResponse', {
enumerable: true,
get: function () {
return logHttpClientResponse;
}
});
Object.defineProperty(exports, 'makeAdapterHandler', {
enumerable: true,
get: function () {
return makeAdapterHandler;
}
});
Object.defineProperty(exports, 'makeRuntime', {
enumerable: true,
get: function () {
return makeRuntime;
}
});