@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 +19 -19
- package/src/handlers/path-rules.ts +1 -1
- package/src/handlers/redirect.test.ts +3 -3
- package/src/handlers/static.ts +4 -4
- package/src/schemas.ts +3 -0
- package/src/utils.test.ts +64 -0
- package/src/utils.ts +35 -4
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@just-be/wildcard",
|
|
3
|
-
"version": "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
|
-
"
|
|
18
|
-
"
|
|
6
|
+
"proxy",
|
|
7
|
+
"redirect",
|
|
19
8
|
"routing",
|
|
20
9
|
"static-site",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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
|
-
"
|
|
32
|
-
"
|
|
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
|
});
|
package/src/handlers/static.ts
CHANGED
|
@@ -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
|
|