@just-be/wildcard 0.6.0 → 0.7.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.
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
1
  {
2
2
  "name": "@just-be/wildcard",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Portable wildcard subdomain routing with pluggable storage backends",
5
- "type": "module",
6
- "main": "./src/index.ts",
7
- "exports": {
8
- ".": "./src/index.ts",
9
- "./schemas": "./src/schemas.ts",
10
- "./handlers": "./src/handlers/index.ts",
11
- "./types": "./src/types.ts"
12
- },
13
- "files": [
14
- "src"
15
- ],
16
5
  "keywords": [
17
- "wildcard",
18
- "subdomain",
6
+ "proxy",
7
+ "redirect",
19
8
  "routing",
20
9
  "static-site",
21
- "redirect",
22
- "proxy"
10
+ "subdomain",
11
+ "wildcard"
23
12
  ],
24
- "author": "Justin Bennett",
25
13
  "license": "MIT",
14
+ "author": "Justin Bennett",
26
15
  "repository": {
27
16
  "type": "git",
28
17
  "url": "git+https://github.com/just-be-dev/just-be.dev.git",
29
18
  "directory": "packages/wildcard"
30
19
  },
31
- "peerDependencies": {
32
- "zod": "^4.0.0"
20
+ "files": [
21
+ "src"
22
+ ],
23
+ "type": "module",
24
+ "main": "./src/index.ts",
25
+ "exports": {
26
+ ".": "./src/index.ts",
27
+ "./schemas": "./src/schemas.ts",
28
+ "./handlers": "./src/handlers/index.ts",
29
+ "./types": "./src/types.ts"
33
30
  },
34
31
  "publishConfig": {
35
32
  "access": "public"
33
+ },
34
+ "peerDependencies": {
35
+ "zod": "^4.0.0"
36
36
  }
37
37
  }
@@ -7,7 +7,7 @@ import { matchPath, proxyRequest } from "../utils";
7
7
  */
8
8
  export async function handlePathRules(
9
9
  request: Request,
10
- config: StaticConfig
10
+ config: StaticConfig,
11
11
  ): Promise<Response | null> {
12
12
  const url = new URL(request.url);
13
13
  const pathname = url.pathname;
@@ -56,10 +56,10 @@ describe("handleRedirect", () => {
56
56
  };
57
57
  const response = await handleRedirect(
58
58
  makeRequest("https://git.just-be.dev/pds?tab=readme"),
59
- config
59
+ config,
60
60
  );
61
61
  expect(response.headers.get("location")).toBe(
62
- "https://github.com/just-be-dev/pds?tab=readme"
62
+ "https://github.com/just-be-dev/pds?tab=readme",
63
63
  );
64
64
  });
65
65
 
@@ -90,7 +90,7 @@ describe("handleRedirect", () => {
90
90
  };
91
91
  const response = await handleRedirect(
92
92
  makeRequest("https://sub.just-be.dev/some/path"),
93
- config
93
+ config,
94
94
  );
95
95
  expect(response.headers.get("location")).toBe("https://example.com");
96
96
  });
