'use strict'; var zod = require('zod'); // src/types/web-handler.ts var EdgeSpecResponse = class { constructor(options = {}) { this.options = options; } statusCode() { return this.options.status ?? 200; } status(status) { this.options.status = status; return this; } header(key, value) { this.options.headers = mergeHeaders(this.options.headers, { [key]: value }); return this; } headers(headers) { this.options.headers = mergeHeaders(this.options.headers, headers); return this; } statusText(statusText) { this.options.statusText = statusText; return this; } static json(...args) { return new EdgeSpecJsonResponse(...args); } static multipartFormData(...args) { return new EdgeSpecMultiPartFormDataResponse(...args); } static custom(...args) { return new EdgeSpecCustomResponse(...args); } }; var EdgeSpecJsonResponse = class extends EdgeSpecResponse { constructor(data, options = {}) { super(options); this.data = data; this.options.headers = mergeHeaders(this.options.headers, { "Content-Type": "application/json" }); } serializeToResponse(schema) { return new Response(JSON.stringify(schema.parse(this.data)), this.options); } }; var EdgeSpecCustomResponse = class extends EdgeSpecResponse { constructor(data, contentType, options = {}) { super(options); this.data = data; this.contentType = contentType; this.options.headers = mergeHeaders(this.options.headers, { "Content-Type": contentType }); } serializeToResponse(schema) { return new Response(schema.parse(this.data), this.options); } }; var EdgeSpecMultiPartFormDataResponse = class extends EdgeSpecResponse { constructor(data, options = {}) { super(options); this.data = data; this.options.headers = mergeHeaders(this.options.headers, { "Content-Type": "multipart/form-data" }); } serializeToResponse(schema) { const formData = new FormData(); for (const [key, value] of Object.entries(schema.parse(this.data))) { formData.append(key, value instanceof Blob ? value : String(value)); } return new Response(formData, this.options); } }; function createEdgeSpecRequest(request, options) { return Object.assign(request, options); } function mergeHeaders(h1, h2) { return new Headers( Object.fromEntries([ ...h1 instanceof Headers ? h1 : new Headers(h1 ?? void 0).entries() ?? [], ...h2 instanceof Headers ? h2 : new Headers(h2 ?? void 0).entries() ?? [] ]) ); } // src/lib/format-zod-error.ts var zodIssueToString = (issue) => { if (issue.path.join(".") === "") { return issue.message; } if (issue.message === "Required") { return `\`${issue.path.join(".")}\` is required`; } return `${issue.message} for "${issue.path.join(".")}"`; }; var formatZodError = (error) => { let message; if (error.issues.length === 1) { const issue = error.issues[0]; message = zodIssueToString(issue); } else { const message_components = []; for (const issue of error.issues) { message_components.push(zodIssueToString(issue)); } message = `${error.issues.length} Zod validation issues: ` + message_components.join(", "); } message += `. Full Zod error: ${JSON.stringify(error.issues, null, 2)}`; return message; }; // src/middleware/http-exceptions.ts var EdgeSpecMiddlewareError = class extends Error { constructor(message, status = 500) { super(message); this.message = message; this.status = status; this._isHttpException = true; this.name = this.constructor.name; } }; var MethodNotAllowedError = class extends EdgeSpecMiddlewareError { constructor(allowedMethods) { super(`only ${allowedMethods.join(",")} accepted`, 405); } }; var BadRequestError = class extends EdgeSpecMiddlewareError { constructor(message) { super(message, 400); } }; var InvalidQueryParamsError = class extends BadRequestError { constructor(message) { super(message); } }; var InvalidContentTypeError = class extends BadRequestError { constructor(message) { super(message); } }; var InputParsingError = class extends BadRequestError { constructor(message) { super(message); } }; var InputValidationError = class extends BadRequestError { constructor(error) { super(formatZodError(error)); } }; var ResponseValidationError = class extends EdgeSpecMiddlewareError { constructor(error) { super(formatZodError(error), 500); } }; // src/middleware/with-methods.ts var withMethods = (methods) => (req, ctx, next) => { if (!methods.includes(req.method)) { throw new MethodNotAllowedError(methods); } return next(req, ctx); }; var getZodObjectSchemaFromZodEffectSchema = (isZodEffect, schema) => { if (!isZodEffect) { return schema; } let currentSchema = schema; while (currentSchema instanceof zod.z.ZodEffects) { currentSchema = currentSchema._def.schema; } return currentSchema; }; var getZodDefFromZodSchemaHelpers = (schema) => { const special_zod_types = [ zod.ZodFirstPartyTypeKind.ZodOptional, zod.ZodFirstPartyTypeKind.ZodDefault, zod.ZodFirstPartyTypeKind.ZodEffects ]; while (special_zod_types.includes(schema._def.typeName)) { if (schema._def.typeName === zod.ZodFirstPartyTypeKind.ZodOptional || schema._def.typeName === zod.ZodFirstPartyTypeKind.ZodDefault) { schema = schema._def.innerType; continue; } if (schema._def.typeName === zod.ZodFirstPartyTypeKind.ZodEffects) { schema = schema._def.schema; continue; } } return schema._def; }; var tryGetZodSchemaAsObject = (schema) => { const isZodEffect = schema._def.typeName === zod.ZodFirstPartyTypeKind.ZodEffects; const safe_schema = getZodObjectSchemaFromZodEffectSchema(isZodEffect, schema); const isZodObject = safe_schema._def.typeName === zod.ZodFirstPartyTypeKind.ZodObject; if (!isZodObject) { return void 0; } return safe_schema; }; var isZodSchemaArray = (schema) => { const def = getZodDefFromZodSchemaHelpers(schema); return def.typeName === zod.ZodFirstPartyTypeKind.ZodArray; }; var isZodSchemaBoolean = (schema) => { const def = getZodDefFromZodSchemaHelpers(schema); return def.typeName === zod.ZodFirstPartyTypeKind.ZodBoolean; }; var parseQueryParams = (schema, input, supportedArrayFormats) => { const parsed_input = Object.assign({}, input); const obj_schema = tryGetZodSchemaAsObject(schema); if (obj_schema) { for (const [key, value] of Object.entries(obj_schema.shape)) { if (isZodSchemaArray(value)) { const array_input = input[key]; if (typeof array_input === "string" && supportedArrayFormats.includes("comma")) { parsed_input[key] = array_input.split(","); } const bracket_syntax_array_input = input[`${key}[]`]; if (typeof bracket_syntax_array_input === "string" && supportedArrayFormats.includes("brackets")) { const pre_split_array = bracket_syntax_array_input; parsed_input[key] = pre_split_array.split(","); } if (Array.isArray(bracket_syntax_array_input) && supportedArrayFormats.includes("brackets")) { parsed_input[key] = bracket_syntax_array_input; } continue; } if (isZodSchemaBoolean(value)) { const boolean_input = input[key]; if (typeof boolean_input === "string") { parsed_input[key] = boolean_input === "true"; } } } } return schema.parse(parsed_input); }; var validateQueryParams = (inputUrl, schema, supportedArrayFormats) => { const url = new URL(inputUrl, "http://dummy.com"); const seenKeys = /* @__PURE__ */ new Set(); const obj_schema = tryGetZodSchemaAsObject(schema); if (!obj_schema) { return; } for (const key of url.searchParams.keys()) { for (const [schemaKey, value] of Object.entries(obj_schema.shape)) { if (isZodSchemaArray(value)) { if (key === `${schemaKey}[]` && !supportedArrayFormats.includes("brackets")) { throw new InvalidQueryParamsError( `Bracket syntax not supported for query param "${schemaKey}"` ); } } } const key_schema = obj_schema.shape[key]; if (key_schema) { if (isZodSchemaArray(key_schema)) { if (seenKeys.has(key) && !supportedArrayFormats.includes("repeat")) { throw new InvalidQueryParamsError( `Repeated parameters not supported for duplicate query param "${key}"` ); } } } seenKeys.add(key); } }; var withInputValidation = (input) => async (req, ctx, next) => { const { supportedArrayFormats } = input; if (input.formData && input.jsonBody || input.formData && input.commonParams) { throw new Error("Cannot use formData with jsonBody or commonParams"); } if ((req.method === "POST" || req.method === "PATCH") && (input.jsonBody || input.commonParams) && !req.headers.get("content-type")?.includes("application/json")) { throw new InvalidContentTypeError( `${req.method} requests must have Content-Type header with "application/json"` ); } if (input.urlEncodedFormData && req.method !== "GET" && !req.headers.get("content-type")?.includes("application/x-www-form-urlencoded")) { throw new InvalidContentTypeError( `Must have Content-Type header with "application/x-www-form-urlencoded"` ); } if (input.formData && (req.method === "POST" || req.method === "PATCH") && !req.headers.get("content-type")?.includes("multipart/form-data")) { throw new InvalidContentTypeError( `${req.method} requests must have Content-Type header with "multipart/form-data"` ); } const originalParams = Object.fromEntries( new URL(req.url).searchParams.entries() ); let jsonBody; if (input.jsonBody || input.commonParams) { try { jsonBody = await req.clone().json(); } catch (e) { if (!input.jsonBody?.isOptional()) { throw new InputParsingError("Error while parsing JSON body"); } } } let multiPartFormData = void 0; if (input.formData) { try { multiPartFormData = await req.clone().formData(); multiPartFormData = Object.fromEntries(multiPartFormData.entries()); } catch (e) { if (!input.formData?.isOptional()) { throw new InputParsingError("Error while parsing form data"); } } } let urlEncodedFormData = void 0; if (input.urlEncodedFormData) { try { const params = new URLSearchParams(await req.clone().text()); urlEncodedFormData = Object.fromEntries(params.entries()); } catch (e) { if (!input.urlEncodedFormData?.isOptional()) { throw new InputParsingError( "Error while parsing url encoded form data" ); } } } try { const originalCombinedParams = { ...originalParams, ...typeof jsonBody === "object" ? jsonBody : {} }; const willValidateRequestBody = !["GET", "DELETE", "HEAD"].includes( req.method ); if (Boolean(input.formData) && willValidateRequestBody) { req.multiPartFormData = input.formData?.parse(multiPartFormData); } if (Boolean(input.jsonBody) && willValidateRequestBody) { req.jsonBody = input.jsonBody?.parse(jsonBody); } if (Boolean(input.urlEncodedFormData) && willValidateRequestBody) { req.urlEncodedFormData = input.urlEncodedFormData?.parse(urlEncodedFormData); } if (Boolean(input.routeParams) && "routeParams" in req) { req.routeParams = input.routeParams?.parse(req.routeParams); } if (input.queryParams) { if (!req.url) { throw new Error("req.url is undefined"); } validateQueryParams(req.url, input.queryParams, supportedArrayFormats); req.query = parseQueryParams( input.queryParams, originalParams, supportedArrayFormats ); } if (input.commonParams) { req.commonParams = parseQueryParams( input.commonParams, originalCombinedParams, supportedArrayFormats ); } } catch (error) { if (error instanceof BadRequestError) { throw error; } if (error instanceof zod.ZodError) { throw new InputValidationError(error); } throw new InputParsingError("Error while parsing input"); } return next(req, ctx); }; // src/middleware/with-unhandled-exception-handling.ts var withUnhandledExceptionHandling = async (req, ctx, next) => { try { return await next(req, ctx); } catch (e) { const logger = ctx.logger ?? console; if ("_isHttpException" in e) { logger.warn( "Caught unhandled HTTP exception thrown by EdgeSpec provided middleware. Consider adding createWithDefaultExceptionHandling middleware to your global or route spec." ); } else { logger.warn( "Caught unknown unhandled exception; consider adding a exception handling middleware to your global or route spec." ); } logger.error(e); return new Response(null, { status: 500 }); } }; // src/create-with-edge-spec.ts var attachMetadataToRouteFn = ({ globalSpec, routeSpec }, routeFn) => { routeFn._globalSpec = globalSpec; routeFn._routeSpec = routeSpec; return routeFn; }; var createWithEdgeSpec = (globalSpec) => { return (routeSpec) => (routeFn) => attachMetadataToRouteFn( { globalSpec, routeSpec }, async (request, ctx) => { const onMultipleAuthMiddlewareFailures = globalSpec.onMultipleAuthMiddlewareFailures ?? routeSpec.onMultipleAuthMiddlewareFailures; const supportedAuthMiddlewares = new Set( routeSpec.auth == null || routeSpec.auth === "none" ? [] : Array.isArray(routeSpec.auth) ? routeSpec.auth : [routeSpec.auth] ); const authMiddlewares = Object.entries(globalSpec.authMiddleware).filter(([k, _]) => supportedAuthMiddlewares.has(k)).map(([_, v]) => v); return await wrapMiddlewares( [ // Injected into the VM when running in WinterCG emulation mode // @ts-expect-error ...typeof _injectedEdgeSpecMiddleware !== "undefined" ? ( // @ts-expect-error _injectedEdgeSpecMiddleware ) : [], withUnhandledExceptionHandling, // this serializes responses that are returned by middleware WITHOUT // validating them against the routeSpec // // this allows returning EdgeSpecResponse.json or ctx.json in a // middleware, instead of having to return a raw Response // // this is needed, for instance, when an error middleware returns an // error response that does not match the routeSpec's response shape serializeResponse(globalSpec, routeSpec, false), ...globalSpec.beforeAuthMiddleware ?? [], firstAuthMiddlewareThatSucceeds( authMiddlewares, onMultipleAuthMiddlewareFailures ), ...globalSpec.afterAuthMiddleware ?? [], ...routeSpec.middleware ?? [], withMethods(routeSpec.methods), withInputValidation({ supportedArrayFormats: globalSpec.supportedArrayFormats ?? [ "brackets", "comma", "repeat" ], commonParams: routeSpec.commonParams, formData: routeSpec.multiPartFormData, jsonBody: routeSpec.jsonBody, queryParams: routeSpec.queryParams, routeParams: routeSpec.routeParams, urlEncodedFormData: routeSpec.urlEncodedFormData }), // this serializes responses that are returned by the route function, // validating them against the routeSpec serializeResponse(globalSpec, routeSpec) ], routeFn, request, ctx ); } ); }; function serializeResponse(globalSpec, routeSpec, skipValidation = false) { return async (req, ctx, next) => { const rawResponse = await next(req, ctx); const statusCode = rawResponse instanceof EdgeSpecResponse ? rawResponse.statusCode() : rawResponse.status; const isSuccess = statusCode >= 200 && statusCode < 300; try { const response = serializeToResponse( isSuccess && !skipValidation && (globalSpec.shouldValidateResponses ?? true), routeSpec, rawResponse ); return response; } catch (err) { throw new ResponseValidationError(err); } }; } async function wrapMiddlewares(middlewares, routeFn, request, ctx) { return await middlewares.reduceRight( (next, middleware) => { return async (req, ctx2) => { return middleware(req, ctx2, next); }; }, async (request2, ctx2) => routeFn(request2, ctx2) )(request, ctx); } function serializeToResponse(shouldValidateResponse, routeSpec, response) { if (!shouldValidateResponse) { return "serializeToResponse" in response ? response.serializeToResponse(zod.z.any()) : response; } if (response instanceof EdgeSpecResponse) { if (response instanceof EdgeSpecJsonResponse) { return response.serializeToResponse(routeSpec.jsonResponse ?? zod.z.any()); } if (response instanceof EdgeSpecMultiPartFormDataResponse) { return response.serializeToResponse( routeSpec.multipartFormDataResponse ?? zod.z.any() ); } if (response instanceof EdgeSpecCustomResponse) { return response.serializeToResponse(zod.z.any()); } } if ("serializeToResponse" in response) { throw new Error("Unknown Response type"); } return response; } function firstAuthMiddlewareThatSucceeds(authMiddlewares, onMultipleAuthMiddlewareFailures) { return async (req, ctx, next) => { if (authMiddlewares.length === 0) { return next(req, ctx); } let errors = []; let didAuthMiddlewareThrow = true; for (const middleware of authMiddlewares) { try { return await middleware(req, ctx, (...args) => { didAuthMiddlewareThrow = false; return next(...args); }); } catch (error) { if (didAuthMiddlewareThrow) { errors.push(error); continue; } else { throw error; } } } if (onMultipleAuthMiddlewareFailures && didAuthMiddlewareThrow) { onMultipleAuthMiddlewareFailures(errors); } throw errors[errors.length - 1]; }; } // src/types/context.ts var DEFAULT_CONTEXT = { json: EdgeSpecResponse.json, multipartFormData: EdgeSpecResponse.multipartFormData, custom: EdgeSpecResponse.custom }; var getDefaultContext = () => ({ ...DEFAULT_CONTEXT }); // src/types/edge-spec.ts function makeRequestAgainstEdgeSpec(edgeSpec, options = {}) { return async (request) => { const { routeMatcher, routeMapWithHandlers, handle404 = () => new Response("Not found", { status: 404 }) } = edgeSpec; const { removePathnamePrefix, automaticallyRemovePathnamePrefix = true } = options; let pathname = new URL(request.url).pathname; if (removePathnamePrefix) { if (automaticallyRemovePathnamePrefix) { throw new Error( "automaticallyRemovePathnamePrefix and removePathnamePrefix cannot both be specified" ); } pathname = pathname.replace(removePathnamePrefix, ""); } else { if (request.routeParams) { const routeParams2 = request.routeParams; const wildcardRouteParameters = Object.values(routeParams2).filter( (p) => Array.isArray(p) ); if (wildcardRouteParameters.length === 0) { throw new Error("No wildcard route parameters found"); } if (wildcardRouteParameters.length > 1) { throw new Error("Only one wildcard route parameter is supported"); } const wildcardRouteParameter = wildcardRouteParameters[0]; pathname = `/${wildcardRouteParameter.join("/")}`; } } const { matchedRoute, routeParams } = routeMatcher(pathname) ?? {}; let routeFn = matchedRoute && routeMapWithHandlers[matchedRoute]; const edgeSpecRequest = createEdgeSpecRequest(request, { edgeSpec, routeParams: routeParams ?? {} }); if (!routeFn) { return await handle404(edgeSpecRequest, getDefaultContext()); } return wrapMiddlewares( options.middleware ?? [], routeFn, edgeSpecRequest, getDefaultContext() ); }; } var edgeSpecConfigSchema = zod.z.object({ /** * Defaults to the current working directory. */ rootDirectory: zod.z.string().optional(), /** * If this path is relative, it's resolved relative to the `rootDirectory` option. */ tsconfigPath: zod.z.string().optional(), /** * If this path is relative, it's resolved relative to the `rootDirectory` option. */ routesDirectory: zod.z.string().optional(), /** * The platform you're targeting. * * Defaults to `wintercg-minimal`, and you should use this whenever possible for maximal compatibility. * * Check [the docs](https://github.com/seamapi/edgespec/blob/main/docs/edgespec-config.md) for more information. */ platform: zod.z.enum(["node", "wintercg-minimal"]).default("wintercg-minimal").optional() }).strict(); var defineConfig = (config) => { const parsedConfig = edgeSpecConfigSchema.safeParse(config); if (parsedConfig.success) { return parsedConfig.data; } throw new Error(`Invalid config: ${parsedConfig.error}`); }; // src/helpers.ts var loadBundle = async (bundlePath) => { const bundle = await import(bundlePath); return bundle.default.default ?? bundle.default; }; exports.EdgeSpecCustomResponse = EdgeSpecCustomResponse; exports.EdgeSpecJsonResponse = EdgeSpecJsonResponse; exports.EdgeSpecResponse = EdgeSpecResponse; exports.createWithEdgeSpec = createWithEdgeSpec; exports.defineConfig = defineConfig; exports.loadBundle = loadBundle; exports.makeRequestAgainstEdgeSpec = makeRequestAgainstEdgeSpec; exports.wrapMiddlewares = wrapMiddlewares; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map