@decocms/runtime 1.0.0-alpha.14 → 1.0.0-alpha.16

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cors.ts +140 -0
  3. package/src/index.ts +26 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/runtime",
3
- "version": "1.0.0-alpha.14",
3
+ "version": "1.0.0-alpha.16",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@cloudflare/workers-types": "^4.20250617.0",
package/src/cors.ts ADDED
@@ -0,0 +1,140 @@
1
+ export type CORSOrigin =
2
+ | string
3
+ | string[]
4
+ | ((origin: string, req: Request) => string | null | undefined);
5
+
6
+ export interface CORSOptions {
7
+ /**
8
+ * The value of "Access-Control-Allow-Origin" CORS header.
9
+ * Can be a string, array of strings, or a function that returns the allowed origin.
10
+ * @default '*'
11
+ */
12
+ origin?: CORSOrigin;
13
+ /**
14
+ * The value of "Access-Control-Allow-Methods" CORS header.
15
+ * @default ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
16
+ */
17
+ allowMethods?: string[];
18
+ /**
19
+ * The value of "Access-Control-Allow-Headers" CORS header.
20
+ * @default []
21
+ */
22
+ allowHeaders?: string[];
23
+ /**
24
+ * The value of "Access-Control-Max-Age" CORS header (in seconds).
25
+ */
26
+ maxAge?: number;
27
+ /**
28
+ * The value of "Access-Control-Allow-Credentials" CORS header.
29
+ */
30
+ credentials?: boolean;
31
+ /**
32
+ * The value of "Access-Control-Expose-Headers" CORS header.
33
+ * @default []
34
+ */
35
+ exposeHeaders?: string[];
36
+ }
37
+
38
+ const DEFAULT_ALLOW_METHODS = ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"];
39
+
40
+ const resolveOrigin = (
41
+ origin: CORSOrigin | undefined,
42
+ requestOrigin: string | null,
43
+ req: Request,
44
+ ): string | null => {
45
+ if (!requestOrigin) return null;
46
+
47
+ if (origin === undefined || origin === "*") {
48
+ return "*";
49
+ }
50
+
51
+ if (typeof origin === "string") {
52
+ return origin === requestOrigin ? origin : null;
53
+ }
54
+
55
+ if (Array.isArray(origin)) {
56
+ return origin.includes(requestOrigin) ? requestOrigin : null;
57
+ }
58
+
59
+ if (typeof origin === "function") {
60
+ return origin(requestOrigin, req) ?? null;
61
+ }
62
+
63
+ return null;
64
+ };
65
+
66
+ const setCORSHeaders = (
67
+ headers: Headers,
68
+ req: Request,
69
+ options: CORSOptions,
70
+ ): void => {
71
+ const requestOrigin = req.headers.get("Origin");
72
+ const allowedOrigin = resolveOrigin(options.origin, requestOrigin, req);
73
+
74
+ if (allowedOrigin) {
75
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
76
+ }
77
+
78
+ if (options.credentials) {
79
+ headers.set("Access-Control-Allow-Credentials", "true");
80
+ }
81
+
82
+ if (options.exposeHeaders?.length) {
83
+ headers.set(
84
+ "Access-Control-Expose-Headers",
85
+ options.exposeHeaders.join(", "),
86
+ );
87
+ }
88
+ };
89
+
90
+ export const handlePreflight = (
91
+ req: Request,
92
+ options: CORSOptions,
93
+ ): Response => {
94
+ const headers = new Headers();
95
+ const requestOrigin = req.headers.get("Origin");
96
+ const allowedOrigin = resolveOrigin(options.origin, requestOrigin, req);
97
+
98
+ if (allowedOrigin) {
99
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
100
+ }
101
+
102
+ if (options.credentials) {
103
+ headers.set("Access-Control-Allow-Credentials", "true");
104
+ }
105
+
106
+ const allowMethods = options.allowMethods ?? DEFAULT_ALLOW_METHODS;
107
+ headers.set("Access-Control-Allow-Methods", allowMethods.join(", "));
108
+
109
+ const requestHeaders = req.headers.get("Access-Control-Request-Headers");
110
+ if (options.allowHeaders?.length) {
111
+ headers.set(
112
+ "Access-Control-Allow-Headers",
113
+ options.allowHeaders.join(", "),
114
+ );
115
+ } else if (requestHeaders) {
116
+ // Mirror the requested headers if no explicit allowHeaders configured
117
+ headers.set("Access-Control-Allow-Headers", requestHeaders);
118
+ }
119
+
120
+ if (options.maxAge !== undefined) {
121
+ headers.set("Access-Control-Max-Age", options.maxAge.toString());
122
+ }
123
+
124
+ return new Response(null, { status: 204, headers });
125
+ };
126
+
127
+ export const withCORS = (
128
+ response: Response,
129
+ req: Request,
130
+ options: CORSOptions,
131
+ ): Response => {
132
+ const newHeaders = new Headers(response.headers);
133
+ setCORSHeaders(newHeaders, req, options);
134
+
135
+ return new Response(response.body, {
136
+ status: response.status,
137
+ statusText: response.statusText,
138
+ headers: newHeaders,
139
+ });
140
+ };
package/src/index.ts CHANGED
@@ -10,12 +10,14 @@ import {
10
10
  MCPServer,
11
11
  } from "./tools.ts";
