@tknf/matchbox 0.2.6 → 0.3.1

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 (37) hide show
  1. package/README.md +212 -46
  2. package/dist/cgi.d.ts +10 -21
  3. package/dist/cgi.js +26 -97
  4. package/dist/htaccess/access-control.d.ts +9 -0
  5. package/dist/htaccess/access-control.js +91 -0
  6. package/dist/htaccess/error-document.d.ts +9 -0
  7. package/dist/htaccess/error-document.js +16 -0
  8. package/dist/htaccess/headers.d.ts +32 -0
  9. package/dist/htaccess/headers.js +98 -0
  10. package/dist/htaccess/index.d.ts +8 -0
  11. package/dist/htaccess/index.js +20 -0
  12. package/dist/htaccess/parser.d.ts +9 -0
  13. package/dist/htaccess/parser.js +365 -0
  14. package/dist/htaccess/rewrite.d.ts +13 -0
  15. package/dist/htaccess/rewrite.js +69 -0
  16. package/dist/htaccess/types.d.ts +156 -0
  17. package/dist/htaccess/types.js +0 -0
  18. package/dist/htaccess/utils.d.ts +21 -0
  19. package/dist/htaccess/utils.js +69 -0
  20. package/dist/html.d.ts +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +5 -1
  23. package/dist/middleware/auth.d.ts +8 -0
  24. package/dist/middleware/auth.js +28 -0
  25. package/dist/middleware/htaccess.d.ts +13 -0
  26. package/dist/middleware/htaccess.js +42 -0
  27. package/dist/middleware/index.d.ts +10 -0
  28. package/dist/middleware/index.js +14 -0
  29. package/dist/middleware/protected-files.d.ts +8 -0
  30. package/dist/middleware/protected-files.js +14 -0
  31. package/dist/middleware/session.d.ts +16 -0
  32. package/dist/middleware/session.js +37 -0
  33. package/dist/middleware/trailing-slash.d.ts +8 -0
  34. package/dist/middleware/trailing-slash.js +12 -0
  35. package/dist/with-defaults.d.ts +2 -1
  36. package/dist/with-defaults.js +17 -28
  37. package/package.json +1 -1
package/README.md CHANGED
@@ -1,29 +1,36 @@
1
1
  # Matchbox
2
2
 