@@ -6,7 +6,7 @@ import { handlePathRules } from "./path-rules";
6
6
  export const handleStatic: Handler<StaticConfig, { fileLoader: FileLoader }> = async (
7
7
  request,
8
8
  config,
9
- { fileLoader }
9
+ { fileLoader },
10
10
  ) => {
11
11
  // Check path-level redirects and rewrites first
12
12
  const pathResponse = await handlePathRules(request, config);
@@ -30,7 +30,7 @@ async function handleSpaMode(
30
30
  fileLoader: FileLoader,
31
31
  basePath: string,
32
32
  pathname: string,
33
- request: Request
33
+ request: Request,
34
34
  ): Promise<Response> {
35
35
  // Sanitize paths to prevent directory traversal
36
36
  const sanitizedBase = sanitizePath(basePath);
@@ -79,7 +79,7 @@ async function handleStaticMode(
79
79
  basePath: string,
80
80
  pathname: string,
81
81
  request: Request,
82
- fallback?: string
82
+ fallback?: string,
83
83
  ): Promise<Response> {
84
84
  // Sanitize paths to prevent directory traversal
85
85
  const sanitizedBase = sanitizePath(basePath);
@@ -145,7 +145,7 @@ function buildFileResponse(fileObject: FileObject, key: string, request: Request
145
145
  if (!headers.has("Content-Security-Policy")) {
146
146
  headers.set(
147
147
  "Content-Security-Policy",
148
- "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
148
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
149
149
  );
150
150
  }
151
151
 
package/src/schemas.ts CHANGED
@@ -37,6 +37,7 @@ export const StaticConfigSchema = z
37
37
  fallback: z.string().optional(), // Fallback file path for non-SPA mode (e.g., "404.html")
38
38
  redirects: z.array(PathRedirectSchema).optional(),
39
39
  rewrites: z.array(PathRewriteSchema).optional(),
40
+ password: z.string().optional(), // Name of the Worker secret containing the password for HTTP Basic Auth
40
41
  })
41
42
  .refine((data) => (data.spa && data.fallback ? false : true), {
42
43
  message: "fallback cannot be used with spa mode (spa: true)",
@@ -47,12 +48,14 @@ export const RedirectConfigSchema = z.object({
47
48
  url: safeUrl(),
48
49
  permanent: z.boolean().optional(),
49
50
  preservePath: z.boolean().optional(),
51
+ password: z.string().optional(),
50
52
  });
51
53
 
52
54
  export const RewriteConfigSchema = z.object({
53
55
  type: z.literal("rewrite"),
54
56
  url: safeUrl(),
55
57
  allowedMethods: z.array(HttpMethod).optional().default(["GET", "HEAD", "OPTIONS"]),
58
+ password: z.string().optional(),
56
59
  });
57
60
 
58
61
  export const RouteConfigSchema = z.discriminatedUnion("type", [
package/src/utils.test.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  filterSafeHeaders,
7
7
  isValidSubdomain,
8
8
  matchPath,
9
+ checkBasicAuth,
10
+ unauthorizedResponse,
9
11
  SAFE_REQUEST_HEADERS,
10
12
  } from "./utils";
11
13
 
@@ -343,6 +345,68 @@ describe("isValidSubdomain", () => {
343
345
  });
344
346
  });
345
347
 
348
+ describe("checkBasicAuth", () => {
349
+ function requestWithAuth(username: string, password: string): Request {
350
+ const encoded = btoa(`${username}:${password}`);
351
+ return new Request("https://example.com", {
352
+ headers: { Authorization: `Basic ${encoded}` },
353
+ });
354
+ }
355
+
356
+ it("should return true for correct password with any username", () => {
357
+ expect(checkBasicAuth(requestWithAuth("user", "secret"), "secret")).toBe(true);
358
+ expect(checkBasicAuth(requestWithAuth("admin", "secret"), "secret")).toBe(true);
359
+ expect(checkBasicAuth(requestWithAuth("", "secret"), "secret")).toBe(true);
360
+ });
361
+
362
+ it("should return false for wrong password", () => {
363
+ expect(checkBasicAuth(requestWithAuth("user", "wrong"), "secret")).toBe(false);
364
+ });
365
+
366
+ it("should return false when no Authorization header", () => {
367
+ const request = new Request("https://example.com");
368
+ expect(checkBasicAuth(request, "secret")).toBe(false);
369
+ });
370
+
371
+ it("should return false for non-Basic auth schemes", () => {
372
+ const request = new Request("https://example.com", {
373
+ headers: { Authorization: "Bearer token123" },
374
+ });
375
+ expect(checkBasicAuth(request, "secret")).toBe(false);
376
+ });
377
+
378
+ it("should return false for malformed base64", () => {
379
+ const request = new Request("https://example.com", {
380
+ headers: { Authorization: "Basic !!!invalid!!!" },
381
+ });
382
+ expect(checkBasicAuth(request, "secret")).toBe(false);
383
+ });
384
+
385
+ it("should return false for base64 without colon separator", () => {
386
+ const encoded = btoa("nocolon");
387
+ const request = new Request("https://example.com", {
388
+ headers: { Authorization: `Basic ${encoded}` },
389
+ });
390
+ expect(checkBasicAuth(request, "nocolon")).toBe(false);
391
+ });
392
+
393
+ it("should handle passwords containing colons", () => {
394
+ expect(checkBasicAuth(requestWithAuth("user", "pass:word:here"), "pass:word:here")).toBe(true);
395
+ });
396
+ });
397
+
398
+ describe("unauthorizedResponse", () => {
399
+ it("should return 401 status", () => {
400
+ const response = unauthorizedResponse();
401
+ expect(response.status).toBe(401);
402
+ });
403
+
404
+ it("should include WWW-Authenticate header", () => {
405
+ const response = unauthorizedResponse();
406
+ expect(response.headers.get("WWW-Authenticate")).toBe('Basic realm="Protected"');
407
+ });
408
+ });
409
+
346
410
  describe("matchPath", () => {
347
411
  describe("exact match patterns", () => {
348
412
  it("should match exact paths", () => {
package/src/utils.ts CHANGED
@@ -236,6 +236,40 @@ export function isValidSubdomain(subdomain: string): boolean {
236
236
  return true;
237
237
  }
238
238
 
239
+ /**
240
+ * Checks if a request has valid HTTP Basic Auth credentials
241
+ * Any username is accepted; only the password is validated.
242
+ */
243
+ export function checkBasicAuth(request: Request, expectedPassword: string): boolean {
244
+ const authorization = request.headers.get("Authorization");
245
+ if (!authorization || !authorization.startsWith("Basic ")) {
246
+ return false;
247
+ }
248
+
249
+ try {
250
+ const encoded = authorization.slice("Basic ".length);
251
+ const decoded = atob(encoded);
252
+ const colonIndex = decoded.indexOf(":");
253
+ if (colonIndex === -1) {
254
+ return false;
255
+ }
256
+ const password = decoded.slice(colonIndex + 1);
257
+ return password === expectedPassword;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Returns a 401 Unauthorized response that triggers the browser's Basic Auth dialog
265
+ */
266
+ export function unauthorizedResponse(): Response {
267
+ return new Response("Unauthorized", {
268
+ status: 401,
269
+ headers: { "WWW-Authenticate": 'Basic realm="Protected"' },
270
+ });
271
+ }
272
+
239
273
  /**
240
274
  * Timeout for proxy requests in milliseconds
241
275
  */
@@ -248,10 +282,7 @@ const FETCH_TIMEOUT_MS = 5_000;
248
282
  * @param targetUrl - The target URL to proxy to
249
283
  * @returns Response from the proxied request
250
284
  */
251
- export async function proxyRequest(
252
- request: Request,
253
- targetUrl: URL
254
- ): Promise<Response> {
285
+ export async function proxyRequest(request: Request, targetUrl: URL): Promise<Response> {
255
286
  // Filter headers to only include safe ones (prevents header injection)
256
287
  const safeHeaders = filterSafeHeaders(request.headers);
257
288