12
12
  import type { Binding, ContractBinding, MCPBinding } from "./wrangler.ts";
13
+ import { type CORSOptions, handlePreflight, withCORS } from "./cors.ts";
13
14
  export { proxyConnectionForId } from "./bindings.ts";
14
15
  export {
15
16
  createMCPFetchStub,
16
17
  type CreateStubAPIOptions,
17
18
  type ToolBinder,
18
19
  } from "./mcp.ts";
20
+ export { type CORSOptions, type CORSOrigin } from "./cors.ts";
19
21
 
20
22
  export interface DefaultEnv<TSchema extends z.ZodTypeAny = any> {
21
23
  MESH_REQUEST_CONTEXT: RequestContext<TSchema>;
@@ -56,6 +58,11 @@ export interface UserDefaultExport<
56
58
  env: TEnv,
57
59
  ctx: ExecutionContext,
58
60
  ) => Promise<Response> | Response;
61
+ /**
62
+ * CORS configuration options.
63
+ * Set to `false` to disable CORS handling entirely.
64
+ */
65
+ cors?: CORSOptions | false;
59
66
  }
60
67
 
61
68
  // 1. Map binding type to its interface
@@ -228,6 +235,8 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
228
235
  userFns: UserDefaultExport<TEnv, TSchema>,
229
236
  ): ExportedHandler<TEnv & DefaultEnv<TSchema>> => {
230
237
  const server = createMCPServer<TEnv, TSchema>(userFns);
238
+ const corsOptions = userFns.cors;
239
+
231
240
  const fetcher = async (
232
241
  req: Request,
233
242
  env: TEnv & DefaultEnv<TSchema>,
@@ -265,22 +274,38 @@ export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
265
274
  new Response("Not found", { status: 404 })
266
275
  );
267
276
  };
277
+
268
278
  return {
269
279
  fetch: async (
270
280
  req: Request,
271
281
  env: TEnv & DefaultEnv<TSchema>,
272
282
  ctx: ExecutionContext,
273
283
  ) => {
284
+ // Handle CORS preflight (OPTIONS) requests
285
+ if (corsOptions !== false && req.method === "OPTIONS") {
286
+ const options = corsOptions ?? {};
287
+ return handlePreflight(req, options);
288
+ }
289
+
274
290
  const bindings = withBindings({
275
291
  env,
276
292
  server,
277
293
  tokenOrContext: req.headers.get("x-mesh-token") ?? undefined,
278
294
  url: req.url,
279
295
  });
280
- return await State.run(
296
+
297
+ const response = await State.run(
281
298
  { req, env: bindings, ctx },
282
299
  async () => await fetcher(req, bindings, ctx),
283
300
  );
301
+
302
+ // Add CORS headers to response
303
+ if (corsOptions !== false) {
304
+ const options = corsOptions ?? {};
305
+ return withCORS(response, req, options);
306
+ }
307
+
308
+ return response;
284
309
  },
285
310
  };
286
311
  };