3
- A simple CGI-style web server framework built on top of Hono. Treats `.cgi.tsx` / `.cgi.jsx` files under `public/` as pages, and brings Apache-style conventions like `.htaccess` and `.htpasswd` into modern tooling.
3
+ A modern CGI-style web framework built on [Hono](https://hono.dev). Brings Apache-style conventions (`.htaccess`, `.htpasswd`) into the modern TypeScript/JSX ecosystem with file-based routing and familiar CGI variables.
4
4
 
5
5
  ## Features
6
6
 
7
- - File-based routing (`.cgi.tsx` / `.cgi.jsx` map directly to endpoints)
8
- - CGI-style context (`$_GET`, `$_POST`, `$_SESSION`, etc.)
9
- - `.htaccess` rewrites/redirects and `.htpasswd` Basic Auth
10
- - Session cookie configuration and custom middleware
11
- - Vite + TypeScript + JSX support
7
+ - **File-based routing** - `.cgi.tsx`/`.cgi.jsx` files map directly to URL endpoints
8
+ - **CGI-style context** - Familiar `$_GET`, `$_POST`, `$_SESSION`, `$_SERVER`, `$_COOKIE`, etc.
9
+ - **Apache compatibility** - `.htaccess` for rewrites/redirects/headers, `.htpasswd` for Basic Auth
10
+ - **Modern tooling** - Full TypeScript support, Vite integration, JSX rendering
11
+ - **Flexible configuration** - Session management, custom middleware, security headers
12
+ - **Production ready** - Comprehensive test coverage, security best practices
12
13
 
13
- ## Install
14
+ ## Installation
14
15
 
15
16
  ```bash
16
- pnpm add matchbox hono
17
+ npm add @tknf/matchbox hono
18
+ # or
19
+ pnpm add @tknf/matchbox hono
20
+ # or
21
+ yarn add @tknf/matchbox hono
17
22
  ```
18
23
 
19
24
  ## Quick Start
20
25
 
21
- `vite.config.ts`:
26
+ ### 1. Configure Vite
22
27
 
23
- ```ts
28
+ Create `vite.config.ts`:
29
+
30
+ ```typescript
24
31
  import devServer from "@hono/vite-dev-server";
25
32
  import { defineConfig } from "vite";
26
- import { MatchboxPlugin } from "matchbox/plugin";
33
+ import { MatchboxPlugin } from "@tknf/matchbox/plugin";
27
34
 
28
35
  export default defineConfig({
29
36
  plugins: [
@@ -36,98 +43,257 @@ export default defineConfig({
36
43
  });
37
44
  ```
38
45
 
39
- `server.ts`:
46
+ ### 2. Create Server Entry
47
+
48
+ Create `server.ts`:
40
49
 
41
- ```ts
42
- import { createCgi } from "matchbox";
50
+ ```typescript
51
+ import { createCgi } from "@tknf/matchbox";
43
52
 
44
53
  export default createCgi();
45
54
  ```
46
55
 
47
- `public/index.cgi.tsx`:
56
+ ### 3. Create Your First Page
57
+
58
+ Create `public/index.cgi.tsx`:
48
59
 
49
60
  ```tsx
50
- import type { CgiContext } from "matchbox";
61
+ import type { CgiContext } from "@tknf/matchbox";
51
62
 
52
- export default function ({ $_SERVER }: CgiContext) {
63
+ export default function ({ $_SERVER, $_GET }: CgiContext) {
53
64
  return (
54
65
  <html>
55
66
  <head>
56
67
  <title>Matchbox</title>
57
68
  </head>
58
69
  <body>
59
- <h1>Hello Matchbox</h1>
60
- <p>Method: {$_SERVER.REQUEST_METHOD}</p>
70
+ <h1>Hello from Matchbox!</h1>
71
+ <p>Request Method: {$_SERVER.REQUEST_METHOD}</p>
72
+ <p>Query Params: {JSON.stringify($_GET)}</p>
61
73
  </body>
62
74
  </html>
63
75
  );
64
76
  }
65
77
  ```
66
78
 
67
- Start the dev server:
79
+ ### 4. Start Development Server
68
80
 
69
81
  ```bash
70
- pnpm dev
82
+ npm run dev
71
83
  ```
72
84
 
85
+ Visit `http://localhost:5173` to see your page.
86
+
73
87
  ## CGI Context
74
88
 
75
- Page functions receive a `CgiContext`.
89
+ Every page function receives a `CgiContext` object with:
90
+
91
+ ### Request Data
92
+
93
+ - `$_GET` - Query parameters
94
+ - `$_POST` - Form data (POST/PUT)
95
+ - `$_FILES` - Uploaded files
96
+ - `$_REQUEST` - Combined `$_GET` + `$_POST` + `$_COOKIE`
97
+ - `$_COOKIE` - Cookie values
98
+ - `$_SERVER` - Server and request information
99
+ - `$_ENV` - Environment variables
100
+ - `$_SESSION` - Session data
101
+
102
+ ### Helper Functions
76
103
 
77
- - `$_GET` / `$_POST` / `$_FILES` / `$_REQUEST`
78
- - `$_SESSION` / `$_COOKIE` / `$_ENV` / `$_SERVER`
79
- - `header(name, value)` / `status(code)` / `redirect(url, status?)`
80
- - `cgiinfo()` / `get_modules()` / `get_version()`
104
+ - `header(name, value)` - Set response header
105
+ - `status(code)` - Set HTTP status code
106
+ - `redirect(url, status?)` - Redirect to another URL
107
+ - `cgiinfo()` - HTML debug info block
108
+ - `get_modules()` - List all available CGI modules
109
+ - `get_version()` - Get Matchbox version string
110
+ - `log(message)` - Custom logging
111
+ - `request_headers()` - Get all request headers
112
+ - `response_headers()` - Get current response headers
113
+
114
+ ### Example Usage
115
+
116
+ ```tsx
117
+ export default function (ctx: CgiContext) {
118
+ const { $_GET, $_POST, $_SESSION, header, status, redirect } = ctx;
119
+
120
+ // Handle form submission
121
+ if ($_POST.username) {
122
+ $_SESSION.user = $_POST.username;
123
+ return redirect("/dashboard");
124
+ }
125
+
126
+ // Set custom headers
127
+ header("X-Custom-Header", "value");
128
+ status(200);
129
+
130
+ return <div>Welcome</div>;
131
+ }
132
+ ```
81
133
 
82
134
  ## Configuration
83
135
 
84
- ### MatchboxPlugin
136
+ ### Plugin Options
85
137
 
86
- ```ts
138
+ ```typescript
87
139
  MatchboxPlugin({
88
- publicDir: "public",
89
- config: { siteName: "My Site" },
140
+ publicDir: "public", // Directory for .cgi files (default: "public")
141
+ config: { // Custom config object injected into pages
142
+ siteName: "My Site",
143
+ apiUrl: "https://api.example.com"
144
+ }
90
145
  });
91
146
  ```
92
147
 
93
- - `publicDir`: Root directory for page discovery (default: `public`)
94
- - `config`: Configuration object injected into pages
148
+ Access config in your pages via `context.config`.
95
149
 
96
- ### createCgi
150
+ ### Runtime Options
97
151
 
98
- ```ts
152
+ ```typescript
99
153
  createCgi({
154
+ // Session cookie configuration
100
155
  sessionCookie: {
101
- name: "_SESSION_ID",
102
- sameSite: "Lax",
103
- secure: true,
104
- maxAge: 3600,
156
+ name: "_SESSION_ID", // Cookie name (default: "_SESSION_ID")
157
+ path: "/", // Cookie path (default: "/")
158
+ domain: "example.com", // Cookie domain
159
+ secure: true, // HTTPS only (default: false)
160
+ sameSite: "Strict", // CSRF protection: "Strict" | "Lax" | "None"
161
+ maxAge: 3600, // Session timeout in seconds
105
162
  },
163
+
164
+ // URL trailing slash enforcement
106
165
  enforceTrailingSlash: true,
166
+
167
+ // Custom middleware (runs before page handlers)
107
168
  middleware: [
108
169
  async (c, next) => {
109
170
  c.header("X-App", "matchbox");
110
171
  await next();
111
172
  },
112
173
  ],
174
+
175
+ // Custom logger
113
176
  logger: (message, level) => {
114
177
  console.log(`[${level ?? "info"}] ${message}`);
115
178
  },
116
179
  });
117
180
  ```
118
181
 
119
- ## Auth and Rewrites
182
+ ## Apache-Style Features
183
+
184
+ ### Basic Authentication (.htpasswd)
185
+
186
+ Place a `.htpasswd` file in any directory under `public/` to protect it:
187
+
188
+ ```
189
+ # Generate with: htpasswd -c .htpasswd username
190
+ admin:$apr1$abc123$...
191
+ user:$apr1$xyz789$...
192
+ ```
193
+
194
+ All files in that directory and subdirectories will require authentication.
195
+
196
+ ### URL Rewriting and Redirects (.htaccess)
197
+
198
+ Create `.htaccess` files to configure rewrites, redirects, headers, and error pages:
199
+
200
+ ```apache
201
+ # Permanent redirect
202
+ Redirect 301 /old-page /new-page
120
203
 
121
- Place these under `public/` to enable them.
204
+ # Conditional rewrite
205
+ RewriteCond %{HTTP_HOST} ^www\.example\.com$
206
+ RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
122
207
 
123
- - `.htpasswd`: Directory-level Basic Auth
124
- - `.htaccess`: Simple `RewriteRule` / `Redirect` support
208
+ # Pattern-based rewrite
209
+ RewriteRule ^blog/(.+)$ /posts.cgi?slug=$1 [QSA,L]
125
210
 
126
- ## References
211
+ # Forbidden
212
+ RewriteRule ^private$ - [F]
127
213
 
128
- - Examples: `examples/README.md`
129
- - Security: `docs/security.md`
214
+ # Security headers
215
+ Header set X-Frame-Options "SAMEORIGIN"
216
+ Header set X-Content-Type-Options "nosniff"
217
+ Header set Strict-Transport-Security "max-age=31536000"
218
+
219
+ # Custom error pages
220
+ ErrorDocument 404 /errors/404.html
221
+ ErrorDocument 500 /errors/500.html
222
+ ```
223
+
224
+ #### Supported RewriteRule Flags
225
+
226
+ - `[L]` - Last rule, stop processing
227
+ - `[R]` / `[R=301]` / `[R=302]` - Redirect with status code
228
+ - `[F]` - Forbidden (403)
229
+ - `[G]` - Gone (410)
230
+ - `[NC]` - No Case (case-insensitive)
231
+ - `[QSA]` - Query String Append
232
+ - `[QSD]` - Query String Discard
233
+ - `[NE]` - No Escape
234
+
235
+ #### Supported RewriteCond Variables
236
+
237
+ - `%{HTTP_HOST}` - Request host
238
+ - `%{HTTP_USER_AGENT}` - User agent
239
+ - `%{HTTP_REFERER}` - Referer header
240
+ - `%{REQUEST_URI}` - Request URI
241
+ - `%{REQUEST_METHOD}` - HTTP method
242
+ - `%{QUERY_STRING}` - Query string
243
+ - `%{REMOTE_ADDR}` - Client IP
244
+ - And more... (see [documentation](./docs/htaccess.md))
245
+
246
+ ## Examples
247
+
248
+ Check out the [`examples/`](./examples) directory for complete working examples:
249
+
250
+ - **basic** - Minimal setup with a simple page
251
+ - **htaccess-auth** - Authentication and URL rewriting
252
+ - **custom-middleware** - Custom middleware and logging
253
+
254
+ ## Documentation
255
+
256
+ - [Apache .htaccess Features](./docs/htaccess.md) - Complete `.htaccess` feature reference
257
+ - [Security Guide](./docs/security.md) - Security best practices
258
+ - [API Reference](./docs/api.md) - Complete API documentation
259
+ - [Roadmap](./docs/roadmap.md) - Planned features and improvements
260
+
261
+ ## Migration from v0.2.x
262
+
263
+ Version 0.3.0 introduced enhanced `.htaccess` parsing. If you're upgrading:
264
+
265
+ - ✅ Existing `.htaccess` files work without changes
266
+ - ✅ Both `[R=301]` and `R=301` flag syntaxes are supported
267
+ - ⚠️ Malformed directives now throw errors (previously silently ignored)
268
+
269
+ See the [migration guide](./docs/htaccess.md#migration-from-v02x-to-v03) for details.
270
+
271
+ ## Development
272
+
273
+ ```bash
274
+ # Install dependencies
275
+ pnpm install
276
+
277
+ # Run tests
278
+ pnpm test
279
+
280
+ # Build
281
+ pnpm run build
282
+
283
+ # Type check
284
+ pnpm run typecheck
285
+ ```
130
286
 
131
287
  ## License
132
288
 
133
- MIT License. See `LICENSE` for details.
289
+ MIT License - see [LICENSE](./LICENSE) for details.
290
+
291
+ ## Contributing
292
+
293
+ Contributions are welcome! Please read the [contributing guidelines](./CONTRIBUTING.md) before submitting PRs.
294
+
295
+ ## Links
296
+
297
+ - [GitHub Repository](https://github.com/tknf-labs/matchbox)
298
+ - [NPM Package](https://www.npmjs.com/package/@tknf/matchbox)
299
+ - [Hono Framework](https://hono.dev)
package/dist/cgi.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import { Context, Hono } from 'hono';
3
3
  import { HtmlEscapedString } from 'hono/utils/html';
4
+ import { HtaccessConfig } from './htaccess/types.js';
4
5
 
5
- type ConfigObject = Record<string, any>;
6
+ type ConfigObject = Record<string, unknown>;
6
7
  /**
7
8
  * --- Session Cookie Configuration ---
8
9
  */
@@ -49,10 +50,10 @@ interface CgiContext<ConfigType = ConfigObject> {
49
50
  $_GET: Record<string, string>;
50
51
  $_POST: Record<string, string>;
51
52
  $_FILES: Record<string, File | File[]>;
52
- $_REQUEST: Record<string, any>;
53
- $_SESSION: Record<string, any>;
53
+ $_REQUEST: Record<string, unknown>;
54
+ $_SESSION: Record<string, unknown>;
54
55
  $_COOKIE: Record<string, string>;
55
- $_ENV: Record<string, string | undefined>;
56
+ $_ENV: Record<string, unknown>;
56
57
  $_SERVER: {
57
58
  REQUEST_METHOD: string;
58
59
  REQUEST_URI: string;
@@ -61,7 +62,7 @@ interface CgiContext<ConfigType = ConfigObject> {
61
62
  SCRIPT_NAME: string;
62
63
  PATH_INFO: string;
63
64
  QUERY_STRING: string;
64
- [key: string]: any;
65
+ [key: string]: unknown;
65
66
  };
66
67
  config: ConfigType;
67
68
  c: Context;
@@ -83,24 +84,12 @@ interface CgiContext<ConfigType = ConfigObject> {
83
84
  type Page = {
84
85
  urlPath: string;
85
86
  dirPath: string | null;
86
- component: (context: CgiContext) => any | Promise<any>;
87
+ component: (context: CgiContext) => unknown | Promise<unknown>;
87
88
  };
88
- type RedirectRule = {
89
- type: "redirect";
90
- code: string;
91
- source: string;
92
- target: string;
93
- };
94
- type RewriteRule = {
95
- type: "rewrite";
96
- pattern: string;
97
- target: string;
98
- flags: string;
99
- };
100
- type RewriteMap = Record<string, Array<RedirectRule | RewriteRule>>;
89
+
101
90
  /**
102
91
  * --- Matchbox Runtime Engine ---
103
92
  */
104
- declare const createCgiWithPages: (pages: Page[], siteConfig?: ConfigObject, authMap?: Record<string, string>, rewriteMap?: RewriteMap, options?: MatchboxOptions) => Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
93
+ declare const createCgiWithPages: (pages: Page[], siteConfig?: ConfigObject, authMap?: Record<string, string>, htaccessConfig?: HtaccessConfig, options?: MatchboxOptions) => Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
105
94
 
106
- export { type CgiContext, type ConfigObject, type MatchboxOptions, type ModuleInfo, type Page, type RewriteMap, type SessionCookieOptions, createCgiWithPages };
95
+ export { type CgiContext, type ConfigObject, HtaccessConfig, type MatchboxOptions, type ModuleInfo, type Page, type SessionCookieOptions, createCgiWithPages };
package/dist/cgi.js CHANGED
@@ -1,13 +1,20 @@
1
1
  import { Hono } from "hono";
2
- import { basicAuth } from "hono/basic-auth";
3
- import { getCookie, setCookie } from "hono/cookie";
2
+ import { getCookie } from "hono/cookie";
4
3
  import { generateCgiError, generateCgiInfo } from "./html.js";
4
+ import {
5
+ applyBasicAuth,
6
+ applyHtaccessMiddleware,
7
+ applyErrorDocumentMiddleware,
8
+ getSessionFromCookie,
9
+ saveSessionToCookie,
10
+ applyProtectedFilesMiddleware,
11
+ applyTrailingSlashMiddleware
12
+ } from "./middleware/index.js";
5
13
  const isRedirectObject = (obj) => {
6
- return obj && obj.__type === "redirect" && typeof obj.url === "string";
14
+ return typeof obj === "object" && obj !== null && "__type" in obj && obj.__type === "redirect" && "url" in obj && typeof obj.url === "string";
7
15
  };
8
- const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
16
+ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, htaccessConfig = {}, options = {}) => {
9
17
  const app = new Hono();
10
- const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
11
18
  if (options.middleware && options.middleware.length > 0) {
12
19
  for (const mw of options.middleware) {
13
20
  app.use("*", async (c, next) => {
@@ -18,66 +25,12 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
18
25
  });
19
26
  }
20
27
  }
21
- const protectedFiles = [".htaccess", ".htpasswd", ".htdigest", ".htgroup"];
22
- app.use("*", async (c, next) => {
23
- const path = c.req.path;
24
- const lastSegment = path.slice(path.lastIndexOf("/") + 1);
25
- if (protectedFiles.some((file) => lastSegment === file)) {
26
- return c.text("Forbidden", 403);
27
- }
28
- await next();
29
- });
28
+ applyProtectedFilesMiddleware(app);
30
29
  if (options.enforceTrailingSlash) {
31
- app.use("*", async (c, next) => {
32
- const path = c.req.path;
33
- if (!path.endsWith("/") && !path.includes(".")) {
34
- return c.redirect(`${path}/`, 301);
35
- }
36
- await next();
37
- });
30
+ applyTrailingSlashMiddleware(app);
38
31
  }
39
- Object.entries(rewriteMap).forEach(([dir, rules]) => {
40
- const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
41
- app.use(`${basePath}/*`, async (c, next) => {
42
- const relPath = c.req.path.replace(basePath, "") || "/";
43
- for (const rule of rules) {
44
- if (rule.type === "redirect") {
45
- if (relPath === rule.source) {
46
- return c.redirect(
47
- rule.target,
48
- Number.parseInt(rule.code, 10) || 302
49
- );
50
- }
51
- } else if (rule.type === "rewrite") {
52
- const regex = new RegExp(rule.pattern);
53
- if (regex.test(relPath)) {
54
- const target = rule.target.startsWith("/") ? rule.target : `${basePath}/${rule.target}`;
55
- if (rule.flags.includes("R")) {
56
- const code = rule.flags.match(/R=(\d+)/)?.[1] || "302";
57
- return c.redirect(target, Number.parseInt(code, 10));
58
- }
59
- }
60
- }
61
- }
62
- await next();
63
- });
64
- });
65
- Object.entries(authMap).forEach(([dir, content]) => {
66
- const credentials = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
67
- const [username, password] = line.split(":");
68
- return { username, password };
69
- });
70
- if (credentials.length > 0) {
71
- const authPath = dir === "/" ? "*" : `${dir.replace(/\/$/, "")}/*`;
72
- app.use(authPath, async (c, next) => {
73
- const handler = basicAuth({
74
- verifyUser: (u, p) => credentials.some((cred) => cred.username === u && cred.password === p),
75
- realm: "Restricted Area"
76
- });
77
- return handler(c, next);
78
- });
79
- }
80
- });
32
+ applyHtaccessMiddleware(app, htaccessConfig);
33
+ applyBasicAuth(app, authMap);
81
34
  pages.forEach(({ urlPath, dirPath, component }) => {
82
35
  const routes = [urlPath, `${urlPath}/*`];
83
36
  if (dirPath) {
@@ -96,7 +49,7 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
96
49
  if (value instanceof File || Array.isArray(value) && value[0] instanceof File) {
97
50
  $_FILES[key] = value;
98
51
  } else {
99
- $_POST[key] = value;
52
+ $_POST[key] = String(value);
100
53
  }
101
54
  }
102
55
  const $_COOKIE = getCookie(c);
@@ -106,14 +59,10 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
106
59
  ...$_POST
107
60
  };
108
61
  const $_ENV = typeof process !== "undefined" && process.env ? process.env : c.env || {};
109
- let $_SESSION = {};
110
- const sRaw = getCookie(c, SESS_KEY);
111
- if (sRaw) {
112
- try {
113
- $_SESSION = JSON.parse(decodeURIComponent(sRaw));
114
- } catch {
115
- }
116
- }
62
+ let $_SESSION = getSessionFromCookie(
63
+ c,
64
+ options.sessionCookie?.name
65
+ );
117
66
  let responseStatus = 200;
118
67
  const responseHeaders = {
119
68
  "Content-Type": "text/html; charset=utf-8"
@@ -169,7 +118,7 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
169
118
  }
170
119
  },
171
120
  get_version: () => {
172
- return `MatchboxCGI/v${"0.2.6"}`;
121
+ return `MatchboxCGI/v${"0.3.1"}`;
173
122
  },
174
123
  /**
175
124
  * Returns information about all loaded CGI modules
@@ -184,48 +133,28 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
184
133
  };
185
134
  try {
186
135
  const result = await component(context);
187
- const sessionValue = encodeURIComponent(JSON.stringify($_SESSION));
188
- const sessionOptions = {
189
- path: options.sessionCookie?.path || "/",
190
- httpOnly: true,
191
- sameSite: options.sessionCookie?.sameSite || "Lax"
192
- };
193
- if (options.sessionCookie?.secure !== void 0) {
194
- sessionOptions.secure = options.sessionCookie.secure;
195
- }
196
- if (options.sessionCookie?.domain) {
197
- sessionOptions.domain = options.sessionCookie.domain;
198
- }
199
- if (options.sessionCookie?.maxAge) {
200
- sessionOptions.maxAge = options.sessionCookie.maxAge;
201
- }
136
+ saveSessionToCookie(c, $_SESSION, options.sessionCookie);
202
137
  if (isRedirectObject(result)) {
203
- setCookie(c, SESS_KEY, sessionValue, sessionOptions);
204
138
  return c.redirect(result.url, result.status);
205
139
  }
206
140
  if (result instanceof Response) {
207
- setCookie(c, SESS_KEY, sessionValue, sessionOptions);
208
141
  return result;
209
142
  }
210
- setCookie(c, SESS_KEY, sessionValue, sessionOptions);
211
143
  Object.entries(responseHeaders).forEach(([key, value]) => {
212
144
  c.header(key, value);
213
145
  });
214
146
  const contentType = responseHeaders["content-type"];
215
147
  if (contentType?.includes("application/json")) {
216
- return c.json(
217
- // biome-ignore lint/suspicious/noExplicitAny: to JSON response
218
- result ?? { success: true },
219
- responseStatus
220
- );
148
+ return c.json(result ?? { success: true }, responseStatus);
221
149
  }
222
- return c.html(result, responseStatus);
150
+ return c.html(String(result ?? ""), responseStatus);
223
151
  } catch (error) {
224
152
  return c.html(generateCgiError({ error, $_SERVER }), 500);
225
153
  }
226
154
  });
227
155
  });
228
156
  });
157
+ applyErrorDocumentMiddleware(app, htaccessConfig);
229
158
  return app;
230
159
  };
231
160
  export {
@@ -0,0 +1,9 @@
1
+ import { Context } from 'hono';
2
+ import { AccessControlConfig } from './types.js';
3
+
4
+ /**
5
+ * Create middleware for access control (Order/Allow/Deny)
6
+ */
7
+ declare function createAccessControlMiddleware(config: AccessControlConfig): (c: Context, next: () => Promise<void>) => Promise<Response | void>;
8
+
9
+ export { createAccessControlMiddleware };
@@ -0,0 +1,91 @@
1
+ function matchesIP(clientIP, ruleValue) {
2
+ const ips = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
3
+ for (const ip of ips) {
4
+ if (clientIP === ip) return true;
5
+ if (ip.includes("/")) {
6
+ const [network, bits] = ip.split("/");
7
+ const mask = parseInt(bits, 10);
8
+ if (isSameNetwork(clientIP, network, mask)) return true;
9
+ }
10
+ }
11
+ return false;
12
+ }
13
+ function isSameNetwork(ip1, ip2, maskBits) {
14
+ const ip1Parts = ip1.split(".").map(Number);
15
+ const ip2Parts = ip2.split(".").map(Number);
16
+ let bits = maskBits;
17
+ for (let i = 0; i < 4; i++) {
18
+ if (bits <= 0) break;
19
+ const mask = bits >= 8 ? 255 : 256 - Math.pow(2, 8 - bits);
20
+ if ((ip1Parts[i] & mask) !== (ip2Parts[i] & mask)) {
21
+ return false;
22
+ }
23
+ bits -= 8;
24
+ }
25
+ return true;
26
+ }
27
+ function matchesHost(clientHost, ruleValue) {
28
+ const hosts = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
29
+ for (const host of hosts) {
30
+ if (clientHost === host) return true;
31
+ if (host.startsWith("*.")) {
32
+ const domain = host.slice(2);
33
+ if (clientHost.endsWith(domain)) return true;
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+ function matchesRule(rule, c) {
39
+ switch (rule.type) {
40
+ case "all":
41
+ return true;
42
+ case "ip": {
43
+ const clientIP = c.req.header("x-forwarded-for")?.split(",")[0].trim() || c.req.header("x-real-ip") || c.env?.REMOTE_ADDR || "127.0.0.1";
44
+ return rule.value ? matchesIP(clientIP, rule.value) : false;
45
+ }
46
+ case "host": {
47
+ const clientHost = c.req.header("host") || "";
48
+ return rule.value ? matchesHost(clientHost, rule.value) : false;
49
+ }
50
+ case "env": {
51
+ if (!rule.value) return false;
52
+ const envVar = typeof rule.value === "string" ? rule.value : rule.value[0];
53
+ return c.env?.[envVar] !== void 0;
54
+ }
55
+ default:
56
+ return false;
57
+ }
58
+ }
59
+ function evaluateAccessControl(config, c) {
60
+ const order = config.order || "allow,deny";
61
+ const allowMatches = config.allow.some((rule) => matchesRule(rule, c));
62
+ const denyMatches = config.deny.some((rule) => matchesRule(rule, c));
63
+ switch (order) {
64
+ case "allow,deny":
65
+ if (denyMatches) return false;
66
+ if (allowMatches) return true;
67
+ return false;
68
+ case "deny,allow":
69
+ if (allowMatches) return true;
70
+ if (denyMatches) return false;
71
+ return true;
72
+ case "mutual-failure":
73
+ if (denyMatches) return false;
74
+ if (allowMatches) return true;
75
+ return false;
76
+ default:
77
+ return false;
78
+ }
79
+ }
80
+ function createAccessControlMiddleware(config) {
81
+ return async (c, next) => {
82
+ const allowed = evaluateAccessControl(config, c);
83
+ if (!allowed) {
84
+ return c.text("Forbidden", 403);
85
+ }
86
+ await next();
87
+ };
88
+ }
89
+ export {
90
+ createAccessControlMiddleware
91
+ };