@tknf/matchbox 0.2.5 → 0.3.0

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/dist/cgi.d.ts +10 -21
  2. package/dist/cgi.js +26 -97
  3. package/dist/htaccess/access-control.d.ts +9 -0
  4. package/dist/htaccess/access-control.js +91 -0
  5. package/dist/htaccess/error-document.d.ts +9 -0
  6. package/dist/htaccess/error-document.js +16 -0
  7. package/dist/htaccess/headers.d.ts +32 -0
  8. package/dist/htaccess/headers.js +98 -0
  9. package/dist/htaccess/index.d.ts +8 -0
  10. package/dist/htaccess/index.js +20 -0
  11. package/dist/htaccess/parser.d.ts +9 -0
  12. package/dist/htaccess/parser.js +365 -0
  13. package/dist/htaccess/rewrite.d.ts +13 -0
  14. package/dist/htaccess/rewrite.js +69 -0
  15. package/dist/htaccess/types.d.ts +156 -0
  16. package/dist/htaccess/types.js +0 -0
  17. package/dist/htaccess/utils.d.ts +21 -0
  18. package/dist/htaccess/utils.js +69 -0
  19. package/dist/html.d.ts +1 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +5 -1
  22. package/dist/middleware/auth.d.ts +8 -0
  23. package/dist/middleware/auth.js +28 -0
  24. package/dist/middleware/htaccess.d.ts +13 -0
  25. package/dist/middleware/htaccess.js +42 -0
  26. package/dist/middleware/index.d.ts +10 -0
  27. package/dist/middleware/index.js +14 -0
  28. package/dist/middleware/protected-files.d.ts +8 -0
  29. package/dist/middleware/protected-files.js +14 -0
  30. package/dist/middleware/session.d.ts +16 -0
  31. package/dist/middleware/session.js +37 -0
  32. package/dist/middleware/trailing-slash.d.ts +8 -0
  33. package/dist/middleware/trailing-slash.js +12 -0
  34. package/dist/plugin.js +1 -1
  35. package/dist/with-defaults.d.ts +2 -1
  36. package/dist/with-defaults.js +17 -28
  37. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export { CgiContext, ConfigObject, MatchboxOptions, ModuleInfo, Page, SessionCookieOptions } from './cgi.js';
2
+ export { HtaccessConfig } from './htaccess/types.js';
3
+ export { parseHtaccess } from './htaccess/parser.js';
4
+ export { corsHeaders, securityHeaders } from './htaccess/headers.js';
2
5
  export { createCgi } from './with-defaults.js';
3
6
  import 'hono/types';
4
7
  import 'hono';
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
+ import { corsHeaders, parseHtaccess, securityHeaders } from "./htaccess/index.js";
1
2
  import { createCgi } from "./with-defaults.js";
2
3
  export {
3
- createCgi
4
+ corsHeaders,
5
+ createCgi,
6
+ parseHtaccess,
7
+ securityHeaders
4
8
  };
