export-runtime 0.0.14 → 0.0.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.
@@ -42,6 +42,11 @@ const r2Bindings = cloudflareConfig.r2 || [];
42
42
  const kvBindings = cloudflareConfig.kv || [];
43
43
  const authConfig = cloudflareConfig.auth || null;
44
44
 
45
+ // Optional: Security configuration
46
+ const securityConfig = pkg.security || {};
47
+ const accessConfig = securityConfig.access || {};
48
+ const allowedOrigins = accessConfig.origin || []; // empty = allow all (default Workers behavior)
49
+
45
50
  // Auth requires a D1 database for better-auth
46
51
  const allD1Bindings = [...d1Bindings];
47
52
  if (authConfig && !allD1Bindings.includes("AUTH_DB")) {
@@ -81,11 +86,11 @@ function discoverModules(dir, base = "") {
81
86
  const modules = [];
82
87
  if (!fs.existsSync(dir)) return modules;
83
88
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
84
- if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
89
+ if (entry.name.startsWith("_") || entry.name.startsWith(".") || entry.name === "node_modules") continue;
85
90
  const fullPath = path.join(dir, entry.name);
86
91
  if (entry.isDirectory()) {
87
92
  modules.push(...discoverModules(fullPath, base ? `${base}/${entry.name}` : entry.name));
88
- } else if (EXTENSIONS.includes(path.extname(entry.name))) {
93
+ } else if (EXTENSIONS.includes(path.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
89
94
  const nameWithoutExt = entry.name.replace(/\.(ts|tsx|js|jsx)$/, "");
90
95
  const routePath = nameWithoutExt === "index"
91
96
  ? base // index.ts → directory path ("" for root)
@@ -391,16 +396,21 @@ const wranglerLines = [
391
396
  ``,
392
397
  ];
393
398
 
394
- // Add static assets configuration if main is specified
399
+ // Add static assets configuration if main is specified and directory exists
395
400
  if (assetsDir) {
396
401
  const normalizedAssetsDir = assetsDir.startsWith("./") ? assetsDir : `./${assetsDir}`;
397
- wranglerLines.push(
398
- `[assets]`,
399
- `directory = "${normalizedAssetsDir}"`,
400
- `binding = "ASSETS"`,
401
- `run_worker_first = true`,
402
- ``,
403
- );
402
+ const absoluteAssetsDir = path.resolve(cwd, normalizedAssetsDir);
403
+ if (fs.existsSync(absoluteAssetsDir)) {
404
+ wranglerLines.push(
405
+ `[assets]`,
406
+ `directory = "${normalizedAssetsDir}"`,
407
+ `binding = "ASSETS"`,
408
+ `run_worker_first = true`,
409
+ ``,
410
+ );
411
+ } else {
412
+ console.log(`Note: Assets directory "${normalizedAssetsDir}" not found. Run "vite build" first to enable static assets.`);
413
+ }
404
414
  }
405
415
 
406
416
  // Add Durable Objects for shared state
@@ -465,6 +475,7 @@ export const d1Bindings = ${JSON.stringify(allD1Bindings)};
465
475
  export const r2Bindings = ${JSON.stringify(r2Bindings)};
466
476
  export const kvBindings = ${JSON.stringify(kvBindings)};
467
477
  export const authConfig = ${JSON.stringify(authConfig)};
478
+ export const securityConfig = ${JSON.stringify({ access: { origin: allowedOrigins } })};
468
479
  `;
469
480
  fs.writeFileSync(configPath, configContent);
470
481
 
package/handler.js CHANGED
@@ -5,14 +5,14 @@ import { handleAuthRoute, getSessionFromRequest, verifySession } from "./auth.js
5
5
 
6
6
  const JS = "application/javascript; charset=utf-8";
7
7
  const TS = "application/typescript; charset=utf-8";
8
- const CORS = { "Access-Control-Allow-Origin": "*" };
9
8
  const IMMUTABLE = "public, max-age=31536000, immutable";
10
9
 
11
- const jsResponse = (body, extra = {}) =>
12
- new Response(body, { headers: { "Content-Type": JS, ...CORS, ...extra } });
13
-
14
- const tsResponse = (body, status = 200) =>
15
- new Response(body, { status, headers: { "Content-Type": TS, ...CORS, "Cache-Control": "no-cache" } });
10
+ const createResponseHelpers = (corsHeaders) => ({
11
+ jsResponse: (body, extra = {}) =>
12
+ new Response(body, { headers: { "Content-Type": JS, ...corsHeaders, ...extra } }),
13
+ tsResponse: (body, status = 200) =>
14
+ new Response(body, { status, headers: { "Content-Type": TS, ...corsHeaders, "Cache-Control": "no-cache" } }),
15
+ });
16
16
 
17
17
  export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore, exportConfig = {}) => {
18
18
  // moduleMap: { routePath: moduleNamespace, ... }
@@ -28,7 +28,33 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
28
28
  }
29
29
 
30
30
  // Export configuration
31
- const { d1Bindings = [], r2Bindings = [], kvBindings = [], authConfig = null } = exportConfig;
31
+ const { d1Bindings = [], r2Bindings = [], kvBindings = [], authConfig = null, securityConfig = {} } = exportConfig;
32
+
33
+ // Security: allowed origins (empty array = allow all)
34
+ const allowedOrigins = securityConfig?.access?.origin || [];
35
+ const hasOriginRestriction = allowedOrigins.length > 0;
36
+
37
+ // Check if origin is allowed
38
+ const isOriginAllowed = (origin) => {
39
+ if (!hasOriginRestriction) return true;
40
+ if (!origin) return false;
41
+ return allowedOrigins.includes(origin);
42
+ };
43
+
44
+ // Get CORS headers for a request
45
+ const getCorsHeaders = (request) => {
46
+ const origin = request.headers.get("Origin");
47
+ if (!hasOriginRestriction) {
48
+ return { "Access-Control-Allow-Origin": "*" };
49
+ }
50
+ if (origin && isOriginAllowed(origin)) {
51
+ return {
52
+ "Access-Control-Allow-Origin": origin,
53
+ "Vary": "Origin",
54
+ };
55
+ }
56
+ return {};
57
+ };
32
58
  const hasClient = d1Bindings.length > 0 || r2Bindings.length > 0 || kvBindings.length > 0 || authConfig;
33
59
 
34
60
  // Generate core code with config
@@ -70,9 +96,8 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
70
96
  const namedExports = keys
71
97
  .map((key) => `export const ${key} = createProxy([${JSON.stringify(route)}, ${JSON.stringify(key)}]);`)
72
98
  .join("\n");
73
- // Include default export (client) if configured
74
- const defaultExport = hasClient ? `\nexport { default } from ".${cpath}";` : "";
75
- return `import { createProxy } from ".${cpath}";\n${namedExports}${defaultExport}`;
99
+ // Always include default export (client) - it will be null if no bindings configured
100
+ return `import { createProxy } from ".${cpath}";\nexport { default } from ".${cpath}";\n${namedExports}`;
76
101
  };
77
102
 
78
103
  const buildExportModule = (cpath, route, name) =>
@@ -331,9 +356,24 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
331
356
  async fetch(request, env) {
332
357
  const url = new URL(request.url);
333
358
  const isShared = url.searchParams.has("shared");
359
+ const origin = request.headers.get("Origin");
360
+
361
+ // --- Origin check ---
362
+ if (hasOriginRestriction && origin && !isOriginAllowed(origin)) {
363
+ return new Response("Forbidden: Origin not allowed", { status: 403 });
364
+ }
365
+
366
+ // Get CORS headers for this request
367
+ const corsHeaders = getCorsHeaders(request);
368
+ const { jsResponse, tsResponse } = createResponseHelpers(corsHeaders);
334
369
 
335
370
  // --- WebSocket upgrade ---
336
371
  if (request.headers.get("Upgrade") === "websocket") {
372
+ // Origin check for WebSocket (browsers send Origin header)
373
+ if (hasOriginRestriction && origin && !isOriginAllowed(origin)) {
374
+ return new Response("Forbidden: Origin not allowed", { status: 403 });
375
+ }
376
+
337
377
  const pair = new WebSocketPair();
338
378
  const [client, server] = Object.values(pair);
339
379
  server.accept();
@@ -393,7 +433,7 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
393
433
  if (env?.ASSETS) {
394
434
  return env.ASSETS.fetch(request);
395
435
  }
396
- return new Response("Not found", { status: 404 });
436
+ return new Response("Not found", { status: 404, headers: corsHeaders });
397
437
  }
398
438
 
399
439
  const { route, exportName } = resolved;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "export-runtime",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Cloudflare Workers ESM Export Framework Runtime",
5
5
  "keywords": [
6
6
  "cloudflare",