@gaganref/confect-server 10.3.1-fork.72 → 10.3.1-fork.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/HttpRouter.d.ts +1 -0
- package/dist/HttpRouter.d.ts.map +1 -1
- package/dist/HttpRouter.js +13 -3
- package/dist/HttpRouter.js.map +1 -1
- package/dist/internal/httpBody.d.ts +13 -0
- package/dist/internal/httpBody.d.ts.map +1 -0
- package/dist/internal/httpBody.js +47 -0
- package/dist/internal/httpBody.js.map +1 -0
- package/dist/internal/httpOptions.d.ts +4 -0
- package/dist/internal/httpOptions.d.ts.map +1 -0
- package/dist/internal/httpOptions.js +83 -0
- package/dist/internal/httpOptions.js.map +1 -0
- package/package.json +2 -2
- package/src/HttpRouter.ts +15 -1
- package/src/internal/httpBody.ts +72 -0
- package/src/internal/httpOptions.ts +154 -0
package/dist/HttpRouter.d.ts
CHANGED
|
@@ -79,4 +79,5 @@ export declare const forSchema: <DatabaseSchema_ extends DatabaseSchema.AnyWithP
|
|
|
79
79
|
readonly observability?: Layer.Layer<never> | undefined;
|
|
80
80
|
readonly api?: HttpApi.HttpApi<Id, Groups> | undefined;
|
|
81
81
|
}) => ConvexHttpRouter;
|
|
82
|
+
export { withBodyLimit } from "./internal/httpBody.js";
|
|
82
83
|
//# sourceMappingURL=HttpRouter.d.ts.map
|
package/dist/HttpRouter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpRouter.d.ts","sourceRoot":"","sources":["../src/HttpRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,IAAI,gBAAgB,EAOpC,MAAM,eAAe,CAAC;AAMvB,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,KAAK,KAAK,IAAI,MAAM,aAAa,CAAC;AAIzC,OAAO,KAAK,OAAO,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,KAAK,YAAY,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,KAAK,YAAY,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAIrD,OAAO,KAAK,KAAK,YAAY,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,KAAK,OAAO,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"HttpRouter.d.ts","sourceRoot":"","sources":["../src/HttpRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,IAAI,gBAAgB,EAOpC,MAAM,eAAe,CAAC;AAMvB,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,KAAK,KAAK,IAAI,MAAM,aAAa,CAAC;AAIzC,OAAO,KAAK,OAAO,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,KAAK,YAAY,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,KAAK,YAAY,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAIrD,OAAO,KAAK,KAAK,YAAY,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,KAAK,OAAO,MAAM,cAAc,CAAC;AAK7C,OAAO,KAAK,KAAK,cAAc,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAErD,OAAO,KAAK,KAAK,SAAS,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAChB,WAAW,CAAC,WAAW,GACvB,cAAc,CAAC,cAAc,GAC7B,YAAY,CAAC,YAAY,GACzB,SAAS,CAAC,SAAS,GACnB,IAAI,CAAC,IAAI,GACT,iBAAiB,GACjB,eAAe,GACf,OAAO,GACP,aAAa,GACb,aAAa,GACb,mBAAmB,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,MAAM,CAAC,SAAS,GAAG,QAAQ,IAAI,KAAK,CAAC,KAAK,CACpD,KAAK,EACL,KAAK,EACH,UAAU,CAAC,UAAU,GACrB,IAAI,CAAC,SAAS,GACd,UAAU,CAAC,UAAU,GACrB,YAAY,CAAC,YAAY,GACzB,IAAI,CAAC,IAAI,GACT,UAAU,CAAC,OAAO,CAAC,UAAU,EAAE,SAAS,CAAC,GACzC,UAAU,CAAC,OAAO,CAAC,gBAAgB,EAAE,SAAS,CAAC,GAC/C,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,GAChC,UAAU,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CACzC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,IAAI,GAAI,EAAE,SAAS,MAAM,EAAE,MAAM,SAAS,YAAY,CAAC,UAAU,UACpE,MAAM,YACJ;IACR,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CACxD,KACA,gBAMA,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,SAAS,GACnB,eAAe,SAAS,cAAc,CAAC,YAAY,UAC1C,eAAe,MAExB,EAAE,SAAS,MAAM,EAAE,MAAM,SAAS,YAAY,CAAC,UAAU,UAChD,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC,YAC7C;IACR,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CACxD,KACA,gBAkBA,CAAC;AA4RN,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/HttpRouter.js
CHANGED
|
@@ -3,6 +3,8 @@ import { layer } from "./ConvexConfigProvider.js";
|
|
|
3
3
|
import { layer as layer$1 } from "./ConvexLogger.js";
|
|
4
4
|
import { memoized } from "./internal/observability.js";
|
|
5
5
|
import { actionLayer, baseActionLayer } from "./RegisteredFunction.js";
|
|
6
|
+
import { withOptions } from "./internal/httpOptions.js";
|
|
7
|
+
import { withBodyLimit } from "./internal/httpBody.js";
|
|
6
8
|
import * as Context from "effect/Context";
|
|
7
9
|
import * as Effect from "effect/Effect";
|
|
8
10
|
import * as Layer from "effect/Layer";
|
|
@@ -21,7 +23,8 @@ import * as HttpServer from "effect/http/HttpServer";
|
|
|
21
23
|
//#region src/HttpRouter.ts
|
|
22
24
|
var HttpRouter_exports = /* @__PURE__ */ __exportAll({
|
|
23
25
|
forSchema: () => forSchema,
|
|
24
|
-
make: () => make
|
|
26
|
+
make: () => make,
|
|
27
|
+
withBodyLimit: () => withBodyLimit
|
|
25
28
|
});
|
|
26
29
|
/**
|
|
27
30
|
* Create a Convex HTTP router serving the given routes.
|
|
@@ -60,7 +63,14 @@ const makeWithLayer = (routes, layer$2, api, observability, siteRelative = false
|
|
|
60
63
|
const prefix = siteRelative ? (yield* Config.schema(Schema.URLFromString, "CONVEX_SITE_URL")).pathname.replace(/\/$/, "") : "";
|
|
61
64
|
return (request) => convexHttpRouter.lookup(new URL(request.originalUrl).pathname.slice(prefix.length) || "/", request.method)?.[2] ?? "/*";
|
|
62
65
|
}).pipe(Effect.orDie));
|
|
63
|
-
const
|
|
66
|
+
const endpoints = /* @__PURE__ */ new Set();
|
|
67
|
+
if (api !== void 0) HttpApi.reflect(api, {
|
|
68
|
+
onGroup: () => {},
|
|
69
|
+
onEndpoint: ({ endpoint }) => {
|
|
70
|
+
endpoints.add(`${endpoint.method} ${apiRouterPath(endpoint.path, endpoint.params)}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const AppLayer = withOptions(routes, endpoints).pipe(Layer.provideMerge(observability === void 0 ? layer$1 : Layer.merge(memoized(observability), spanName)), Layer.provideMerge(layer), Layer.provide(HttpServer.layerServices));
|
|
64
74
|
const { handler } = HttpRouter.toWebHandler(AppLayer, {
|
|
65
75
|
disableLogger: true,
|
|
66
76
|
...observability === void 0 ? {} : { middleware: (effect) => Effect.andThen(Effect.annotateCurrentSpan("kind", "httpAction"), effect) }
|
|
@@ -199,6 +209,6 @@ const applyMonkeyPatches = () => {
|
|
|
199
209
|
});
|
|
200
210
|
};
|
|
201
211
|
//#endregion
|
|
202
|
-
export { HttpRouter_exports, forSchema, make };
|
|
212
|
+
export { HttpRouter_exports, forSchema, make, withBodyLimit };
|
|
203
213
|
|
|
204
214
|
//# sourceMappingURL=HttpRouter.js.map
|
package/dist/HttpRouter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpRouter.js","names":["RegisteredFunction.baseActionLayer","RegisteredFunction.actionLayer","ConvexLogger.layer","ObservabilityRuntime.memoized","ConvexConfigProvider.layer","layer"],"sources":["../src/HttpRouter.ts"],"sourcesContent":["import {\n type HttpRouter as ConvexHttpRouter,\n httpActionGeneric,\n httpRouter,\n ROUTABLE_HTTP_METHODS,\n type RouteSpecWithPathPrefix,\n type GenericActionCtx,\n type GenericDataModel,\n} from \"convex/server\";\nimport * as Array from \"effect/Array\";\nimport * as Console from \"effect/Console\";\nimport * as Context from \"effect/Context\";\nimport * as Config from \"effect/Config\";\nimport * as Effect from \"effect/Effect\";\nimport type * as FileSystem from \"effect/FileSystem\";\nimport * as Layer from \"effect/Layer\";\nimport * as Match from \"effect/Match\";\nimport type * as Path from \"effect/Path\";\nimport * as Schema from \"effect/Schema\";\nimport * as SchemaAST from \"effect/SchemaAST\";\nimport * as FindMyWay from \"effect/http/FindMyWay\";\nimport * as HttpApi from \"effect/http-api/HttpApi\";\nimport type * as HttpApiGroup from \"effect/http-api/HttpApiGroup\";\nimport type * as Etag from \"effect/http/Etag\";\nimport type * as HttpPlatform from \"effect/http/HttpPlatform\";\nimport * as HttpRouter from \"effect/http/HttpRouter\";\nimport * as HttpMiddleware from \"effect/http/HttpMiddleware\";\nimport type * as HttpServerRequest from \"effect/http/HttpServerRequest\";\nimport * as HttpServer from \"effect/http/HttpServer\";\nimport type * as ActionRunner from \"./ActionRunner.js\";\nimport type * as Auth from \"./Auth.js\";\nimport type { ExecutionMetadata } from \"./ExecutionMetadata.js\";\nimport type { RequestMetadata } from \"./RequestMetadata.js\";\nimport type * as DatabaseSchema from \"./DatabaseSchema.js\";\nimport type * as DataModel from \"./DataModel.js\";\nimport type * as Handler from \"./Handler.js\";\nimport * as ConvexConfigProvider from \"./ConvexConfigProvider.js\";\nimport * as ObservabilityRuntime from \"./internal/observability.js\";\nimport * as ConvexLogger from \"./ConvexLogger.js\";\nimport type * as MutationRunner from \"./MutationRunner.js\";\nimport type * as QueryRunner from \"./QueryRunner.js\";\nimport * as RegisteredFunction from \"./RegisteredFunction.js\";\nimport type * as Scheduler from \"./Scheduler.js\";\nimport type { Storage } from \"./Storage.js\";\nimport type { StorageActionWriter } from \"./StorageActionWriter.js\";\nimport type { StorageReader } from \"./StorageReader.js\";\nimport type { StorageWriter } from \"./StorageWriter.js\";\n\n/**\n * The Confect services available to HTTP route handlers and middleware,\n * supplied per request from the Convex action context.\n */\nexport type Services =\n | QueryRunner.QueryRunner\n | MutationRunner.MutationRunner\n | ActionRunner.ActionRunner\n | Scheduler.Scheduler\n | Auth.Auth\n | ExecutionMetadata\n | RequestMetadata\n | Storage\n | StorageReader\n | StorageWriter\n | StorageActionWriter;\n\n/**\n * A layer that registers routes on the HTTP router—the input to {@link make}.\n *\n * Compose it from Effect's `effect/http` and `effect/http-api` modules:\n * `HttpApiBuilder.layer(api)` registers an `HttpApi`'s endpoints (provide its\n * group handler layers with `Layer.provide`), `HttpApiScalar.layer(api, ...)`\n * serves interactive API docs, `HttpRouter.add` registers a plain route, and\n * `HttpRouter.middleware(fn, { global: true })` applies middleware to every\n * route. Merge any number of these with `Layer.mergeAll`.\n *\n * Route handlers and middleware may require any of the Confect {@link Services},\n * which surface as request-level `Requires` markers and are supplied per\n * request. Anything else the layer requires is a type error—notably, an\n * `HttpApiBuilder.layer(api)` whose group handler layers are not provided\n * leaves an `HttpApiGroup.ToService` requirement behind, so a missing handler\n * group is caught at compile time.\n *\n * Layers are built with Confect's Convex-aware `ConfigProvider` in context, so\n * `Config` reads (e.g. in `Layer.unwrap`) resolve against Convex environment\n * variables.\n */\nexport type Routes<Services_ = Services> = Layer.Layer<\n never,\n never,\n | HttpRouter.HttpRouter\n | Etag.Generator\n | FileSystem.FileSystem\n | HttpPlatform.HttpPlatform\n | Path.Path\n | HttpRouter.Request<\"Requires\", Services_>\n | HttpRouter.Request<\"GlobalRequires\", Services_>\n | HttpRouter.Request<\"Error\", any>\n | HttpRouter.Request<\"GlobalError\", any>\n>;\n\n/**\n * Create a Convex HTTP router serving the given routes.\n *\n * A single catch-all `httpAction` is registered for every routable method under\n * the path prefix `\"/\"`, making the Effect router the single source of truth\n * for paths: place an API with `HttpApi.prefix` or absolute route paths.\n * Requests that match no route receive the Effect router's 404 response.\n *\n * Supply `{ api }` with the `HttpApi` registered by the routes layer to show\n * endpoint patterns in Convex completion logs and the dashboard. Requests that\n * match no reflected endpoint keep the catch-all completion label. Without the\n * option, Convex reports the catch-all registrations. Application endpoints\n * must not overlap component HTTP mounts: Convex also uses dashboard route\n * entries to select between application and component routers.\n *\n * Plain Convex routes can still be added to the returned router—Convex matches\n * exact paths first and longer path prefixes before the catch-all, so they take\n * precedence.\n */\nexport const make = <Id extends string, Groups extends HttpApiGroup.Constraint>(\n routes: Routes,\n options?: {\n readonly observability?: Layer.Layer<never> | undefined;\n readonly api?: HttpApi.HttpApi<Id, Groups> | undefined;\n },\n): ConvexHttpRouter =>\n makeWithLayer(\n routes,\n RegisteredFunction.baseActionLayer,\n options?.api,\n options?.observability,\n );\n\n/**\n * Derive HTTP capabilities and ID scopes from the generated database schema.\n * Component routes are relative to their installation's HTTP mount prefix.\n * Supply `{ api }` as in {@link make}; labels stay component-relative.\n */\nexport const forSchema =\n <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(\n schema: DatabaseSchema_,\n ) =>\n <Id extends string, Groups extends HttpApiGroup.Constraint>(\n routes: Routes<Handler.ActionServices<DatabaseSchema_>>,\n options?: {\n readonly observability?: Layer.Layer<never> | undefined;\n readonly api?: HttpApi.HttpApi<Id, Groups> | undefined;\n },\n ): ConvexHttpRouter =>\n makeWithLayer<\n Handler.ActionServices<DatabaseSchema_>,\n DataModel.ToConvex<DataModel.FromSchema<DatabaseSchema_>>,\n Id,\n Groups\n >(\n Match.value(schema.target.kind).pipe(\n Match.when(\"app\", () => routes),\n Match.when(\"component\", () =>\n routes.pipe(Layer.provide(componentRouter)),\n ),\n Match.exhaustive,\n ),\n (ctx) => RegisteredFunction.actionLayer(schema, ctx),\n options?.api,\n options?.observability,\n schema.target.kind === \"component\",\n );\n\n// Convex uses a component-relative path to select its HTTP action but passes\n// the original request URL to that action. Its CONVEX_SITE_URL includes the\n// installation's full mount prefix (including parent mounts). Prefixing the\n// Effect router preserves relative routes without rewriting the Request body.\nconst componentRouter = Layer.effect(\n HttpRouter.HttpRouter,\n Effect.gen(function* () {\n const router = yield* HttpRouter.HttpRouter;\n const siteUrl = yield* Config.schema(\n Schema.URLFromString,\n \"CONVEX_SITE_URL\",\n );\n return router.prefixed(siteUrl.pathname);\n }).pipe(Effect.orDie),\n);\n\nconst makeWithLayer = <\n Services_,\n DataModel_ extends GenericDataModel,\n Id extends string,\n Groups extends HttpApiGroup.Constraint,\n>(\n routes: Routes<Services_>,\n layer: (ctx: GenericActionCtx<DataModel_>) => Layer.Layer<Services_>,\n api: HttpApi.HttpApi<Id, Groups> | undefined,\n observability?: Layer.Layer<never>,\n siteRelative = false,\n): ConvexHttpRouter => {\n applyMonkeyPatches();\n\n const spanName = Layer.effect(\n HttpMiddleware.SpanNameGenerator,\n Effect.gen(function* () {\n const prefix = siteRelative\n ? (yield* Config.schema(\n Schema.URLFromString,\n \"CONVEX_SITE_URL\",\n )).pathname.replace(/\\/$/, \"\")\n : \"\";\n return (request: HttpServerRequest.HttpServerRequest) =>\n convexHttpRouter.lookup(\n new URL(request.originalUrl).pathname.slice(prefix.length) || \"/\",\n request.method as (typeof ROUTABLE_HTTP_METHODS)[number],\n )?.[2] ?? \"/*\";\n }).pipe(Effect.orDie),\n );\n\n // Provided (not just merged) so that route layers' own construction—e.g.\n // a `Layer.unwrap` reading `Config`—resolves configuration through the\n // Convex-aware provider; merged so that request fibers—endpoint handlers\n // and middleware—do too.\n const AppLayer = routes.pipe(\n Layer.provideMerge(\n observability === undefined\n ? ConvexLogger.layer\n : Layer.merge(ObservabilityRuntime.memoized(observability), spanName),\n ),\n Layer.provideMerge(ConvexConfigProvider.layer),\n Layer.provide(HttpServer.layerServices),\n );\n\n // The Confect service requirements of route handlers and middleware surface\n // as request-level `Requires` markers on the layer, which the web handler\n // exposes as a per-request context argument.\n const { handler } = HttpRouter.toWebHandler(AppLayer, {\n disableLogger: true,\n /* oxlint-disable effecttsgo/any-unknown-in-error-context -- Keep Effect's erased HTTP boundary errors intact. */\n ...(observability === undefined\n ? {}\n : {\n middleware: (effect) =>\n Effect.andThen(\n Effect.annotateCurrentSpan(\"kind\", \"httpAction\"),\n effect,\n ),\n }),\n /* oxlint-enable effecttsgo/any-unknown-in-error-context */\n });\n\n const httpAction = httpActionGeneric((ctx, request): Promise<Response> => {\n // The ctx-backed service layers are all synchronous and finalizer-free,\n // so the scope can close as soon as the context is built.\n // Convex's HTTP registration erases the data model. The supplied generated\n // schema restores it; this does not change the runtime context or services.\n const services = Effect.runSync(\n Effect.scoped(\n Layer.build(layer(ctx as unknown as GenericActionCtx<DataModel_>)),\n ),\n );\n return handler(\n request,\n Context.add(services, Console.Console, globalThis.console),\n );\n });\n\n const convexHttpRouter = httpRouter();\n\n Array.forEach(ROUTABLE_HTTP_METHODS, (method) => {\n const routeSpec: RouteSpecWithPathPrefix = {\n pathPrefix: \"/\",\n method,\n handler: httpAction,\n };\n convexHttpRouter.route(routeSpec);\n });\n\n if (api !== undefined) {\n const matcher = FindMyWay.make<string>();\n const reflected: ReturnType<ConvexHttpRouter[\"getRoutes\"]> = [];\n HttpApi.reflect(api, {\n onGroup: () => {},\n onEndpoint: ({ endpoint }) => {\n const method = ROUTABLE_HTTP_METHODS.find(\n (candidate) => candidate === endpoint.method,\n );\n if (method === undefined) return;\n const path = apiRouterPath(\n endpoint.path as FindMyWay.PathInput,\n endpoint.params,\n );\n matcher.on(method, path, endpoint.path);\n // Effect also matches a wildcard route's base path without its slash.\n if (path.endsWith(\"/*\")) {\n matcher.on(method, path.slice(0, -2) as `/${string}`, endpoint.path);\n }\n reflected.push([endpoint.path, method, httpAction]);\n },\n });\n reflected.sort(\n ([pathA, methodA], [pathB, methodB]) =>\n pathA.localeCompare(pathB) || methodA.localeCompare(methodB),\n );\n\n const getRoutes = convexHttpRouter.getRoutes;\n convexHttpRouter.getRoutes = () =>\n reflected.length === 0\n ? getRoutes()\n : [\n ...getRoutes().filter(([, , action]) => action !== httpAction),\n ...reflected,\n ];\n const lookup = convexHttpRouter.lookup;\n convexHttpRouter.lookup = (path, method) => {\n const match = lookup(path, method);\n if (match === null || match[0] !== httpAction) return match;\n const normalized = match[1];\n const reflectedPath = matcher.find(normalized, path)?.handler;\n return reflectedPath === undefined\n ? match\n : [httpAction, normalized, reflectedPath];\n };\n }\n\n return convexHttpRouter;\n};\n\n/*!\n * apiRouterPath is adapted from Effect 4.0.1 http-api/internal/path.ts.\n *\n * MIT License\n *\n * Copyright (c) 2023 Effectful Technologies Inc\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n// Match Effect's HttpApiBuilder path normalization, which is not publicly\n// exported (effect/http-api/internal/path.ts). Undeclared colons are literals;\n// declared params followed by a literal suffix need an explicit regex boundary.\nconst apiRouterPath = (\n path: FindMyWay.PathInput,\n params: Schema.Top | undefined,\n): FindMyWay.PathInput => {\n if (params === undefined || !path.includes(\":\")) return path;\n let ast = params.ast;\n while (ast.encoding !== undefined) {\n ast = ast.encoding[ast.encoding.length - 1].to;\n }\n if (!SchemaAST.isObjects(ast) || ast.indexSignatures.length !== 0)\n return path;\n const names = new Set(\n ast.propertySignatures.map((property) => String(property.name)),\n );\n let normalized = \"\";\n let depth = 0;\n let regexStart = -1;\n for (let i = 0; i < path.length; i++) {\n const char = path[i];\n if (depth > 0 || i === regexStart) {\n normalized += char;\n if (char === \"\\\\\") normalized += path[++i] ?? \"\";\n else if (char === \"(\") depth++;\n else if (char === \")\") depth--;\n } else if (char !== \":\") {\n normalized += char;\n } else if (path[i + 1] === \":\") {\n normalized += \"::\";\n i++;\n } else {\n const param = /^:(\\w+)/.exec(path.slice(i));\n if (param !== null && names.has(param[1])) {\n normalized += param[0];\n i += param[0].length - 1;\n const next = path[i + 1];\n if (next === \"(\") regexStart = i + 1;\n else if (next !== undefined && !\"/.-?\".includes(next))\n normalized += \"(.*?)\";\n } else {\n normalized += \"::\";\n }\n }\n }\n return normalized as FindMyWay.PathInput;\n};\n\n// These are necessary until the Convex runtime supports these APIs. See\n// https://discord.com/channels/1019350475847499849/1281364098419785760\n// Each patch is applied only where the API is actually broken, so runtimes\n// with working implementations—Node under convex-test, notably, where\n// `new AbortSignal()` is an illegal constructor—keep their native behavior.\nconst applyMonkeyPatches = () => {\n const urlCredentialsBroken = (() => {\n try {\n return new URL(\"https://confect.dev\").username === undefined;\n } catch {\n return true;\n }\n })();\n if (urlCredentialsBroken) {\n URL = class extends URL {\n override get username() {\n return \"\";\n }\n override get password() {\n return \"\";\n }\n };\n }\n\n const requestSignalBroken = (() => {\n try {\n return new Request(\"https://confect.dev\").signal === undefined;\n } catch {\n return true;\n }\n })();\n if (requestSignalBroken) {\n // `configurable` so repeated definition—e.g. module re-evaluation in a\n // long-lived environment—does not throw.\n Object.defineProperty(Request.prototype, \"signal\", {\n get: () => new AbortSignal(),\n configurable: true,\n });\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HttpRouter.js","names":["RegisteredFunction.baseActionLayer","RegisteredFunction.actionLayer","ConvexLogger.layer","ObservabilityRuntime.memoized","ConvexConfigProvider.layer","layer"],"sources":["../src/HttpRouter.ts"],"sourcesContent":["import {\n type HttpRouter as ConvexHttpRouter,\n httpActionGeneric,\n httpRouter,\n ROUTABLE_HTTP_METHODS,\n type RouteSpecWithPathPrefix,\n type GenericActionCtx,\n type GenericDataModel,\n} from \"convex/server\";\nimport * as Array from \"effect/Array\";\nimport * as Console from \"effect/Console\";\nimport * as Context from \"effect/Context\";\nimport * as Config from \"effect/Config\";\nimport * as Effect from \"effect/Effect\";\nimport type * as FileSystem from \"effect/FileSystem\";\nimport * as Layer from \"effect/Layer\";\nimport * as Match from \"effect/Match\";\nimport type * as Path from \"effect/Path\";\nimport * as Schema from \"effect/Schema\";\nimport * as SchemaAST from \"effect/SchemaAST\";\nimport * as FindMyWay from \"effect/http/FindMyWay\";\nimport * as HttpApi from \"effect/http-api/HttpApi\";\nimport type * as HttpApiGroup from \"effect/http-api/HttpApiGroup\";\nimport type * as Etag from \"effect/http/Etag\";\nimport type * as HttpPlatform from \"effect/http/HttpPlatform\";\nimport * as HttpRouter from \"effect/http/HttpRouter\";\nimport * as HttpMiddleware from \"effect/http/HttpMiddleware\";\nimport type * as HttpServerRequest from \"effect/http/HttpServerRequest\";\nimport * as HttpServer from \"effect/http/HttpServer\";\nimport type * as ActionRunner from \"./ActionRunner.js\";\nimport type * as Auth from \"./Auth.js\";\nimport type { ExecutionMetadata } from \"./ExecutionMetadata.js\";\nimport type { RequestMetadata } from \"./RequestMetadata.js\";\nimport type * as DatabaseSchema from \"./DatabaseSchema.js\";\nimport type * as DataModel from \"./DataModel.js\";\nimport type * as Handler from \"./Handler.js\";\nimport * as ConvexConfigProvider from \"./ConvexConfigProvider.js\";\nimport * as ObservabilityRuntime from \"./internal/observability.js\";\nimport { withOptions } from \"./internal/httpOptions.js\";\nimport * as ConvexLogger from \"./ConvexLogger.js\";\nimport type * as MutationRunner from \"./MutationRunner.js\";\nimport type * as QueryRunner from \"./QueryRunner.js\";\nimport * as RegisteredFunction from \"./RegisteredFunction.js\";\nimport type * as Scheduler from \"./Scheduler.js\";\nimport type { Storage } from \"./Storage.js\";\nimport type { StorageActionWriter } from \"./StorageActionWriter.js\";\nimport type { StorageReader } from \"./StorageReader.js\";\nimport type { StorageWriter } from \"./StorageWriter.js\";\n\n/**\n * The Confect services available to HTTP route handlers and middleware,\n * supplied per request from the Convex action context.\n */\nexport type Services =\n | QueryRunner.QueryRunner\n | MutationRunner.MutationRunner\n | ActionRunner.ActionRunner\n | Scheduler.Scheduler\n | Auth.Auth\n | ExecutionMetadata\n | RequestMetadata\n | Storage\n | StorageReader\n | StorageWriter\n | StorageActionWriter;\n\n/**\n * A layer that registers routes on the HTTP router—the input to {@link make}.\n *\n * Compose it from Effect's `effect/http` and `effect/http-api` modules:\n * `HttpApiBuilder.layer(api)` registers an `HttpApi`'s endpoints (provide its\n * group handler layers with `Layer.provide`), `HttpApiScalar.layer(api, ...)`\n * serves interactive API docs, `HttpRouter.add` registers a plain route, and\n * `HttpRouter.middleware(fn, { global: true })` applies middleware to every\n * route. Merge any number of these with `Layer.mergeAll`.\n *\n * Route handlers and middleware may require any of the Confect {@link Services},\n * which surface as request-level `Requires` markers and are supplied per\n * request. Anything else the layer requires is a type error—notably, an\n * `HttpApiBuilder.layer(api)` whose group handler layers are not provided\n * leaves an `HttpApiGroup.ToService` requirement behind, so a missing handler\n * group is caught at compile time.\n *\n * Layers are built with Confect's Convex-aware `ConfigProvider` in context, so\n * `Config` reads (e.g. in `Layer.unwrap`) resolve against Convex environment\n * variables.\n */\nexport type Routes<Services_ = Services> = Layer.Layer<\n never,\n never,\n | HttpRouter.HttpRouter\n | Etag.Generator\n | FileSystem.FileSystem\n | HttpPlatform.HttpPlatform\n | Path.Path\n | HttpRouter.Request<\"Requires\", Services_>\n | HttpRouter.Request<\"GlobalRequires\", Services_>\n | HttpRouter.Request<\"Error\", any>\n | HttpRouter.Request<\"GlobalError\", any>\n>;\n\n/**\n * Create a Convex HTTP router serving the given routes.\n *\n * A single catch-all `httpAction` is registered for every routable method under\n * the path prefix `\"/\"`, making the Effect router the single source of truth\n * for paths: place an API with `HttpApi.prefix` or absolute route paths.\n * Requests that match no route receive the Effect router's 404 response.\n *\n * Supply `{ api }` with the `HttpApi` registered by the routes layer to show\n * endpoint patterns in Convex completion logs and the dashboard. Requests that\n * match no reflected endpoint keep the catch-all completion label. Without the\n * option, Convex reports the catch-all registrations. Application endpoints\n * must not overlap component HTTP mounts: Convex also uses dashboard route\n * entries to select between application and component routers.\n *\n * Plain Convex routes can still be added to the returned router—Convex matches\n * exact paths first and longer path prefixes before the catch-all, so they take\n * precedence.\n */\nexport const make = <Id extends string, Groups extends HttpApiGroup.Constraint>(\n routes: Routes,\n options?: {\n readonly observability?: Layer.Layer<never> | undefined;\n readonly api?: HttpApi.HttpApi<Id, Groups> | undefined;\n },\n): ConvexHttpRouter =>\n makeWithLayer(\n routes,\n RegisteredFunction.baseActionLayer,\n options?.api,\n options?.observability,\n );\n\n/**\n * Derive HTTP capabilities and ID scopes from the generated database schema.\n * Component routes are relative to their installation's HTTP mount prefix.\n * Supply `{ api }` as in {@link make}; labels stay component-relative.\n */\nexport const forSchema =\n <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(\n schema: DatabaseSchema_,\n ) =>\n <Id extends string, Groups extends HttpApiGroup.Constraint>(\n routes: Routes<Handler.ActionServices<DatabaseSchema_>>,\n options?: {\n readonly observability?: Layer.Layer<never> | undefined;\n readonly api?: HttpApi.HttpApi<Id, Groups> | undefined;\n },\n ): ConvexHttpRouter =>\n makeWithLayer<\n Handler.ActionServices<DatabaseSchema_>,\n DataModel.ToConvex<DataModel.FromSchema<DatabaseSchema_>>,\n Id,\n Groups\n >(\n Match.value(schema.target.kind).pipe(\n Match.when(\"app\", () => routes),\n Match.when(\"component\", () =>\n routes.pipe(Layer.provide(componentRouter)),\n ),\n Match.exhaustive,\n ),\n (ctx) => RegisteredFunction.actionLayer(schema, ctx),\n options?.api,\n options?.observability,\n schema.target.kind === \"component\",\n );\n\n// Convex uses a component-relative path to select its HTTP action but passes\n// the original request URL to that action. Its CONVEX_SITE_URL includes the\n// installation's full mount prefix (including parent mounts). Prefixing the\n// Effect router preserves relative routes without rewriting the Request body.\nconst componentRouter = Layer.effect(\n HttpRouter.HttpRouter,\n Effect.gen(function* () {\n const router = yield* HttpRouter.HttpRouter;\n const siteUrl = yield* Config.schema(\n Schema.URLFromString,\n \"CONVEX_SITE_URL\",\n );\n return router.prefixed(siteUrl.pathname);\n }).pipe(Effect.orDie),\n);\n\nconst makeWithLayer = <\n Services_,\n DataModel_ extends GenericDataModel,\n Id extends string,\n Groups extends HttpApiGroup.Constraint,\n>(\n routes: Routes<Services_>,\n layer: (ctx: GenericActionCtx<DataModel_>) => Layer.Layer<Services_>,\n api: HttpApi.HttpApi<Id, Groups> | undefined,\n observability?: Layer.Layer<never>,\n siteRelative = false,\n): ConvexHttpRouter => {\n applyMonkeyPatches();\n\n const spanName = Layer.effect(\n HttpMiddleware.SpanNameGenerator,\n Effect.gen(function* () {\n const prefix = siteRelative\n ? (yield* Config.schema(\n Schema.URLFromString,\n \"CONVEX_SITE_URL\",\n )).pathname.replace(/\\/$/, \"\")\n : \"\";\n return (request: HttpServerRequest.HttpServerRequest) =>\n convexHttpRouter.lookup(\n new URL(request.originalUrl).pathname.slice(prefix.length) || \"/\",\n request.method as (typeof ROUTABLE_HTTP_METHODS)[number],\n )?.[2] ?? \"/*\";\n }).pipe(Effect.orDie),\n );\n\n // Provided (not just merged) so that route layers' own construction—e.g.\n // a `Layer.unwrap` reading `Config`—resolves configuration through the\n // Convex-aware provider; merged so that request fibers—endpoint handlers\n // and middleware—do too.\n // Collected now: toWebHandler builds AppLayer before the reflection below.\n const endpoints = new Set<string>();\n if (api !== undefined)\n HttpApi.reflect(api, {\n onGroup: () => {},\n onEndpoint: ({ endpoint }) => {\n endpoints.add(\n `${endpoint.method} ${apiRouterPath(endpoint.path as FindMyWay.PathInput, endpoint.params)}`,\n );\n },\n });\n const AppLayer = withOptions(routes, endpoints).pipe(\n Layer.provideMerge(\n observability === undefined\n ? ConvexLogger.layer\n : Layer.merge(ObservabilityRuntime.memoized(observability), spanName),\n ),\n Layer.provideMerge(ConvexConfigProvider.layer),\n Layer.provide(HttpServer.layerServices),\n );\n\n // The Confect service requirements of route handlers and middleware surface\n // as request-level `Requires` markers on the layer, which the web handler\n // exposes as a per-request context argument.\n const { handler } = HttpRouter.toWebHandler(AppLayer, {\n disableLogger: true,\n /* oxlint-disable effecttsgo/any-unknown-in-error-context -- Keep Effect's erased HTTP boundary errors intact. */\n ...(observability === undefined\n ? {}\n : {\n middleware: (effect) =>\n Effect.andThen(\n Effect.annotateCurrentSpan(\"kind\", \"httpAction\"),\n effect,\n ),\n }),\n /* oxlint-enable effecttsgo/any-unknown-in-error-context */\n });\n\n const httpAction = httpActionGeneric((ctx, request): Promise<Response> => {\n // The ctx-backed service layers are all synchronous and finalizer-free,\n // so the scope can close as soon as the context is built.\n // Convex's HTTP registration erases the data model. The supplied generated\n // schema restores it; this does not change the runtime context or services.\n const services = Effect.runSync(\n Effect.scoped(\n Layer.build(layer(ctx as unknown as GenericActionCtx<DataModel_>)),\n ),\n );\n return handler(\n request,\n Context.add(services, Console.Console, globalThis.console),\n );\n });\n\n const convexHttpRouter = httpRouter();\n\n Array.forEach(ROUTABLE_HTTP_METHODS, (method) => {\n const routeSpec: RouteSpecWithPathPrefix = {\n pathPrefix: \"/\",\n method,\n handler: httpAction,\n };\n convexHttpRouter.route(routeSpec);\n });\n\n if (api !== undefined) {\n const matcher = FindMyWay.make<string>();\n const reflected: ReturnType<ConvexHttpRouter[\"getRoutes\"]> = [];\n HttpApi.reflect(api, {\n onGroup: () => {},\n onEndpoint: ({ endpoint }) => {\n const method = ROUTABLE_HTTP_METHODS.find(\n (candidate) => candidate === endpoint.method,\n );\n if (method === undefined) return;\n const path = apiRouterPath(\n endpoint.path as FindMyWay.PathInput,\n endpoint.params,\n );\n matcher.on(method, path, endpoint.path);\n // Effect also matches a wildcard route's base path without its slash.\n if (path.endsWith(\"/*\")) {\n matcher.on(method, path.slice(0, -2) as `/${string}`, endpoint.path);\n }\n reflected.push([endpoint.path, method, httpAction]);\n },\n });\n reflected.sort(\n ([pathA, methodA], [pathB, methodB]) =>\n pathA.localeCompare(pathB) || methodA.localeCompare(methodB),\n );\n\n const getRoutes = convexHttpRouter.getRoutes;\n convexHttpRouter.getRoutes = () =>\n reflected.length === 0\n ? getRoutes()\n : [\n ...getRoutes().filter(([, , action]) => action !== httpAction),\n ...reflected,\n ];\n const lookup = convexHttpRouter.lookup;\n convexHttpRouter.lookup = (path, method) => {\n const match = lookup(path, method);\n if (match === null || match[0] !== httpAction) return match;\n const normalized = match[1];\n const reflectedPath = matcher.find(normalized, path)?.handler;\n return reflectedPath === undefined\n ? match\n : [httpAction, normalized, reflectedPath];\n };\n }\n\n return convexHttpRouter;\n};\n\n/*!\n * apiRouterPath is adapted from Effect 4.0.1 http-api/internal/path.ts.\n *\n * MIT License\n *\n * Copyright (c) 2023 Effectful Technologies Inc\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n// Match Effect's HttpApiBuilder path normalization, which is not publicly\n// exported (effect/http-api/internal/path.ts). Undeclared colons are literals;\n// declared params followed by a literal suffix need an explicit regex boundary.\nconst apiRouterPath = (\n path: FindMyWay.PathInput,\n params: Schema.Top | undefined,\n): FindMyWay.PathInput => {\n if (params === undefined || !path.includes(\":\")) return path;\n let ast = params.ast;\n while (ast.encoding !== undefined) {\n ast = ast.encoding[ast.encoding.length - 1].to;\n }\n if (!SchemaAST.isObjects(ast) || ast.indexSignatures.length !== 0)\n return path;\n const names = new Set(\n ast.propertySignatures.map((property) => String(property.name)),\n );\n let normalized = \"\";\n let depth = 0;\n let regexStart = -1;\n for (let i = 0; i < path.length; i++) {\n const char = path[i];\n if (depth > 0 || i === regexStart) {\n normalized += char;\n if (char === \"\\\\\") normalized += path[++i] ?? \"\";\n else if (char === \"(\") depth++;\n else if (char === \")\") depth--;\n } else if (char !== \":\") {\n normalized += char;\n } else if (path[i + 1] === \":\") {\n normalized += \"::\";\n i++;\n } else {\n const param = /^:(\\w+)/.exec(path.slice(i));\n if (param !== null && names.has(param[1])) {\n normalized += param[0];\n i += param[0].length - 1;\n const next = path[i + 1];\n if (next === \"(\") regexStart = i + 1;\n else if (next !== undefined && !\"/.-?\".includes(next))\n normalized += \"(.*?)\";\n } else {\n normalized += \"::\";\n }\n }\n }\n return normalized as FindMyWay.PathInput;\n};\n\n// These are necessary until the Convex runtime supports these APIs. See\n// https://discord.com/channels/1019350475847499849/1281364098419785760\n// Each patch is applied only where the API is actually broken, so runtimes\n// with working implementations—Node under convex-test, notably, where\n// `new AbortSignal()` is an illegal constructor—keep their native behavior.\nconst applyMonkeyPatches = () => {\n const urlCredentialsBroken = (() => {\n try {\n return new URL(\"https://confect.dev\").username === undefined;\n } catch {\n return true;\n }\n })();\n if (urlCredentialsBroken) {\n URL = class extends URL {\n override get username() {\n return \"\";\n }\n override get password() {\n return \"\";\n }\n };\n }\n\n const requestSignalBroken = (() => {\n try {\n return new Request(\"https://confect.dev\").signal === undefined;\n } catch {\n return true;\n }\n })();\n if (requestSignalBroken) {\n // `configurable` so repeated definition—e.g. module re-evaluation in a\n // long-lived environment—does not throw.\n Object.defineProperty(Request.prototype, \"signal\", {\n get: () => new AbortSignal(),\n configurable: true,\n });\n }\n};\n\nexport { withBodyLimit } from \"./internal/httpBody.js\";\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as ByteSize from "effect/ByteSize";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
4
|
+
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
5
|
+
/**
|
|
6
|
+
* Count the body before downstream middleware or decoding. Configure maxBytes
|
|
7
|
+
* from the endpoint's policy. Non-identity Content-Encoding receives 415. Read
|
|
8
|
+
* arrayBuffer in the wrapped effect to verify the same bytes the decoder uses.
|
|
9
|
+
*/
|
|
10
|
+
export declare const withBodyLimit: <A, E, R>(effect: Effect.Effect<A, E, R>, options: {
|
|
11
|
+
readonly maxBytes: ByteSize.ByteSize;
|
|
12
|
+
}) => Effect.Effect<A | HttpServerResponse.HttpServerResponse, E, HttpServerRequest.HttpServerRequest | Exclude<R, HttpServerRequest.HttpServerRequest>>;
|
|
13
|
+
//# sourceMappingURL=httpBody.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpBody.d.ts","sourceRoot":"","sources":["../../src/internal/httpBody.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,iBAAiB,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,kBAAkB,MAAM,gCAAgC,CAAC;AAOrE;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,UAC3B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,WACrB;IAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAA;CAAE,uJAoD/C,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
5
|
+
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
6
|
+
//#region src/internal/httpBody.ts
|
|
7
|
+
var BodyTooLarge = class extends Schema.TaggedError()("BodyTooLarge", {}) {};
|
|
8
|
+
/**
|
|
9
|
+
* Count the body before downstream middleware or decoding. Configure maxBytes
|
|
10
|
+
* from the endpoint's policy. Non-identity Content-Encoding receives 415. Read
|
|
11
|
+
* arrayBuffer in the wrapped effect to verify the same bytes the decoder uses.
|
|
12
|
+
*/
|
|
13
|
+
const withBodyLimit = (effect, options) => Effect.gen(function* () {
|
|
14
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
15
|
+
const encoding = request.headers["content-encoding"]?.trim().toLowerCase();
|
|
16
|
+
if (encoding !== void 0 && encoding !== "identity") return HttpServerResponse.empty({ status: 415 });
|
|
17
|
+
let size = 0;
|
|
18
|
+
const chunks = [];
|
|
19
|
+
if (yield* (request.source instanceof Request && request.source.body === null ? Effect.void : Stream.runForEach(request.stream, (chunk) => {
|
|
20
|
+
size += chunk.byteLength;
|
|
21
|
+
if (BigInt(size) > options.maxBytes) return Effect.fail(new BodyTooLarge());
|
|
22
|
+
chunks.push(chunk);
|
|
23
|
+
return Effect.void;
|
|
24
|
+
})).pipe(Effect.as(false), Effect.catchTag("BodyTooLarge", () => Effect.succeed(true)), Effect.orDie)) return HttpServerResponse.empty({ status: 413 });
|
|
25
|
+
const bytes = new Uint8Array(size);
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const chunk of chunks) {
|
|
28
|
+
bytes.set(chunk, offset);
|
|
29
|
+
offset += chunk.byteLength;
|
|
30
|
+
}
|
|
31
|
+
const source = new Request(request.originalUrl, {
|
|
32
|
+
method: request.method,
|
|
33
|
+
headers: request.headers,
|
|
34
|
+
...request.method === "GET" || request.method === "HEAD" ? {} : { body: bytes }
|
|
35
|
+
});
|
|
36
|
+
source.arrayBuffer = () => Promise.resolve(bytes.buffer);
|
|
37
|
+
source.text = () => Promise.resolve(new TextDecoder().decode(bytes));
|
|
38
|
+
const buffered = HttpServerRequest.fromWeb(source).modify({
|
|
39
|
+
url: request.url,
|
|
40
|
+
remoteAddress: request.remoteAddress
|
|
41
|
+
});
|
|
42
|
+
return yield* Effect.provideService(effect, HttpServerRequest.HttpServerRequest, buffered);
|
|
43
|
+
});
|
|
44
|
+
//#endregion
|
|
45
|
+
export { withBodyLimit };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=httpBody.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpBody.js","names":[],"sources":["../../src/internal/httpBody.ts"],"sourcesContent":["import type * as ByteSize from \"effect/ByteSize\";\nimport * as Effect from \"effect/Effect\";\nimport * as Schema from \"effect/Schema\";\nimport * as Stream from \"effect/Stream\";\nimport * as HttpServerRequest from \"effect/http/HttpServerRequest\";\nimport * as HttpServerResponse from \"effect/http/HttpServerResponse\";\n\nclass BodyTooLarge extends Schema.TaggedError<BodyTooLarge>()(\n \"BodyTooLarge\",\n {},\n) {}\n\n/**\n * Count the body before downstream middleware or decoding. Configure maxBytes\n * from the endpoint's policy. Non-identity Content-Encoding receives 415. Read\n * arrayBuffer in the wrapped effect to verify the same bytes the decoder uses.\n */\nexport const withBodyLimit = <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n options: { readonly maxBytes: ByteSize.ByteSize },\n) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const encoding = request.headers[\"content-encoding\"]?.trim().toLowerCase();\n if (encoding !== undefined && encoding !== \"identity\")\n return HttpServerResponse.empty({ status: 415 });\n let size = 0;\n const chunks: Array<Uint8Array> = [];\n const empty =\n request.source instanceof Request && request.source.body === null;\n const read = empty\n ? Effect.void\n : Stream.runForEach(request.stream, (chunk) => {\n size += chunk.byteLength;\n if (BigInt(size) > options.maxBytes)\n return Effect.fail(new BodyTooLarge());\n chunks.push(chunk);\n return Effect.void;\n });\n const limited = yield* read.pipe(\n Effect.as(false),\n Effect.catchTag(\"BodyTooLarge\", () => Effect.succeed(true)),\n Effect.orDie,\n );\n if (limited) return HttpServerResponse.empty({ status: 413 });\n const bytes = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n const source = new Request(request.originalUrl, {\n method: request.method,\n headers: request.headers,\n ...(request.method === \"GET\" || request.method === \"HEAD\"\n ? {}\n : { body: bytes }),\n });\n // WORKAROUND: Effect 4.0.4 caches text and bytes independently. Remove these\n // overrides when the minimum Effect version shares text and byte reads.\n source.arrayBuffer = () => Promise.resolve(bytes.buffer);\n source.text = () => Promise.resolve(new TextDecoder().decode(bytes));\n const buffered = HttpServerRequest.fromWeb(source).modify({\n url: request.url,\n remoteAddress: request.remoteAddress,\n });\n return yield* Effect.provideService(\n effect,\n HttpServerRequest.HttpServerRequest,\n buffered,\n );\n });\n"],"mappings":";;;;;;AAOA,IAAM,eAAN,cAA2B,OAAO,YAA0B,CAAC,CAC3D,gBACA,CAAC,CACH,CAAC,CAAC,CAAC;;;;;;AAOH,MAAa,iBACX,QACA,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,kBAAkB;CACzC,MAAM,WAAW,QAAQ,QAAQ,mBAAmB,EAAE,KAAK,CAAC,CAAC,YAAY;CACzE,IAAI,aAAa,KAAA,KAAa,aAAa,YACzC,OAAO,mBAAmB,MAAM,EAAE,QAAQ,IAAI,CAAC;CACjD,IAAI,OAAO;CACX,MAAM,SAA4B,CAAC;CAiBnC,IAAI,QAfF,QAAQ,kBAAkB,WAAW,QAAQ,OAAO,SAAS,OAE3D,OAAO,OACP,OAAO,WAAW,QAAQ,SAAS,UAAU;EAC3C,QAAQ,MAAM;EACd,IAAI,OAAO,IAAI,IAAI,QAAQ,UACzB,OAAO,OAAO,KAAK,IAAI,aAAa,CAAC;EACvC,OAAO,KAAK,KAAK;EACjB,OAAO,OAAO;CAChB,CAAC,EAAA,CACuB,KAC1B,OAAO,GAAG,KAAK,GACf,OAAO,SAAS,sBAAsB,OAAO,QAAQ,IAAI,CAAC,GAC1D,OAAO,KACT,GACa,OAAO,mBAAmB,MAAM,EAAE,QAAQ,IAAI,CAAC;CAC5D,MAAM,QAAQ,IAAI,WAAW,IAAI;CACjC,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,IAAI,OAAO,MAAM;EACvB,UAAU,MAAM;CAClB;CACA,MAAM,SAAS,IAAI,QAAQ,QAAQ,aAAa;EAC9C,QAAQ,QAAQ;EAChB,SAAS,QAAQ;EACjB,GAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,SAC/C,CAAC,IACD,EAAE,MAAM,MAAM;CACpB,CAAC;CAGD,OAAO,oBAAoB,QAAQ,QAAQ,MAAM,MAAM;CACvD,OAAO,aAAa,QAAQ,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAAC;CACnE,MAAM,WAAW,kBAAkB,QAAQ,MAAM,CAAC,CAAC,OAAO;EACxD,KAAK,QAAQ;EACb,eAAe,QAAQ;CACzB,CAAC;CACD,OAAO,OAAO,OAAO,eACnB,QACA,kBAAkB,mBAClB,QACF;AACF,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
import * as HttpRouter from "effect/http/HttpRouter";
|
|
3
|
+
export declare const withOptions: <A, E, R>(routes: Layer.Layer<A, E, R>, endpoints: ReadonlySet<string>) => Layer.Layer<A, E, HttpRouter.HttpRouter | Exclude<R, HttpRouter.HttpRouter> | Exclude<HttpRouter.Request<"Error", any>, A> | Exclude<HttpRouter.Request<"Requires", any>, A>>;
|
|
4
|
+
//# sourceMappingURL=httpOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpOptions.d.ts","sourceRoot":"","sources":["../../src/internal/httpOptions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AA2CrD,eAAO,MAAM,WAAW,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,UACzB,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,aACjB,WAAW,CAAC,MAAM,CAAC,kLAwG7B,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import * as HttpRouter from "effect/http/HttpRouter";
|
|
6
|
+
import * as HttpServerError from "effect/http/HttpServerError";
|
|
7
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
8
|
+
//#region src/internal/httpOptions.ts
|
|
9
|
+
const notFound = Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => Effect.fail(new HttpServerError.HttpServerError({ reason: new HttpServerError.RouteNotFound({ request }) })));
|
|
10
|
+
const optional = /(\/:[^/()]*?)\?(\/?)/;
|
|
11
|
+
const forms = (path) => optional.test(path) ? [path.replace(optional, "$1$2"), path.replace(optional, "$2") || "/"] : [path];
|
|
12
|
+
const missingRouteContext = `Service not found: ${HttpRouter.RouteContext.key}`;
|
|
13
|
+
const preflight = (handler) => handler.pipe(Effect.updateContext(Context.omit(HttpRouter.RouteContext)), Effect.catchDefect((defect) => defect instanceof Error && defect.message === missingRouteContext ? notFound : Effect.die(defect)));
|
|
14
|
+
const withOptions = (routes, endpoints) => Layer.unwrap(Effect.gen(function* () {
|
|
15
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
16
|
+
const registered = [];
|
|
17
|
+
const wrap = (target, prefix) => {
|
|
18
|
+
const addAll = (entries) => Effect.contextWith((context) => {
|
|
19
|
+
for (const route of entries) registered.push({
|
|
20
|
+
route,
|
|
21
|
+
prefix,
|
|
22
|
+
context
|
|
23
|
+
});
|
|
24
|
+
return target.addAll(entries);
|
|
25
|
+
});
|
|
26
|
+
return HttpRouter.HttpRouter.of({
|
|
27
|
+
...target,
|
|
28
|
+
addAll,
|
|
29
|
+
add: (method, path, handler, options) => addAll([HttpRouter.route(method, path, handler, options)]),
|
|
30
|
+
prefixed: (next) => wrap(target.prefixed(next), `${prefix}${next.replace(/\/$/, "")}`)
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const register = Effect.gen(function* () {
|
|
34
|
+
const byMethod = /* @__PURE__ */ new Map();
|
|
35
|
+
const first = yield* HttpRouter.make;
|
|
36
|
+
const explicit = yield* HttpRouter.make;
|
|
37
|
+
for (const { route, prefix, context } of registered) {
|
|
38
|
+
const at = (target) => prefix === "" ? target : target.prefixed(prefix);
|
|
39
|
+
if (route.method === "OPTIONS" || route.method === "*") {
|
|
40
|
+
yield* at(explicit).addAll([{
|
|
41
|
+
...route,
|
|
42
|
+
handler: notFound
|
|
43
|
+
}]);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const copy = {
|
|
47
|
+
...route,
|
|
48
|
+
method: "OPTIONS",
|
|
49
|
+
handler: endpoints.has(`${route.method} ${route.path}`) ? preflight(route.handler) : notFound
|
|
50
|
+
};
|
|
51
|
+
const candidate = byMethod.get(route.method) ?? (yield* HttpRouter.make);
|
|
52
|
+
byMethod.set(route.method, candidate);
|
|
53
|
+
yield* Effect.provideContext(at(candidate).addAll([copy]), context);
|
|
54
|
+
const placed = prefix === "" ? copy : HttpRouter.prefixRoute(copy, prefix);
|
|
55
|
+
for (const path of forms(placed.path)) {
|
|
56
|
+
const form = first.addAll([{
|
|
57
|
+
...placed,
|
|
58
|
+
path
|
|
59
|
+
}]);
|
|
60
|
+
yield* Effect.exit(Effect.provideContext(form, context));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const get = byMethod.get("GET");
|
|
64
|
+
if (get !== void 0) {
|
|
65
|
+
const head = byMethod.get("HEAD") ?? (yield* HttpRouter.make);
|
|
66
|
+
byMethod.set("HEAD", head);
|
|
67
|
+
yield* Effect.exit(head.add("OPTIONS", "*", get.asHttpEffect()));
|
|
68
|
+
}
|
|
69
|
+
const claimed = yield* Effect.exit(explicit.add("OPTIONS", "*", notFound));
|
|
70
|
+
if (Exit.isFailure(claimed)) return;
|
|
71
|
+
const candidates = new Map(Array.from(byMethod, ([method, candidate]) => [method, candidate.asHttpEffect()]));
|
|
72
|
+
const fallback = first.asHttpEffect();
|
|
73
|
+
yield* router.add("OPTIONS", "*", Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {
|
|
74
|
+
const method = request.headers["access-control-request-method"];
|
|
75
|
+
return method === void 0 ? fallback : candidates.get(method) ?? notFound;
|
|
76
|
+
}));
|
|
77
|
+
});
|
|
78
|
+
return Layer.effectDiscard(register).pipe(Layer.provideMerge(routes.pipe(Layer.provide(Layer.succeed(HttpRouter.HttpRouter, wrap(router, ""))))));
|
|
79
|
+
}));
|
|
80
|
+
//#endregion
|
|
81
|
+
export { withOptions };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=httpOptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpOptions.js","names":[],"sources":["../../src/internal/httpOptions.ts"],"sourcesContent":["import * as Context from \"effect/Context\";\nimport * as Effect from \"effect/Effect\";\nimport * as Exit from \"effect/Exit\";\nimport * as Layer from \"effect/Layer\";\nimport * as HttpRouter from \"effect/http/HttpRouter\";\nimport * as HttpServerError from \"effect/http/HttpServerError\";\nimport * as HttpServerRequest from \"effect/http/HttpServerRequest\";\n\n/* oxlint-disable effecttsgo/any-unknown-in-error-context -- Preserve the router's erased request errors at the routing boundary. */\n\ntype Route = Parameters<HttpRouter.HttpRouter[\"addAll\"]>[0][number];\n\nconst notFound = Effect.flatMap(\n HttpServerRequest.HttpServerRequest,\n (request) =>\n Effect.fail(\n new HttpServerError.HttpServerError({\n reason: new HttpServerError.RouteNotFound({ request }),\n }),\n ),\n);\n\n// Effect's router adds `/a/:b` and `/a` for `/a/:b?` in one call, so an earlier\n// `/a/:id` rejects both. Split the same way, each form claims `first` alone.\nconst optional = /(\\/:[^/()]*?)\\?(\\/?)/;\nconst forms = (path: Route[\"path\"]) =>\n (optional.test(path)\n ? [path.replace(optional, \"$1$2\"), path.replace(optional, \"$2\") || \"/\"]\n : [path]) as ReadonlyArray<Route[\"path\"]>;\n\nconst missingRouteContext = `Service not found: ${HttpRouter.RouteContext.key}`;\n\n// WORKAROUND: Effect 4.0.4 compiles endpoint middleware, decoding and the\n// handler into one route handler. Its endpoint body reads RouteContext before\n// decoding or calling the handler, so without RouteContext a preflight that\n// middleware passes on stops there. Remove when Effect can run an endpoint's\n// middleware around another handler.\nconst preflight = (handler: Route[\"handler\"]) =>\n handler.pipe(\n Effect.updateContext(Context.omit(HttpRouter.RouteContext)),\n Effect.catchDefect((defect) =>\n defect instanceof Error && defect.message === missingRouteContext\n ? notFound\n : Effect.die(defect),\n ),\n );\n\nexport const withOptions = <A, E, R>(\n routes: Layer.Layer<A, E, R>,\n endpoints: ReadonlySet<string>,\n) =>\n Layer.unwrap(\n Effect.gen(function* () {\n const router = yield* HttpRouter.HttpRouter;\n const registered: Array<{\n readonly route: Route;\n readonly prefix: string;\n readonly context: Context.Context<never>;\n }> = [];\n const wrap = (\n target: HttpRouter.HttpRouter,\n prefix: string,\n ): HttpRouter.HttpRouter => {\n const addAll: HttpRouter.HttpRouter[\"addAll\"] = (entries) =>\n Effect.contextWith((context: Context.Context<never>) => {\n for (const route of entries)\n registered.push({ route, prefix, context });\n return target.addAll(entries);\n });\n return HttpRouter.HttpRouter.of({\n ...target,\n addAll,\n add: (method, path, handler, options) =>\n addAll([HttpRouter.route(method, path, handler, options)]),\n prefixed: (next) =>\n wrap(target.prefixed(next), `${prefix}${next.replace(/\\/$/, \"\")}`),\n });\n };\n const register = Effect.gen(function* () {\n // Effect's routers decide which paths match: one per declared method,\n // and `first`, which refuses a path equivalent to an earlier one.\n const byMethod = new Map<string, HttpRouter.HttpRouter>();\n const first = yield* HttpRouter.make;\n const explicit = yield* HttpRouter.make;\n for (const { route, prefix, context } of registered) {\n const at = (target: HttpRouter.HttpRouter) =>\n prefix === \"\" ? target : target.prefixed(prefix);\n if (route.method === \"OPTIONS\" || route.method === \"*\") {\n yield* at(explicit).addAll([{ ...route, handler: notFound }]);\n continue;\n }\n const copy: Route = {\n ...route,\n method: \"OPTIONS\",\n handler: endpoints.has(`${route.method} ${route.path}`)\n ? preflight(route.handler)\n : notFound,\n };\n const candidate =\n byMethod.get(route.method) ?? (yield* HttpRouter.make);\n byMethod.set(route.method, candidate);\n // Route registration erases request requirements, as in Effect's router.\n yield* Effect.provideContext(at(candidate).addAll([copy]), context);\n // Split the prefixed path: an optional parameter may sit in the prefix.\n const placed =\n prefix === \"\" ? copy : HttpRouter.prefixRoute(copy, prefix);\n for (const path of forms(placed.path)) {\n const form = first.addAll([{ ...placed, path }]);\n yield* Effect.exit(Effect.provideContext(form, context));\n }\n }\n // Effect's router answers HEAD from GET where no HEAD route matches. The\n // root wildcard matches last, unless a HEAD route already claims it.\n const get = byMethod.get(\"GET\");\n if (get !== undefined) {\n const head = byMethod.get(\"HEAD\") ?? (yield* HttpRouter.make);\n byMethod.set(\"HEAD\", head);\n yield* Effect.exit(head.add(\"OPTIONS\", \"*\", get.asHttpEffect()));\n }\n // An explicit OPTIONS route on the root wildcard answers every path.\n const claimed = yield* Effect.exit(\n explicit.add(\"OPTIONS\", \"*\", notFound),\n );\n if (Exit.isFailure(claimed)) return;\n const candidates = new Map(\n Array.from(byMethod, ([method, candidate]) => [\n method,\n candidate.asHttpEffect(),\n ]),\n );\n const fallback = first.asHttpEffect();\n // The root wildcard matches last, so explicit OPTIONS routes still win.\n yield* router.add(\n \"OPTIONS\",\n \"*\",\n Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {\n const method = request.headers[\"access-control-request-method\"];\n return method === undefined\n ? fallback\n : (candidates.get(method) ?? notFound);\n }),\n );\n });\n return Layer.effectDiscard(register).pipe(\n Layer.provideMerge(\n routes.pipe(\n Layer.provide(\n Layer.succeed(HttpRouter.HttpRouter, wrap(router, \"\")),\n ),\n ),\n ),\n );\n }),\n );\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaganref/confect-server",
|
|
3
|
-
"version": "10.3.1-fork.
|
|
3
|
+
"version": "10.3.1-fork.73",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@effect/ai-openai-compat": "4.0.4",
|
|
6
6
|
"@effect/ai-openrouter": "4.0.4"
|
|
7
7
|
},
|
|
8
8
|
"peerDependencies": {
|
|
9
|
-
"@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.
|
|
9
|
+
"@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.73",
|
|
10
10
|
"@effect/platform-node": ">=4.0.0 <4.1.0",
|
|
11
11
|
"convex": "^1.49.0",
|
|
12
12
|
"effect": ">=4.0.4 <4.1.0"
|
package/src/HttpRouter.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type * as DataModel from "./DataModel.js";
|
|
|
36
36
|
import type * as Handler from "./Handler.js";
|
|
37
37
|
import * as ConvexConfigProvider from "./ConvexConfigProvider.js";
|
|
38
38
|
import * as ObservabilityRuntime from "./internal/observability.js";
|
|
39
|
+
import { withOptions } from "./internal/httpOptions.js";
|
|
39
40
|
import * as ConvexLogger from "./ConvexLogger.js";
|
|
40
41
|
import type * as MutationRunner from "./MutationRunner.js";
|
|
41
42
|
import type * as QueryRunner from "./QueryRunner.js";
|
|
@@ -217,7 +218,18 @@ const makeWithLayer = <
|
|
|
217
218
|
// a `Layer.unwrap` reading `Config`—resolves configuration through the
|
|
218
219
|
// Convex-aware provider; merged so that request fibers—endpoint handlers
|
|
219
220
|
// and middleware—do too.
|
|
220
|
-
|
|
221
|
+
// Collected now: toWebHandler builds AppLayer before the reflection below.
|
|
222
|
+
const endpoints = new Set<string>();
|
|
223
|
+
if (api !== undefined)
|
|
224
|
+
HttpApi.reflect(api, {
|
|
225
|
+
onGroup: () => {},
|
|
226
|
+
onEndpoint: ({ endpoint }) => {
|
|
227
|
+
endpoints.add(
|
|
228
|
+
`${endpoint.method} ${apiRouterPath(endpoint.path as FindMyWay.PathInput, endpoint.params)}`,
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const AppLayer = withOptions(routes, endpoints).pipe(
|
|
221
233
|
Layer.provideMerge(
|
|
222
234
|
observability === undefined
|
|
223
235
|
? ConvexLogger.layer
|
|
@@ -436,3 +448,5 @@ const applyMonkeyPatches = () => {
|
|
|
436
448
|
});
|
|
437
449
|
}
|
|
438
450
|
};
|
|
451
|
+
|
|
452
|
+
export { withBodyLimit } from "./internal/httpBody.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type * as ByteSize from "effect/ByteSize";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
6
|
+
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
7
|
+
|
|
8
|
+
class BodyTooLarge extends Schema.TaggedError<BodyTooLarge>()(
|
|
9
|
+
"BodyTooLarge",
|
|
10
|
+
{},
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Count the body before downstream middleware or decoding. Configure maxBytes
|
|
15
|
+
* from the endpoint's policy. Non-identity Content-Encoding receives 415. Read
|
|
16
|
+
* arrayBuffer in the wrapped effect to verify the same bytes the decoder uses.
|
|
17
|
+
*/
|
|
18
|
+
export const withBodyLimit = <A, E, R>(
|
|
19
|
+
effect: Effect.Effect<A, E, R>,
|
|
20
|
+
options: { readonly maxBytes: ByteSize.ByteSize },
|
|
21
|
+
) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
24
|
+
const encoding = request.headers["content-encoding"]?.trim().toLowerCase();
|
|
25
|
+
if (encoding !== undefined && encoding !== "identity")
|
|
26
|
+
return HttpServerResponse.empty({ status: 415 });
|
|
27
|
+
let size = 0;
|
|
28
|
+
const chunks: Array<Uint8Array> = [];
|
|
29
|
+
const empty =
|
|
30
|
+
request.source instanceof Request && request.source.body === null;
|
|
31
|
+
const read = empty
|
|
32
|
+
? Effect.void
|
|
33
|
+
: Stream.runForEach(request.stream, (chunk) => {
|
|
34
|
+
size += chunk.byteLength;
|
|
35
|
+
if (BigInt(size) > options.maxBytes)
|
|
36
|
+
return Effect.fail(new BodyTooLarge());
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
return Effect.void;
|
|
39
|
+
});
|
|
40
|
+
const limited = yield* read.pipe(
|
|
41
|
+
Effect.as(false),
|
|
42
|
+
Effect.catchTag("BodyTooLarge", () => Effect.succeed(true)),
|
|
43
|
+
Effect.orDie,
|
|
44
|
+
);
|
|
45
|
+
if (limited) return HttpServerResponse.empty({ status: 413 });
|
|
46
|
+
const bytes = new Uint8Array(size);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
bytes.set(chunk, offset);
|
|
50
|
+
offset += chunk.byteLength;
|
|
51
|
+
}
|
|
52
|
+
const source = new Request(request.originalUrl, {
|
|
53
|
+
method: request.method,
|
|
54
|
+
headers: request.headers,
|
|
55
|
+
...(request.method === "GET" || request.method === "HEAD"
|
|
56
|
+
? {}
|
|
57
|
+
: { body: bytes }),
|
|
58
|
+
});
|
|
59
|
+
// WORKAROUND: Effect 4.0.4 caches text and bytes independently. Remove these
|
|
60
|
+
// overrides when the minimum Effect version shares text and byte reads.
|
|
61
|
+
source.arrayBuffer = () => Promise.resolve(bytes.buffer);
|
|
62
|
+
source.text = () => Promise.resolve(new TextDecoder().decode(bytes));
|
|
63
|
+
const buffered = HttpServerRequest.fromWeb(source).modify({
|
|
64
|
+
url: request.url,
|
|
65
|
+
remoteAddress: request.remoteAddress,
|
|
66
|
+
});
|
|
67
|
+
return yield* Effect.provideService(
|
|
68
|
+
effect,
|
|
69
|
+
HttpServerRequest.HttpServerRequest,
|
|
70
|
+
buffered,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as HttpRouter from "effect/http/HttpRouter";
|
|
6
|
+
import * as HttpServerError from "effect/http/HttpServerError";
|
|
7
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
8
|
+
|
|
9
|
+
/* oxlint-disable effecttsgo/any-unknown-in-error-context -- Preserve the router's erased request errors at the routing boundary. */
|
|
10
|
+
|
|
11
|
+
type Route = Parameters<HttpRouter.HttpRouter["addAll"]>[0][number];
|
|
12
|
+
|
|
13
|
+
const notFound = Effect.flatMap(
|
|
14
|
+
HttpServerRequest.HttpServerRequest,
|
|
15
|
+
(request) =>
|
|
16
|
+
Effect.fail(
|
|
17
|
+
new HttpServerError.HttpServerError({
|
|
18
|
+
reason: new HttpServerError.RouteNotFound({ request }),
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Effect's router adds `/a/:b` and `/a` for `/a/:b?` in one call, so an earlier
|
|
24
|
+
// `/a/:id` rejects both. Split the same way, each form claims `first` alone.
|
|
25
|
+
const optional = /(\/:[^/()]*?)\?(\/?)/;
|
|
26
|
+
const forms = (path: Route["path"]) =>
|
|
27
|
+
(optional.test(path)
|
|
28
|
+
? [path.replace(optional, "$1$2"), path.replace(optional, "$2") || "/"]
|
|
29
|
+
: [path]) as ReadonlyArray<Route["path"]>;
|
|
30
|
+
|
|
31
|
+
const missingRouteContext = `Service not found: ${HttpRouter.RouteContext.key}`;
|
|
32
|
+
|
|
33
|
+
// WORKAROUND: Effect 4.0.4 compiles endpoint middleware, decoding and the
|
|
34
|
+
// handler into one route handler. Its endpoint body reads RouteContext before
|
|
35
|
+
// decoding or calling the handler, so without RouteContext a preflight that
|
|
36
|
+
// middleware passes on stops there. Remove when Effect can run an endpoint's
|
|
37
|
+
// middleware around another handler.
|
|
38
|
+
const preflight = (handler: Route["handler"]) =>
|
|
39
|
+
handler.pipe(
|
|
40
|
+
Effect.updateContext(Context.omit(HttpRouter.RouteContext)),
|
|
41
|
+
Effect.catchDefect((defect) =>
|
|
42
|
+
defect instanceof Error && defect.message === missingRouteContext
|
|
43
|
+
? notFound
|
|
44
|
+
: Effect.die(defect),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const withOptions = <A, E, R>(
|
|
49
|
+
routes: Layer.Layer<A, E, R>,
|
|
50
|
+
endpoints: ReadonlySet<string>,
|
|
51
|
+
) =>
|
|
52
|
+
Layer.unwrap(
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
55
|
+
const registered: Array<{
|
|
56
|
+
readonly route: Route;
|
|
57
|
+
readonly prefix: string;
|
|
58
|
+
readonly context: Context.Context<never>;
|
|
59
|
+
}> = [];
|
|
60
|
+
const wrap = (
|
|
61
|
+
target: HttpRouter.HttpRouter,
|
|
62
|
+
prefix: string,
|
|
63
|
+
): HttpRouter.HttpRouter => {
|
|
64
|
+
const addAll: HttpRouter.HttpRouter["addAll"] = (entries) =>
|
|
65
|
+
Effect.contextWith((context: Context.Context<never>) => {
|
|
66
|
+
for (const route of entries)
|
|
67
|
+
registered.push({ route, prefix, context });
|
|
68
|
+
return target.addAll(entries);
|
|
69
|
+
});
|
|
70
|
+
return HttpRouter.HttpRouter.of({
|
|
71
|
+
...target,
|
|
72
|
+
addAll,
|
|
73
|
+
add: (method, path, handler, options) =>
|
|
74
|
+
addAll([HttpRouter.route(method, path, handler, options)]),
|
|
75
|
+
prefixed: (next) =>
|
|
76
|
+
wrap(target.prefixed(next), `${prefix}${next.replace(/\/$/, "")}`),
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const register = Effect.gen(function* () {
|
|
80
|
+
// Effect's routers decide which paths match: one per declared method,
|
|
81
|
+
// and `first`, which refuses a path equivalent to an earlier one.
|
|
82
|
+
const byMethod = new Map<string, HttpRouter.HttpRouter>();
|
|
83
|
+
const first = yield* HttpRouter.make;
|
|
84
|
+
const explicit = yield* HttpRouter.make;
|
|
85
|
+
for (const { route, prefix, context } of registered) {
|
|
86
|
+
const at = (target: HttpRouter.HttpRouter) =>
|
|
87
|
+
prefix === "" ? target : target.prefixed(prefix);
|
|
88
|
+
if (route.method === "OPTIONS" || route.method === "*") {
|
|
89
|
+
yield* at(explicit).addAll([{ ...route, handler: notFound }]);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const copy: Route = {
|
|
93
|
+
...route,
|
|
94
|
+
method: "OPTIONS",
|
|
95
|
+
handler: endpoints.has(`${route.method} ${route.path}`)
|
|
96
|
+
? preflight(route.handler)
|
|
97
|
+
: notFound,
|
|
98
|
+
};
|
|
99
|
+
const candidate =
|
|
100
|
+
byMethod.get(route.method) ?? (yield* HttpRouter.make);
|
|
101
|
+
byMethod.set(route.method, candidate);
|
|
102
|
+
// Route registration erases request requirements, as in Effect's router.
|
|
103
|
+
yield* Effect.provideContext(at(candidate).addAll([copy]), context);
|
|
104
|
+
// Split the prefixed path: an optional parameter may sit in the prefix.
|
|
105
|
+
const placed =
|
|
106
|
+
prefix === "" ? copy : HttpRouter.prefixRoute(copy, prefix);
|
|
107
|
+
for (const path of forms(placed.path)) {
|
|
108
|
+
const form = first.addAll([{ ...placed, path }]);
|
|
109
|
+
yield* Effect.exit(Effect.provideContext(form, context));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Effect's router answers HEAD from GET where no HEAD route matches. The
|
|
113
|
+
// root wildcard matches last, unless a HEAD route already claims it.
|
|
114
|
+
const get = byMethod.get("GET");
|
|
115
|
+
if (get !== undefined) {
|
|
116
|
+
const head = byMethod.get("HEAD") ?? (yield* HttpRouter.make);
|
|
117
|
+
byMethod.set("HEAD", head);
|
|
118
|
+
yield* Effect.exit(head.add("OPTIONS", "*", get.asHttpEffect()));
|
|
119
|
+
}
|
|
120
|
+
// An explicit OPTIONS route on the root wildcard answers every path.
|
|
121
|
+
const claimed = yield* Effect.exit(
|
|
122
|
+
explicit.add("OPTIONS", "*", notFound),
|
|
123
|
+
);
|
|
124
|
+
if (Exit.isFailure(claimed)) return;
|
|
125
|
+
const candidates = new Map(
|
|
126
|
+
Array.from(byMethod, ([method, candidate]) => [
|
|
127
|
+
method,
|
|
128
|
+
candidate.asHttpEffect(),
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
const fallback = first.asHttpEffect();
|
|
132
|
+
// The root wildcard matches last, so explicit OPTIONS routes still win.
|
|
133
|
+
yield* router.add(
|
|
134
|
+
"OPTIONS",
|
|
135
|
+
"*",
|
|
136
|
+
Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {
|
|
137
|
+
const method = request.headers["access-control-request-method"];
|
|
138
|
+
return method === undefined
|
|
139
|
+
? fallback
|
|
140
|
+
: (candidates.get(method) ?? notFound);
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
return Layer.effectDiscard(register).pipe(
|
|
145
|
+
Layer.provideMerge(
|
|
146
|
+
routes.pipe(
|
|
147
|
+
Layer.provide(
|
|
148
|
+
Layer.succeed(HttpRouter.HttpRouter, wrap(router, "")),
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}),
|
|
154
|
+
);
|