@@ -0,0 +1,8 @@
1
+ import { Hono } from 'hono';
2
+
3
+ /**
4
+ * Apply Basic Auth middleware to Hono app for each directory
5
+ */
6
+ declare function applyBasicAuth(app: Hono, authMap: Record<string, string>): void;
7
+
8
+ export { applyBasicAuth };
@@ -0,0 +1,28 @@
1
+ import { basicAuth } from "hono/basic-auth";
2
+ function createBasicAuthMiddleware(htpasswdContent, realm = "Restricted Area") {
3
+ const credentials = htpasswdContent.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
4
+ const [username, password] = line.split(":");
5
+ return { username, password };
6
+ });
7
+ if (credentials.length === 0) {
8
+ return async (_c, next) => {
9
+ await next();
10
+ };
11
+ }
12
+ return async (c, next) => {
13
+ const handler = basicAuth({
14
+ verifyUser: (u, p) => credentials.some((cred) => cred.username === u && cred.password === p),
15
+ realm
16
+ });
17
+ return handler(c, next);
18
+ };
19
+ }
20
+ function applyBasicAuth(app, authMap) {
21
+ Object.entries(authMap).forEach(([dir, content]) => {
22
+ const authPath = dir === "/" ? "*" : `${dir.replace(/\/$/, "")}/*`;
23
+ app.use(authPath, createBasicAuthMiddleware(content));
24
+ });
25
+ }
26
+ export {
27
+ applyBasicAuth
28
+ };
@@ -0,0 +1,13 @@
1
+ import { Hono } from 'hono';
2
+ import { HtaccessConfig } from '../htaccess/types.js';
3
+
4
+ /**
5
+ * Apply htaccess middleware to Hono app in the correct order
6
+ */
7
+ declare function applyHtaccessMiddleware(app: Hono, htaccessConfig: HtaccessConfig): void;
8
+ /**
9
+ * Apply error document middleware (must be applied last)
10
+ */
11
+ declare function applyErrorDocumentMiddleware(app: Hono, htaccessConfig: HtaccessConfig): void;
12
+
13
+ export { applyErrorDocumentMiddleware, applyHtaccessMiddleware };
@@ -0,0 +1,42 @@
1
+ import { createRewriteMiddleware } from "../htaccess/rewrite.js";
2
+ import { createHeaderMiddleware } from "../htaccess/headers.js";
3
+ import { createErrorDocumentMiddleware } from "../htaccess/error-document.js";
4
+ import { createAccessControlMiddleware } from "../htaccess/access-control.js";
5
+ function applyHtaccessMiddleware(app, htaccessConfig) {
6
+ Object.entries(htaccessConfig).forEach(([dir, config]) => {
7
+ if (config.headers.length === 0) return;
8
+ const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
9
+ app.use(`${basePath}/*`, createHeaderMiddleware(config.headers));
10
+ });
11
+ Object.entries(htaccessConfig).forEach(([dir, config]) => {
12
+ const allRules = [
13
+ ...config.rewriteRules,
14
+ ...config.redirects.map((r) => ({
15
+ type: "rewrite",
16
+ pattern: `^${r.source}$`,
17
+ target: r.target,
18
+ flags: { redirect: r.code },
19
+ conditions: []
20
+ }))
21
+ ];
22
+ if (allRules.length === 0) return;
23
+ const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
24
+ app.use(`${basePath}/*`, createRewriteMiddleware(allRules, basePath));
25
+ });
26
+ Object.entries(htaccessConfig).forEach(([dir, config]) => {
27
+ if (!config.accessControl) return;
28
+ const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
29
+ app.use(`${basePath}/*`, createAccessControlMiddleware(config.accessControl));
30
+ });
31
+ }
32
+ function applyErrorDocumentMiddleware(app, htaccessConfig) {
33
+ Object.entries(htaccessConfig).forEach(([dir, config]) => {
34
+ if (config.errorDocuments.length === 0) return;
35
+ const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
36
+ app.use(`${basePath}/*`, createErrorDocumentMiddleware(config.errorDocuments, basePath));
37
+ });
38
+ }
39
+ export {
40
+ applyErrorDocumentMiddleware,
41
+ applyHtaccessMiddleware
42
+ };
@@ -0,0 +1,10 @@
1
+ export { applyBasicAuth } from './auth.js';
2
+ export { applyErrorDocumentMiddleware, applyHtaccessMiddleware } from './htaccess.js';
3
+ export { getSessionFromCookie, saveSessionToCookie } from './session.js';
4
+ export { applyProtectedFilesMiddleware } from './protected-files.js';
5
+ export { applyTrailingSlashMiddleware } from './trailing-slash.js';
6
+ import 'hono';
7
+ import '../htaccess/types.js';
8
+ import '../cgi.js';
9
+ import 'hono/types';
10
+ import 'hono/utils/html';
@@ -0,0 +1,14 @@
1
+ import { applyBasicAuth } from "./auth.js";
2
+ import { applyHtaccessMiddleware, applyErrorDocumentMiddleware } from "./htaccess.js";
3
+ import { getSessionFromCookie, saveSessionToCookie } from "./session.js";
4
+ import { applyProtectedFilesMiddleware } from "./protected-files.js";
5
+ import { applyTrailingSlashMiddleware } from "./trailing-slash.js";
6
+ export {
7
+ applyBasicAuth,
8
+ applyErrorDocumentMiddleware,
9
+ applyHtaccessMiddleware,
10
+ applyProtectedFilesMiddleware,
11
+ applyTrailingSlashMiddleware,
12
+ getSessionFromCookie,
13
+ saveSessionToCookie
14
+ };
@@ -0,0 +1,8 @@
1
+ import { Hono } from 'hono';
2
+
3
+ /**
4
+ * Apply protected files middleware to prevent access to sensitive configuration files
5
+ */
6
+ declare function applyProtectedFilesMiddleware(app: Hono): void;
7
+
8
+ export { applyProtectedFilesMiddleware };
@@ -0,0 +1,14 @@
1
+ const PROTECTED_FILES = [".htaccess", ".htpasswd", ".htdigest", ".htgroup"];
2
+ function applyProtectedFilesMiddleware(app) {
3
+ app.use("*", async (c, next) => {
4
+ const path = c.req.path;
5
+ const lastSegment = path.slice(path.lastIndexOf("/") + 1);
6
+ if (PROTECTED_FILES.some((file) => lastSegment === file)) {
7
+ return c.text("Forbidden", 403);
8
+ }
9
+ await next();
10
+ });
11
+ }
12
+ export {
13
+ applyProtectedFilesMiddleware
14
+ };
@@ -0,0 +1,16 @@
1
+ import { Context } from 'hono';
2
+ import { SessionCookieOptions } from '../cgi.js';
3
+ import 'hono/types';
4
+ import 'hono/utils/html';
5
+ import '../htaccess/types.js';
6
+
7
+ /**
8
+ * Get session data from cookie
9
+ */
10
+ declare function getSessionFromCookie(c: Context, sessionCookieName?: string): Record<string, unknown>;
11
+ /**
12
+ * Save session data to cookie
13
+ */
14
+ declare function saveSessionToCookie(c: Context, session: Record<string, unknown>, options?: SessionCookieOptions): void;
15
+
16
+ export { getSessionFromCookie, saveSessionToCookie };
@@ -0,0 +1,37 @@
1
+ import { getCookie, setCookie } from "hono/cookie";
2
+ const DEFAULT_SESSION_COOKIE_NAME = "_SESSION_ID";
3
+ function getSessionFromCookie(c, sessionCookieName) {
4
+ const cookieName = sessionCookieName || DEFAULT_SESSION_COOKIE_NAME;
5
+ const sessionCookie = getCookie(c, cookieName);
6
+ if (sessionCookie) {
7
+ try {
8
+ return JSON.parse(decodeURIComponent(sessionCookie));
9
+ } catch {
10
+ return {};
11
+ }
12
+ }
13
+ return {};
14
+ }
15
+ function saveSessionToCookie(c, session, options) {
16
+ const cookieName = options?.name || DEFAULT_SESSION_COOKIE_NAME;
17
+ const sessionValue = encodeURIComponent(JSON.stringify(session));
18
+ const sessionOptions = {
19
+ path: options?.path || "/",
20
+ httpOnly: true,
21
+ sameSite: options?.sameSite || "Lax"
22
+ };
23
+ if (options?.secure !== void 0) {
24
+ sessionOptions.secure = options.secure;
25
+ }
26
+ if (options?.domain) {
27
+ sessionOptions.domain = options.domain;
28
+ }
29
+ if (options?.maxAge !== void 0) {
30
+ sessionOptions.maxAge = options.maxAge;
31
+ }
32
+ setCookie(c, cookieName, sessionValue, sessionOptions);
33
+ }
34
+ export {
35
+ getSessionFromCookie,
36
+ saveSessionToCookie
37
+ };
@@ -0,0 +1,8 @@
1
+ import { Hono } from 'hono';
2
+
3
+ /**
4
+ * Apply trailing slash middleware to enforce trailing slash on URLs
5
+ */
6
+ declare function applyTrailingSlashMiddleware(app: Hono): void;
7
+
8
+ export { applyTrailingSlashMiddleware };
@@ -0,0 +1,12 @@
1
+ function applyTrailingSlashMiddleware(app) {
2
+ app.use("*", async (c, next) => {
3
+ const path = c.req.path;
4
+ if (!path.endsWith("/") && !path.includes(".")) {
5
+ return c.redirect(`${path}/`, 301);
6
+ }
7
+ await next();
8
+ });
9
+ }
10
+ export {
11
+ applyTrailingSlashMiddleware
12
+ };
package/dist/plugin.js CHANGED
@@ -12,7 +12,7 @@ const MatchboxPlugin = (options = {}) => {
12
12
  exclude: ["matchbox"]
13
13
  },
