@aryanbansal-launch/edge-utils 0.1.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/dist/auth/basic-auth.js +29 -0
- package/dist/geo/geo-headers.js +9 -0
- package/dist/index.js +7 -0
- package/dist/redirect/redirect.js +9 -0
- package/dist/response/json.js +6 -0
- package/dist/response/passthrough.js +3 -0
- package/dist/security/block-bots.js +19 -0
- package/dist/security/ip-access.js +13 -0
- package/dist/utils/ip.js +6 -0
- package/package.json +13 -0
- package/readme.md +109 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function protectWithBasicAuth(request, options) {
|
|
2
|
+
const url = new URL(request.url);
|
|
3
|
+
if (!url.hostname.includes(options.hostnameIncludes)) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const authHeader = request.headers.get("Authorization");
|
|
7
|
+
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
8
|
+
return Promise.resolve(new Response("Authentication Required", {
|
|
9
|
+
status: 401,
|
|
10
|
+
headers: {
|
|
11
|
+
"WWW-Authenticate": `Basic realm="${options.realm ?? "Protected Area"}"`,
|
|
12
|
+
"Content-Type": "text/html"
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const base64Credentials = authHeader.split(" ")[1];
|
|
18
|
+
const credentials = atob(base64Credentials);
|
|
19
|
+
const [username, password] = credentials.split(":");
|
|
20
|
+
if (username === options.username &&
|
|
21
|
+
password === options.password) {
|
|
22
|
+
return fetch(request);
|
|
23
|
+
}
|
|
24
|
+
return Promise.resolve(new Response("Unauthorized - Invalid credentials", { status: 401 }));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return Promise.resolve(new Response("Unauthorized - Invalid auth format", { status: 401 }));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function getGeoHeaders(request) {
|
|
2
|
+
return {
|
|
3
|
+
country: request.headers.get("x-country-code"),
|
|
4
|
+
region: request.headers.get("x-region-code"),
|
|
5
|
+
city: request.headers.get("x-city"),
|
|
6
|
+
latitude: request.headers.get("x-latitude"),
|
|
7
|
+
longitude: request.headers.get("x-longitude")
|
|
8
|
+
};
|
|
9
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./response/json.js";
|
|
2
|
+
export * from "./response/passthrough.js";
|
|
3
|
+
export * from "./redirect/redirect.js";
|
|
4
|
+
export * from "./auth/basic-auth.js";
|
|
5
|
+
export * from "./security/ip-access.js";
|
|
6
|
+
export * from "./security/block-bots.js";
|
|
7
|
+
export * from "./geo/geo-headers.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function redirectIfMatch(request, options) {
|
|
2
|
+
const url = new URL(request.url);
|
|
3
|
+
if (url.pathname === options.path &&
|
|
4
|
+
(!options.method || request.method === options.method)) {
|
|
5
|
+
url.pathname = options.to;
|
|
6
|
+
return Response.redirect(url, options.status ?? 301);
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const DEFAULT_BOTS = [
|
|
2
|
+
"claudebot",
|
|
3
|
+
"gptbot",
|
|
4
|
+
"googlebot",
|
|
5
|
+
"bingbot",
|
|
6
|
+
"ahrefsbot",
|
|
7
|
+
"yandexbot",
|
|
8
|
+
"semrushbot",
|
|
9
|
+
"mj12bot",
|
|
10
|
+
"facebookexternalhit",
|
|
11
|
+
"twitterbot"
|
|
12
|
+
];
|
|
13
|
+
export function blockAICrawlers(request, bots = DEFAULT_BOTS) {
|
|
14
|
+
const ua = (request.headers.get("user-agent") || "").toLowerCase();
|
|
15
|
+
if (bots.some(bot => ua.includes(bot))) {
|
|
16
|
+
return new Response("Forbidden: AI crawlers are not allowed.", { status: 403 });
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getClientIP } from "../utils/ip.js";
|
|
2
|
+
export function ipAccessControl(request, options) {
|
|
3
|
+
const ip = getClientIP(request);
|
|
4
|
+
if (!ip)
|
|
5
|
+
return null;
|
|
6
|
+
if (options.deny?.includes(ip)) {
|
|
7
|
+
return new Response("Forbidden", { status: 403 });
|
|
8
|
+
}
|
|
9
|
+
if (options.allow && !options.allow.includes(ip)) {
|
|
10
|
+
return new Response("Forbidden", { status: 403 });
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
package/dist/utils/ip.js
ADDED
package/package.json
ADDED
package/readme.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Edge Utils
|
|
2
|
+
|
|
3
|
+
A collection of high-performance utilities designed for Edge Computing environments (like Cloudflare Workers, Vercel Edge Functions, or Contentstack Launch). These utilities help you handle common tasks like authentication, security, redirection, and geo-location directly at the edge.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Usage Example](#usage-example)
|
|
9
|
+
- [API Reference](#api-reference)
|
|
10
|
+
- [Security](#security)
|
|
11
|
+
- [Authentication](#authentication)
|
|
12
|
+
- [Redirection](#redirection)
|
|
13
|
+
- [Geo Location](#geo-location)
|
|
14
|
+
- [Responses](#responses)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Install the package via npm:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @launch/edge-utils
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage Example
|
|
25
|
+
|
|
26
|
+
Here is a comprehensive example of how to use multiple utilities in a single edge handler:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import {
|
|
30
|
+
jsonResponse,
|
|
31
|
+
passThrough,
|
|
32
|
+
redirectIfMatch,
|
|
33
|
+
protectWithBasicAuth,
|
|
34
|
+
ipAccessControl,
|
|
35
|
+
blockAICrawlers,
|
|
36
|
+
getGeoHeaders
|
|
37
|
+
} from "@launch/edge-utils";
|
|
38
|
+
|
|
39
|
+
export default async function handler(request: Request) {
|
|
40
|
+
// 1. Block known AI crawlers and bots
|
|
41
|
+
const botResponse = blockAICrawlers(request);
|
|
42
|
+
if (botResponse) return botResponse;
|
|
43
|
+
|
|
44
|
+
// 2. IP-based access control
|
|
45
|
+
const ipResponse = ipAccessControl(request, { allow: ["203.0.113.10"] });
|
|
46
|
+
if (ipResponse) return ipResponse;
|
|
47
|
+
|
|
48
|
+
// 3. Basic Authentication for specific hostnames
|
|
49
|
+
const authResponse = await protectWithBasicAuth(request, {
|
|
50
|
+
hostnameIncludes: "test-protected-domain.dev",
|
|
51
|
+
username: "admin",
|
|
52
|
+
password: "securepassword"
|
|
53
|
+
});
|
|
54
|
+
if (authResponse && authResponse.status === 401) return authResponse;
|
|
55
|
+
|
|
56
|
+
// 4. Conditional Redirection
|
|
57
|
+
const redirectResponse = redirectIfMatch(request, {
|
|
58
|
+
path: "/old-page",
|
|
59
|
+
method: "GET",
|
|
60
|
+
to: "/new-page"
|
|
61
|
+
});
|
|
62
|
+
if (redirectResponse) return redirectResponse;
|
|
63
|
+
|
|
64
|
+
// 5. Handle specific routes
|
|
65
|
+
if (new URL(request.url).pathname === "/api/status") {
|
|
66
|
+
return jsonResponse({ status: "ok", timestamp: new Date() });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 6. Access Geo-location headers
|
|
70
|
+
const geo = getGeoHeaders(request);
|
|
71
|
+
console.log("Request from country:", geo.country);
|
|
72
|
+
|
|
73
|
+
// 7. Pass through the request if no utility intercepted it
|
|
74
|
+
return passThrough(request);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API Reference
|
|
79
|
+
|
|
80
|
+
### Security
|
|
81
|
+
|
|
82
|
+
#### `blockAICrawlers(request: Request, bots?: string[]): Response | null`
|
|
83
|
+
Blocks common AI crawlers (like GPTBot, ClaudeBot) based on the `User-Agent` header. Returns a `403 Forbidden` response if a bot is detected, otherwise returns `null`.
|
|
84
|
+
|
|
85
|
+
#### `ipAccessControl(request: Request, options: { allow?: string[], deny?: string[] }): Response | null`
|
|
86
|
+
Restricts access based on the client's IP address. You can provide an `allow` list or a `deny` list.
|
|
87
|
+
|
|
88
|
+
### Authentication
|
|
89
|
+
|
|
90
|
+
#### `protectWithBasicAuth(request: Request, options: AuthOptions): Promise<Response> | null`
|
|
91
|
+
Implements HTTP Basic Authentication. If the hostname matches `hostnameIncludes` and credentials are missing or invalid, it returns a `401 Unauthorized` response with the appropriate headers.
|
|
92
|
+
|
|
93
|
+
### Redirection
|
|
94
|
+
|
|
95
|
+
#### `redirectIfMatch(request: Request, options: RedirectOptions): Response | null`
|
|
96
|
+
Redirects the request if the URL path and HTTP method match the provided options.
|
|
97
|
+
|
|
98
|
+
### Geo Location
|
|
99
|
+
|
|
100
|
+
#### `getGeoHeaders(request: Request): Record<string, string | null>`
|
|
101
|
+
Extracts geo-location information (country, city, region, etc.) from the request headers typically provided by edge platforms.
|
|
102
|
+
|
|
103
|
+
### Responses
|
|
104
|
+
|
|
105
|
+
#### `jsonResponse(data: any, init?: ResponseInit): Response`
|
|
106
|
+
A helper to return a JSON response with the correct `Content-Type` header.
|
|
107
|
+
|
|
108
|
+
#### `passThrough(request: Request): Response`
|
|
109
|
+
Continues the request processing by calling `fetch(request)`. Useful at the end of an edge function.
|