14
14
  ssr: {
15
- noExternal: ["matchbox"]
15
+ noExternal: true
16
16
  },
17
17
  // .htpasswd, .htaccess, .htdigest, .htgroup files in /public should be treated as raw assets
18
18
  assetsInclude: [
@@ -1,7 +1,8 @@
1
- import { MatchboxOptions } from './cgi.js';
2
1
  import * as hono from 'hono';
3
2
  import * as hono_types from 'hono/types';
3
+ import { MatchboxOptions } from './cgi.js';
4
4
  import 'hono/utils/html';
5
+ import './htaccess/types.js';
5
6
 
6
7
  declare const createCgi: (options?: MatchboxOptions) => hono.Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
7
8
 
@@ -1,4 +1,5 @@
1
1
  import { createCgiWithPages } from "./cgi.js";
2
+ import { parseHtaccess } from "./htaccess/parser.js";
2
3
  const loadPagesFromPublic = () => {
3
4
  const modules = import.meta.glob("/public/**/*.cgi.{tsx,jsx}", {
4
5
  eager: true
@@ -38,40 +39,28 @@ const loadPagesFromPublic = () => {
38
39
  },
39
40
  {}
40
41
  );
41
- const rewriteMap = Object.keys(htaccessFiles).reduce((acc, key) => {
42
+ const htaccessConfig = Object.keys(htaccessFiles).reduce((acc, key) => {
42
43
  const dir = key.replace(basePathRegex, "").replace(/\.htaccess$/, "") || "/";
43
- const lines = htaccessFiles[key].split("\n");
44
- const rules = lines.map((line) => {
45
- const l = line.trim();
46
- if (!l || l.startsWith("#")) return null;
47
- const parts = l.split(/\s+/);
48
- if (parts[0] === "RewriteRule") {
49
- return {
50
- type: "rewrite",
51
- pattern: parts[1],
52
- target: parts[2],
53
- flags: parts[3] || ""
54
- };
55
- }
56
- if (parts[0] === "Redirect") {
57
- return {
58
- type: "redirect",
59
- code: parts[1],
60
- source: parts[2],
61
- target: parts[3]
62
- };
63
- }
64
- return null;
65
- }).filter(Boolean);
66
- acc[dir] = rules;
44
+ const content = htaccessFiles[key];
45
+ try {
46
+ acc[dir] = parseHtaccess(content);
47
+ } catch (error) {
48
+ console.error(`Error parsing .htaccess in ${dir}:`, error.message);
49
+ acc[dir] = {
50
+ rewriteRules: [],
51
+ redirects: [],
52
+ errorDocuments: [],
53
+ headers: []
54
+ };
55
+ }
67
56
  return acc;
68
57
  }, {});
69
- return { pages, authMap, rewriteMap };
58
+ return { pages, authMap, htaccessConfig };
70
59
  };
71
60
  const createCgi = (options) => {
72
61
  const resolvedConfig = typeof __MATCHBOX_CONFIG__ === "undefined" ? {} : __MATCHBOX_CONFIG__;
73
- const { pages, authMap, rewriteMap } = loadPagesFromPublic();
74
- return createCgiWithPages(pages, resolvedConfig, authMap, rewriteMap, options);
62
+ const { pages, authMap, htaccessConfig } = loadPagesFromPublic();
63
+ return createCgiWithPages(pages, resolvedConfig, authMap, htaccessConfig, options);
75
64
  };
76
65
  export {
77
66
  createCgi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tknf/matchbox",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "A Simple Web Server Framework",
5
5
  "keywords": [
6
6
  "cgi",