@aryanbansal-launch/edge-utils 0.1.11 → 0.1.13
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/bin/launch-config.js +58 -13
- package/bin/launch-edge-local.js +9 -0
- package/bin/launch-edge-test-local.js +53 -0
- package/bin/launch-help.js +39 -0
- package/bin/launch-init.js +251 -9
- package/dist/auth/basic-auth.js +8 -1
- package/dist/dev/rewrite-origin.js +27 -0
- package/dist/index.js +1 -0
- package/examples/local-dev/node_modules/.package-lock.json +23 -0
- package/examples/local-dev/package-lock.json +29 -0
- package/examples/local-dev/package.json +11 -0
- package/examples/local-dev/sample-handler.ts +18 -0
- package/examples/local-dev/worker.ts +11 -0
- package/examples/local-dev/wrangler.toml +6 -0
- package/examples/redirects.csv +12 -0
- package/examples/redirects.json +53 -0
- package/package.json +9 -3
- package/readme.md +400 -15
package/bin/launch-config.js
CHANGED
|
@@ -66,19 +66,64 @@ async function run() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// 1. Redirects
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
source
|
|
78
|
-
destination
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
const redirectMode = await question(`How would you like to add redirects?\n 1) One by one (interactive)\n 2) Bulk import from CSV file\n 3) Bulk import from JSON file\n 4) Skip\nChoose (1-4): `);
|
|
70
|
+
|
|
71
|
+
if (redirectMode === '1') {
|
|
72
|
+
// Interactive mode
|
|
73
|
+
while (true) {
|
|
74
|
+
const addRedirect = await question(`Do you want to add a Redirect?${config.redirects.length > 0 ? ' another?' : ''} (y/n): `);
|
|
75
|
+
if (addRedirect.toLowerCase() !== 'y') break;
|
|
76
|
+
|
|
77
|
+
const source = await question(' Source path (e.g., /source): ');
|
|
78
|
+
const destination = await question(' Destination path (e.g., /destination): ');
|
|
79
|
+
const code = await question(' Status code (default 308): ');
|
|
80
|
+
config.redirects.push({
|
|
81
|
+
source,
|
|
82
|
+
destination,
|
|
83
|
+
statusCode: parseInt(code) || 308
|
|
84
|
+
});
|
|
85
|
+
console.log(`${colors.green} ✔ Redirect added.${colors.reset}`);
|
|
86
|
+
}
|
|
87
|
+
} else if (redirectMode === '2') {
|
|
88
|
+
// CSV import
|
|
89
|
+
const csvPath = await question(' Enter CSV file path (e.g., ./redirects.csv): ');
|
|
90
|
+
try {
|
|
91
|
+
const csvContent = fs.readFileSync(path.resolve(csvPath), 'utf-8');
|
|
92
|
+
const lines = csvContent.split('\n').filter(line => line.trim());
|
|
93
|
+
|
|
94
|
+
// Skip header if present
|
|
95
|
+
const startIndex = lines[0].toLowerCase().includes('source') ? 1 : 0;
|
|
96
|
+
|
|
97
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
98
|
+
const [source, destination, statusCode] = lines[i].split(',').map(s => s.trim());
|
|
99
|
+
if (source && destination) {
|
|
100
|
+
config.redirects.push({
|
|
101
|
+
source,
|
|
102
|
+
destination,
|
|
103
|
+
statusCode: parseInt(statusCode) || 308
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log(`${colors.green} ✔ Imported ${lines.length - startIndex} redirects from CSV.${colors.reset}`);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.log(`${colors.red} ✖ Error reading CSV file: ${error.message}${colors.reset}`);
|
|
110
|
+
}
|
|
111
|
+
} else if (redirectMode === '3') {
|
|
112
|
+
// JSON import
|
|
113
|
+
const jsonPath = await question(' Enter JSON file path (e.g., ./redirects.json): ');
|
|
114
|
+
try {
|
|
115
|
+
const jsonContent = fs.readFileSync(path.resolve(jsonPath), 'utf-8');
|
|
116
|
+
const redirects = JSON.parse(jsonContent);
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(redirects)) {
|
|
119
|
+
config.redirects.push(...redirects);
|
|
120
|
+
console.log(`${colors.green} ✔ Imported ${redirects.length} redirects from JSON.${colors.reset}`);
|
|
121
|
+
} else {
|
|
122
|
+
console.log(`${colors.red} ✖ JSON file must contain an array of redirect objects.${colors.reset}`);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.log(`${colors.red} ✖ Error reading JSON file: ${error.message}${colors.reset}`);
|
|
126
|
+
}
|
|
82
127
|
}
|
|
83
128
|
|
|
84
129
|
// 2. Rewrites
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runs `wrangler dev` using the Wrangler bundled with @aryanbansal-launch/edge-utils.
|
|
4
|
+
* Same as `npx wrangler dev`; extra args are passed through (e.g. --port 8788).
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
let wranglerBin;
|
|
14
|
+
try {
|
|
15
|
+
wranglerBin = require.resolve('wrangler/bin/wrangler.js');
|
|
16
|
+
} catch {
|
|
17
|
+
console.error(
|
|
18
|
+
'Could not resolve wrangler. Reinstall @aryanbansal-launch/edge-utils (it bundles wrangler).'
|
|
19
|
+
);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const wranglerToml = path.join(cwd, 'wrangler.toml');
|
|
25
|
+
if (!fs.existsSync(wranglerToml)) {
|
|
26
|
+
console.error(
|
|
27
|
+
'No wrangler.toml in the current directory.\n' +
|
|
28
|
+
` cwd: ${cwd}\n` +
|
|
29
|
+
' Run this from your project root (where wrangler.toml lives), or run the local wizard first:\n' +
|
|
30
|
+
' npx launch-edge-local'
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const extraArgs = process.argv.slice(2);
|
|
36
|
+
const child = spawn(process.execPath, [wranglerBin, 'dev', ...extraArgs], {
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
cwd,
|
|
39
|
+
env: process.env,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
child.on('error', (err) => {
|
|
43
|
+
console.error(err);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
child.on('exit', (code, signal) => {
|
|
48
|
+
if (signal) {
|
|
49
|
+
process.kill(process.pid, signal);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
process.exit(code ?? 0);
|
|
53
|
+
});
|
package/bin/launch-help.js
CHANGED
|
@@ -66,6 +66,8 @@ ${colors.bright}${colors.green}1. Security & Access Control${colors.reset}
|
|
|
66
66
|
password: string,
|
|
67
67
|
realm?: string
|
|
68
68
|
}
|
|
69
|
+
${colors.dim}Hostname match:${colors.reset} URL host, Host header, or X-Forwarded-Host
|
|
70
|
+
(use with local Wrangler + rewriteRequestToOrigin)
|
|
69
71
|
${colors.dim}Returns:${colors.reset} Promise<Response> | null
|
|
70
72
|
${colors.dim}Example:${colors.reset}
|
|
71
73
|
const auth = await protectWithBasicAuth(request, {
|
|
@@ -148,6 +150,14 @@ ${colors.bright}${colors.green}5. Response Utilities${colors.reset}
|
|
|
148
150
|
${colors.dim}Example:${colors.reset}
|
|
149
151
|
return passThrough(request);
|
|
150
152
|
|
|
153
|
+
${colors.cyan}rewriteRequestToOrigin(request, backendOrigin)${colors.reset}
|
|
154
|
+
Rewrite request URL to your local app (used by functions/dev-worker.edge.js)
|
|
155
|
+
${colors.dim}Parameters:${colors.reset}
|
|
156
|
+
- request: Request object
|
|
157
|
+
- backendOrigin: string (e.g. http://127.0.0.1:3000 from BACKEND_URL)
|
|
158
|
+
${colors.dim}Returns:${colors.reset} Request
|
|
159
|
+
${colors.dim}Note:${colors.reset} Sets X-Forwarded-Host when host changes (Basic Auth + localhost)
|
|
160
|
+
|
|
151
161
|
${colors.bright}${colors.green}6. Configuration${colors.reset}
|
|
152
162
|
|
|
153
163
|
${colors.cyan}generateLaunchConfig(options)${colors.reset}
|
|
@@ -175,6 +185,35 @@ ${colors.dim}──────────────────────
|
|
|
175
185
|
${colors.cyan}npx launch-config${colors.reset}
|
|
176
186
|
Interactive CLI to manage launch.json
|
|
177
187
|
Configure: redirects, rewrites, cache priming
|
|
188
|
+
Supports bulk import from CSV/JSON files
|
|
189
|
+
|
|
190
|
+
${colors.bright}${colors.yellow}🧪 LOCAL TESTING (Wrangler / Miniflare)${colors.reset}
|
|
191
|
+
${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
|
|
192
|
+
|
|
193
|
+
${colors.cyan}npx create-launch-edge local${colors.reset}
|
|
194
|
+
Interactive wizard: pick a preset (redirect, JSON, basic auth, bots, Next.js RSC)
|
|
195
|
+
Writes or updates functions/[proxy].edge.js; creates dev-worker.edge.js + wrangler.toml if missing
|
|
196
|
+
${colors.dim}Alias:${colors.reset} ${colors.cyan}npx launch-edge-local${colors.reset} (same as above)
|
|
197
|
+
|
|
198
|
+
${colors.cyan}npx launch-edge-test-local${colors.reset}
|
|
199
|
+
Starts ${colors.dim}wrangler dev${colors.reset} using the Wrangler bundled with this package
|
|
200
|
+
Run from ${colors.bright}project root${colors.reset} (directory that contains wrangler.toml)
|
|
201
|
+
${colors.dim}Extra args pass through:${colors.reset} --port 8788
|
|
202
|
+
${colors.dim}Example:${colors.reset} npx launch-edge-test-local --var BACKEND_URL=http://127.0.0.1:5173
|
|
203
|
+
|
|
204
|
+
${colors.bright}Typical flow${colors.reset}
|
|
205
|
+
1. ${colors.cyan}npx create-launch-edge local${colors.reset} → choose preset, confirm overwrite if asked
|
|
206
|
+
2. Start your app on the port in ${colors.dim}BACKEND_URL${colors.reset} (see wrangler.toml; default 3000)
|
|
207
|
+
3. ${colors.cyan}npx launch-edge-test-local${colors.reset}
|
|
208
|
+
4. Open the URL Wrangler prints (often ${colors.dim}http://localhost:8787${colors.reset}) + path from the wizard
|
|
209
|
+
|
|
210
|
+
${colors.bright}Quick checks${colors.reset}
|
|
211
|
+
${colors.dim}•${colors.reset} JSON preset: open /api/edge-ping
|
|
212
|
+
${colors.dim}•${colors.reset} Redirect preset: open the legacy path; expect 301
|
|
213
|
+
${colors.dim}•${colors.reset} Basic auth preset: browser prompt (e.g. demo / demo); use hostnameIncludes localhost
|
|
214
|
+
${colors.dim}•${colors.reset} Bots: ${colors.dim}curl -A "GPTBot" http://localhost:8787/${colors.reset} → 403
|
|
215
|
+
|
|
216
|
+
${colors.bright}Before publishing${colors.reset} ${colors.dim}npm run build${colors.reset} in this package so dist/ matches src/
|
|
178
217
|
|
|
179
218
|
${colors.cyan}npx launch-help${colors.reset}
|
|
180
219
|
Display this help guide
|
package/bin/launch-init.js
CHANGED
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import readline from 'node:readline/promises';
|
|
6
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const invokedAsLocal = path.basename(__filename) === 'launch-edge-local.js';
|
|
5
11
|
|
|
6
12
|
const functionsDir = path.join(process.cwd(), 'functions');
|
|
7
13
|
const edgeFile = path.join(functionsDir, '[proxy].edge.js');
|
|
14
|
+
const devWorkerFile = path.join(functionsDir, 'dev-worker.edge.js');
|
|
15
|
+
const wranglerTomlPath = path.join(process.cwd(), 'wrangler.toml');
|
|
8
16
|
|
|
9
|
-
// Simple ANSI colors for better terminal output
|
|
10
17
|
const colors = {
|
|
11
18
|
reset: '\x1b[0m',
|
|
12
19
|
bright: '\x1b[1m',
|
|
20
|
+
dim: '\x1b[2m',
|
|
13
21
|
green: '\x1b[32m',
|
|
14
22
|
yellow: '\x1b[33m',
|
|
15
23
|
cyan: '\x1b[36m',
|
|
16
|
-
blue: '\x1b[34m'
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
red: '\x1b[31m',
|
|
26
|
+
magenta: '\x1b[35m'
|
|
17
27
|
};
|
|
18
28
|
|
|
19
29
|
const template = `
|
|
@@ -84,12 +94,215 @@ export default async function handler(request, context) {
|
|
|
84
94
|
}
|
|
85
95
|
`.trim();
|
|
86
96
|
|
|
97
|
+
const devWorkerTemplate = `
|
|
98
|
+
import { rewriteRequestToOrigin } from "@aryanbansal-launch/edge-utils";
|
|
99
|
+
import handler from "./[proxy].edge.js";
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Wrangler / Miniflare entry: rewrites the request URL to your local app (BACKEND_URL)
|
|
103
|
+
* before running your Launch edge handler. Generated by launch-init.
|
|
104
|
+
*/
|
|
105
|
+
export default {
|
|
106
|
+
async fetch(request, env, ctx) {
|
|
107
|
+
const req = rewriteRequestToOrigin(request, env.BACKEND_URL);
|
|
108
|
+
return handler(req, {});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
`.trim();
|
|
112
|
+
|
|
113
|
+
const wranglerTemplate = `name = "launch-edge-local-dev"
|
|
114
|
+
main = "functions/dev-worker.edge.js"
|
|
115
|
+
compatibility_date = "2024-01-01"
|
|
116
|
+
|
|
117
|
+
[vars]
|
|
118
|
+
BACKEND_URL = "http://127.0.0.1:3000"
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
/** Presets for: npx launch-init local */
|
|
122
|
+
const LOCAL_PRESETS = [
|
|
123
|
+
{
|
|
124
|
+
id: 'redirect',
|
|
125
|
+
title: 'Redirect — path match → new path (301)',
|
|
126
|
+
testPath: '/legacy-demo',
|
|
127
|
+
hint: 'Open /legacy-demo — you should be redirected to /redirect-target',
|
|
128
|
+
template: `
|
|
129
|
+
import { passThrough, redirectIfMatch } from "@aryanbansal-launch/edge-utils";
|
|
130
|
+
|
|
131
|
+
/** Local test: redirect preset (generated by launch-init local) */
|
|
132
|
+
export default async function handler(request, context) {
|
|
133
|
+
const redirectResponse = redirectIfMatch(request, {
|
|
134
|
+
path: "/legacy-demo",
|
|
135
|
+
to: "/redirect-target",
|
|
136
|
+
status: 301
|
|
137
|
+
});
|
|
138
|
+
if (redirectResponse) return redirectResponse;
|
|
139
|
+
|
|
140
|
+
return passThrough(request);
|
|
141
|
+
}
|
|
142
|
+
`.trim()
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'json',
|
|
146
|
+
title: 'JSON — edge-only API route',
|
|
147
|
+
testPath: '/api/edge-ping',
|
|
148
|
+
hint: 'Open /api/edge-ping — JSON is served from the Worker (no backend needed for this path)',
|
|
149
|
+
template: `
|
|
150
|
+
import { jsonResponse, passThrough } from "@aryanbansal-launch/edge-utils";
|
|
151
|
+
|
|
152
|
+
/** Local test: JSON preset (generated by launch-init local) */
|
|
153
|
+
export default async function handler(request, context) {
|
|
154
|
+
const url = new URL(request.url);
|
|
155
|
+
if (url.pathname === "/api/edge-ping") {
|
|
156
|
+
return jsonResponse({ ok: true, source: "edge-utils", path: url.pathname });
|
|
157
|
+
}
|
|
158
|
+
return passThrough(request);
|
|
159
|
+
}
|
|
160
|
+
`.trim()
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'basic-auth',
|
|
164
|
+
title: 'Basic auth — password gate (uses localhost host)',
|
|
165
|
+
testPath: '/',
|
|
166
|
+
hint: 'Browser will prompt for user/pass (demo / demo). Use hostnameIncludes "localhost" in dev.',
|
|
167
|
+
template: `
|
|
168
|
+
import { passThrough, protectWithBasicAuth } from "@aryanbansal-launch/edge-utils";
|
|
169
|
+
|
|
170
|
+
/** Local test: basic auth preset (generated by launch-init local) */
|
|
171
|
+
export default async function handler(request, context) {
|
|
172
|
+
const authResponse = await protectWithBasicAuth(request, {
|
|
173
|
+
hostnameIncludes: "localhost",
|
|
174
|
+
username: "demo",
|
|
175
|
+
password: "demo",
|
|
176
|
+
realm: "Local edge test"
|
|
177
|
+
});
|
|
178
|
+
if (authResponse && authResponse.status === 401) return authResponse;
|
|
179
|
+
|
|
180
|
+
return passThrough(request);
|
|
181
|
+
}
|
|
182
|
+
`.trim()
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'bots',
|
|
186
|
+
title: 'Block AI crawlers — block common bot user-agents',
|
|
187
|
+
testPath: '/',
|
|
188
|
+
hint: 'Normal browser works. Test with curl -A "GPTBot" http://localhost:8787/ — should be blocked',
|
|
189
|
+
template: `
|
|
190
|
+
import { passThrough, blockAICrawlers } from "@aryanbansal-launch/edge-utils";
|
|
191
|
+
|
|
192
|
+
/** Local test: bot block preset (generated by launch-init local) */
|
|
193
|
+
export default async function handler(request, context) {
|
|
194
|
+
const botResponse = blockAICrawlers(request);
|
|
195
|
+
if (botResponse) return botResponse;
|
|
196
|
+
|
|
197
|
+
return passThrough(request);
|
|
198
|
+
}
|
|
199
|
+
`.trim()
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'rsc',
|
|
203
|
+
title: 'Next.js RSC — strip RSC header on selected paths (advanced)',
|
|
204
|
+
testPath: '/your-rsc-page',
|
|
205
|
+
hint: 'Adjust affectedPaths to match a route in your app; used when RSC headers break local proxy',
|
|
206
|
+
template: `
|
|
207
|
+
import { passThrough, handleNextJS_RSC } from "@aryanbansal-launch/edge-utils";
|
|
208
|
+
|
|
209
|
+
/** Local test: Next.js RSC preset (generated by launch-init local) */
|
|
210
|
+
export default async function handler(request, context) {
|
|
211
|
+
const rscResponse = await handleNextJS_RSC(request, {
|
|
212
|
+
affectedPaths: ["/your-rsc-page"]
|
|
213
|
+
});
|
|
214
|
+
if (rscResponse) return rscResponse;
|
|
215
|
+
|
|
216
|
+
return passThrough(request);
|
|
217
|
+
}
|
|
218
|
+
`.trim()
|
|
219
|
+
}
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
function printLocalInstructions(preset) {
|
|
223
|
+
console.log(`\n${colors.bright}${colors.green}Local test — next steps${colors.reset}\n`);
|
|
224
|
+
console.log(` ${colors.cyan}1.${colors.reset} Start your app on the same origin as ${colors.cyan}BACKEND_URL${colors.reset} in wrangler.toml (default ${colors.yellow}http://127.0.0.1:3000${colors.reset}).`);
|
|
225
|
+
console.log(` ${colors.cyan}2.${colors.reset} From project root: ${colors.bright}npx launch-edge-test-local${colors.reset}`);
|
|
226
|
+
console.log(` ${colors.dim}(same as ${colors.reset}${colors.bright}npx wrangler dev${colors.reset}${colors.dim}; extra args pass through, e.g. --port 8788)${colors.reset}`);
|
|
227
|
+
console.log(` ${colors.cyan}3.${colors.reset} Open ${colors.bright}http://localhost:8787${preset.testPath}${colors.reset} ${colors.dim}(Wrangler prints the port if different)${colors.reset}`);
|
|
228
|
+
console.log(`\n ${colors.magenta}Try:${colors.reset} ${preset.hint}\n`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function localWizard() {
|
|
232
|
+
console.log(`\n${colors.bright}${colors.cyan}Edge utilities — test locally (Wrangler / Miniflare)${colors.reset}\n`);
|
|
233
|
+
|
|
234
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
235
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
236
|
+
console.log(`${colors.red}❌ No package.json here.${colors.reset} Run from your project root.\n`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rl = readline.createInterface({ input, output });
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
console.log(`${colors.bright}Pick what you want to try:${colors.reset}\n`);
|
|
244
|
+
LOCAL_PRESETS.forEach((p, i) => {
|
|
245
|
+
console.log(` ${colors.cyan}${i + 1}.${colors.reset} ${p.title}`);
|
|
246
|
+
});
|
|
247
|
+
console.log('');
|
|
248
|
+
|
|
249
|
+
const raw = await rl.question(`${colors.bright}Enter 1–${LOCAL_PRESETS.length} [1]: ${colors.reset}`);
|
|
250
|
+
const n = parseInt(raw || '1', 10);
|
|
251
|
+
if (Number.isNaN(n) || n < 1 || n > LOCAL_PRESETS.length) {
|
|
252
|
+
console.log(`${colors.red}Invalid choice.${colors.reset}\n`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
const preset = LOCAL_PRESETS[n - 1];
|
|
256
|
+
|
|
257
|
+
let overwrite = true;
|
|
258
|
+
if (fs.existsSync(edgeFile)) {
|
|
259
|
+
const ans = await rl.question(
|
|
260
|
+
`${colors.yellow}Overwrite functions/[proxy].edge.js?${colors.reset} [y/N]: `
|
|
261
|
+
);
|
|
262
|
+
overwrite = /^y(es)?$/i.test(ans.trim());
|
|
263
|
+
if (!overwrite) {
|
|
264
|
+
console.log(`\n${colors.blue}Skipped writing [proxy].edge.js.${colors.reset} Still ensuring dev-worker + wrangler…\n`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!fs.existsSync(functionsDir)) {
|
|
269
|
+
fs.mkdirSync(functionsDir, { recursive: true });
|
|
270
|
+
console.log(`${colors.green}✨${colors.reset} Created /functions`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (overwrite) {
|
|
274
|
+
fs.writeFileSync(edgeFile, preset.template + '\n');
|
|
275
|
+
console.log(`${colors.green}✨${colors.reset} Wrote functions/[proxy].edge.js (${preset.id})`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!fs.existsSync(devWorkerFile)) {
|
|
279
|
+
fs.writeFileSync(devWorkerFile, devWorkerTemplate + '\n');
|
|
280
|
+
console.log(`${colors.green}✨${colors.reset} Created /functions/dev-worker.edge.js`);
|
|
281
|
+
} else {
|
|
282
|
+
console.log(`${colors.blue}ℹ${colors.reset} /functions/dev-worker.edge.js already exists`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!fs.existsSync(wranglerTomlPath)) {
|
|
286
|
+
fs.writeFileSync(wranglerTomlPath, wranglerTemplate);
|
|
287
|
+
console.log(`${colors.green}✨${colors.reset} Created wrangler.toml`);
|
|
288
|
+
} else {
|
|
289
|
+
console.log(`${colors.blue}ℹ${colors.reset} wrangler.toml already exists (left unchanged)`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
printLocalInstructions(preset);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error(`${colors.red}Error:${colors.reset}`, err.message);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
} finally {
|
|
297
|
+
rl.close();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
87
301
|
async function init() {
|
|
88
302
|
console.log(`\n${colors.bright}${colors.cyan}🚀 create-launch-edge: Contentstack Launch Initializer${colors.reset}\n`);
|
|
89
303
|
|
|
90
304
|
let actionsTaken = 0;
|
|
91
305
|
|
|
92
|
-
// 1. Root level check: Look for package.json
|
|
93
306
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
94
307
|
if (!fs.existsSync(packageJsonPath)) {
|
|
95
308
|
console.log(`${colors.red}❌ Error: Root directory not detected.${colors.reset}`);
|
|
@@ -100,7 +313,6 @@ async function init() {
|
|
|
100
313
|
}
|
|
101
314
|
|
|
102
315
|
try {
|
|
103
|
-
// 2. Folder existence check
|
|
104
316
|
if (!fs.existsSync(functionsDir)) {
|
|
105
317
|
fs.mkdirSync(functionsDir, { recursive: true });
|
|
106
318
|
console.log(`${colors.green}✨ New:${colors.reset} Created /functions directory`);
|
|
@@ -109,7 +321,6 @@ async function init() {
|
|
|
109
321
|
console.log(`${colors.blue}ℹ️ Existing:${colors.reset} /functions directory already found`);
|
|
110
322
|
}
|
|
111
323
|
|
|
112
|
-
// 3. File existence check (don't overwrite user's work)
|
|
113
324
|
if (!fs.existsSync(edgeFile)) {
|
|
114
325
|
fs.writeFileSync(edgeFile, template + '\n');
|
|
115
326
|
console.log(`${colors.green}✨ New:${colors.reset} Created /functions/[proxy].edge.js`);
|
|
@@ -118,7 +329,22 @@ async function init() {
|
|
|
118
329
|
console.log(`${colors.blue}ℹ️ Existing:${colors.reset} /functions/[proxy].edge.js already found`);
|
|
119
330
|
}
|
|
120
331
|
|
|
121
|
-
|
|
332
|
+
if (!fs.existsSync(devWorkerFile)) {
|
|
333
|
+
fs.writeFileSync(devWorkerFile, devWorkerTemplate + '\n');
|
|
334
|
+
console.log(`${colors.green}✨ New:${colors.reset} Created /functions/dev-worker.edge.js`);
|
|
335
|
+
actionsTaken++;
|
|
336
|
+
} else {
|
|
337
|
+
console.log(`${colors.blue}ℹ️ Existing:${colors.reset} /functions/dev-worker.edge.js already found`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!fs.existsSync(wranglerTomlPath)) {
|
|
341
|
+
fs.writeFileSync(wranglerTomlPath, wranglerTemplate);
|
|
342
|
+
console.log(`${colors.green}✨ New:${colors.reset} Created wrangler.toml (local edge dev)`);
|
|
343
|
+
actionsTaken++;
|
|
344
|
+
} else {
|
|
345
|
+
console.log(`${colors.blue}ℹ️ Existing:${colors.reset} wrangler.toml already found — skipped creating default`);
|
|
346
|
+
}
|
|
347
|
+
|
|
122
348
|
if (actionsTaken === 0) {
|
|
123
349
|
console.log(`\n${colors.bright}${colors.blue}🏁 Everything is already set up!${colors.reset}`);
|
|
124
350
|
console.log(`No changes were made to your existing files.`);
|
|
@@ -130,8 +356,10 @@ async function init() {
|
|
|
130
356
|
console.log(`\n${colors.bright}Next Steps:${colors.reset}`);
|
|
131
357
|
console.log(`1. Open ${colors.cyan}functions/[proxy].edge.js${colors.reset}`);
|
|
132
358
|
console.log(`2. Customize your redirects, auth, and RSC paths`);
|
|
133
|
-
console.log(`3. Deploy your project to Contentstack Launch
|
|
134
|
-
|
|
359
|
+
console.log(`3. Deploy your project to Contentstack Launch`);
|
|
360
|
+
console.log(`4. Test edge locally: ${colors.bright}npx launch-edge-local${colors.reset} (wizard), then ${colors.bright}npx launch-edge-test-local${colors.reset}`);
|
|
361
|
+
console.log(` (${colors.dim}Wrangler is bundled; run app on BACKEND_URL for pass-through routes)${colors.reset}\n`);
|
|
362
|
+
|
|
135
363
|
console.log(`${colors.blue}Documentation:${colors.reset} https://github.com/AryanBansal-launch/launch-edge-utils#readme\n`);
|
|
136
364
|
} catch (error) {
|
|
137
365
|
console.error(`\n${colors.red}❌ Error during setup:${colors.reset}`, error.message);
|
|
@@ -139,4 +367,18 @@ async function init() {
|
|
|
139
367
|
}
|
|
140
368
|
}
|
|
141
369
|
|
|
142
|
-
|
|
370
|
+
const args = process.argv.slice(2);
|
|
371
|
+
const runLocal =
|
|
372
|
+
invokedAsLocal ||
|
|
373
|
+
args[0] === 'local' ||
|
|
374
|
+
args.includes('--local');
|
|
375
|
+
|
|
376
|
+
async function main() {
|
|
377
|
+
if (runLocal) {
|
|
378
|
+
await localWizard();
|
|
379
|
+
} else {
|
|
380
|
+
await init();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
main();
|
package/dist/auth/basic-auth.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export function protectWithBasicAuth(request, options) {
|
|
2
2
|
const url = new URL(request.url);
|
|
3
|
-
|
|
3
|
+
// Match URL host, Host header, or X-Forwarded-Host (set by rewriteRequestToOrigin when
|
|
4
|
+
// the browser hits localhost:8787 but the request URL is rewritten to 127.0.0.1:3000).
|
|
5
|
+
const hostHeader = request.headers.get("host")?.split(":")[0] ?? "";
|
|
6
|
+
const forwardedHost = request.headers.get("x-forwarded-host")?.split(":")[0] ?? "";
|
|
7
|
+
const hostMatches = url.hostname.includes(options.hostnameIncludes) ||
|
|
8
|
+
hostHeader.includes(options.hostnameIncludes) ||
|
|
9
|
+
forwardedHost.includes(options.hostnameIncludes);
|
|
10
|
+
if (!hostMatches) {
|
|
4
11
|
return null;
|
|
5
12
|
}
|
|
6
13
|
const authHeader = request.headers.get("Authorization");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrites the request URL so its origin (and optional path prefix from `backendOrigin`)
|
|
3
|
+
* points at your local app. Use with Wrangler / Miniflare so `passThrough(request)` and
|
|
4
|
+
* other `fetch(request)` calls reach the dev server instead of the Worker dev port.
|
|
5
|
+
*
|
|
6
|
+
* When the incoming host differs from the backend host, sets `X-Forwarded-Host` to the
|
|
7
|
+
* original `incoming.host` (unless already present) so helpers like `protectWithBasicAuth`
|
|
8
|
+
* can still match `localhost` while the URL points at `127.0.0.1`.
|
|
9
|
+
*/
|
|
10
|
+
export function rewriteRequestToOrigin(request, backendOrigin) {
|
|
11
|
+
const base = new URL(backendOrigin);
|
|
12
|
+
const incoming = new URL(request.url);
|
|
13
|
+
const prefix = base.pathname === "/" ? "" : base.pathname.replace(/\/$/, "");
|
|
14
|
+
const path = `${prefix}${incoming.pathname}${incoming.search}${incoming.hash}`;
|
|
15
|
+
const target = new URL(path, base.origin);
|
|
16
|
+
const headers = new Headers(request.headers);
|
|
17
|
+
if (incoming.hostname !== target.hostname && !headers.has("x-forwarded-host")) {
|
|
18
|
+
headers.set("x-forwarded-host", incoming.host);
|
|
19
|
+
}
|
|
20
|
+
return new Request(target.toString(), {
|
|
21
|
+
method: request.method,
|
|
22
|
+
headers,
|
|
23
|
+
body: request.body,
|
|
24
|
+
redirect: request.redirect,
|
|
25
|
+
...(request.body != null ? { duplex: "half" } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "local-dev-example",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"../..": {
|
|
7
|
+
"name": "@aryanbansal-launch/edge-utils",
|
|
8
|
+
"version": "0.1.5",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"wrangler": "^3.114.0"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"launch-edge-local": "bin/launch-edge-local.js",
|
|
15
|
+
"launch-init": "bin/launch-init.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"node_modules/@aryanbansal-launch/edge-utils": {
|
|
19
|
+
"resolved": "../..",
|
|
20
|
+
"link": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "local-dev-example",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"": {
|
|
7
|
+
"name": "local-dev-example",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@aryanbansal-launch/edge-utils": "file:../.."
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"../..": {
|
|
13
|
+
"name": "@aryanbansal-launch/edge-utils",
|
|
14
|
+
"version": "0.1.5",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"wrangler": "^3.114.0"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"launch-edge-local": "bin/launch-edge-local.js",
|
|
21
|
+
"launch-init": "bin/launch-init.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"node_modules/@aryanbansal-launch/edge-utils": {
|
|
25
|
+
"resolved": "../..",
|
|
26
|
+
"link": true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsonResponse, passThrough } from "@aryanbansal-launch/edge-utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Launch-style handler for local Miniflare testing.
|
|
5
|
+
*/
|
|
6
|
+
export default async function handler(request: Request, _context: unknown) {
|
|
7
|
+
const url = new URL(request.url);
|
|
8
|
+
|
|
9
|
+
if (url.pathname === "/api/edge-ping") {
|
|
10
|
+
return jsonResponse({
|
|
11
|
+
ok: true,
|
|
12
|
+
source: "edge-utils-example",
|
|
13
|
+
path: url.pathname,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return passThrough(request);
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { rewriteRequestToOrigin } from "@aryanbansal-launch/edge-utils";
|
|
2
|
+
import handler from "./sample-handler.js";
|
|
3
|
+
|
|
4
|
+
type Env = { BACKEND_URL: string };
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
async fetch(request: Request, env: Env, _ctx: unknown) {
|
|
8
|
+
const req = rewriteRequestToOrigin(request, env.BACKEND_URL);
|
|
9
|
+
return handler(req, {});
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
source,destination,statusCode
|
|
2
|
+
/old-blog/post-1,/blog/post-1,301
|
|
3
|
+
/old-blog/post-2,/blog/post-2,301
|
|
4
|
+
/old-blog/post-3,/blog/post-3,301
|
|
5
|
+
/products/old-sku-123,/products/new-sku-456,308
|
|
6
|
+
/products/old-sku-789,/products/new-sku-101,308
|
|
7
|
+
/legacy/about,/about,301
|
|
8
|
+
/legacy/contact,/contact,301
|
|
9
|
+
/legacy/pricing,/pricing,301
|
|
10
|
+
/old-shop/*,/shop/*,301
|
|
11
|
+
/archive/*,/blog/archive/*,301
|
|
12
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"source": "/old-blog/post-1",
|
|
4
|
+
"destination": "/blog/post-1",
|
|
5
|
+
"statusCode": 301
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"source": "/old-blog/post-2",
|
|
9
|
+
"destination": "/blog/post-2",
|
|
10
|
+
"statusCode": 301
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"source": "/old-blog/post-3",
|
|
14
|
+
"destination": "/blog/post-3",
|
|
15
|
+
"statusCode": 301
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"source": "/products/old-sku-123",
|
|
19
|
+
"destination": "/products/new-sku-456",
|
|
20
|
+
"statusCode": 308
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"source": "/products/old-sku-789",
|
|
24
|
+
"destination": "/products/new-sku-101",
|
|
25
|
+
"statusCode": 308
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"source": "/legacy/about",
|
|
29
|
+
"destination": "/about",
|
|
30
|
+
"statusCode": 301
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"source": "/legacy/contact",
|
|
34
|
+
"destination": "/contact",
|
|
35
|
+
"statusCode": 301
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"source": "/legacy/pricing",
|
|
39
|
+
"destination": "/pricing",
|
|
40
|
+
"statusCode": 301
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"source": "/old-shop/*",
|
|
44
|
+
"destination": "/shop/*",
|
|
45
|
+
"statusCode": 301
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"source": "/archive/*",
|
|
49
|
+
"destination": "/blog/archive/*",
|
|
50
|
+
"statusCode": 301
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryanbansal-launch/edge-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -15,14 +15,20 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"create-launch-edge": "bin/launch-init.js",
|
|
17
17
|
"launch-config": "bin/launch-config.js",
|
|
18
|
-
"launch-help": "bin/launch-help.js"
|
|
18
|
+
"launch-help": "bin/launch-help.js",
|
|
19
|
+
"launch-edge-local": "./bin/launch-edge-local.js",
|
|
20
|
+
"launch-edge-test-local": "./bin/launch-edge-test-local.js"
|
|
19
21
|
},
|
|
20
22
|
"exports": {
|
|
21
23
|
".": "./dist/index.js"
|
|
22
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"wrangler": "^3.114.0"
|
|
27
|
+
},
|
|
23
28
|
"files": [
|
|
24
29
|
"dist",
|
|
25
|
-
"bin"
|
|
30
|
+
"bin",
|
|
31
|
+
"examples"
|
|
26
32
|
],
|
|
27
33
|
"scripts": {
|
|
28
34
|
"build": "tsc",
|
package/readme.md
CHANGED
|
@@ -10,12 +10,26 @@ A comprehensive toolkit for [Contentstack Launch](https://www.contentstack.com/d
|
|
|
10
10
|
## 📋 Table of Contents
|
|
11
11
|
|
|
12
12
|
- [Quick Start](#-quick-start)
|
|
13
|
+
- [User journey](#-user-journey)
|
|
13
14
|
- [Usage Flow](#-usage-flow)
|
|
14
15
|
- [Complete API Reference](#-complete-api-reference)
|
|
15
16
|
- [Real-World Examples](#-real-world-examples)
|
|
16
17
|
- [CLI Commands](#-cli-commands)
|
|
17
18
|
- [Platform Support](#-platform-support)
|
|
18
19
|
|
|
20
|
+
A lightweight, high-performance toolkit specifically designed for **Contentstack Launch Edge Functions**. Speed up your development with production-ready utilities for security, authentication, routing, and Next.js compatibility—all optimized to run at the edge.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ Features
|
|
25
|
+
|
|
26
|
+
- 🛡️ **Security First**: Block AI crawlers and manage IP access with ease.
|
|
27
|
+
- 🔐 **Edge Auth**: Implement Basic Auth directly at the edge for specific hostnames.
|
|
28
|
+
- 📍 **Geo-Aware**: Easily extract location data from request headers.
|
|
29
|
+
- ⚛️ **Next.js Ready**: Built-in fixes for RSC header issues on Launch proxies.
|
|
30
|
+
- 🔀 **Smart Routing**: Declarative redirects based on path and method.
|
|
31
|
+
- ⚡ **Lean edge code**: No runtime deps inside the utilities you import at the edge; the npm package bundles **Wrangler** so local testing works without a separate install.
|
|
32
|
+
|
|
19
33
|
---
|
|
20
34
|
|
|
21
35
|
## ⚡ Quick Start
|
|
@@ -51,6 +65,98 @@ npx launch-help
|
|
|
51
65
|
|
|
52
66
|
---
|
|
53
67
|
|
|
68
|
+
## 🧭 User journey
|
|
69
|
+
|
|
70
|
+
Step-by-step paths for the three most common goals. Adjust paths and ports to match your app.
|
|
71
|
+
|
|
72
|
+
### Scenario 1: Use a particular edge utility in production
|
|
73
|
+
|
|
74
|
+
You want **one or more** helpers from this library (for example Basic Auth, bot blocking, or `redirectIfMatch`) in your **Contentstack Launch** edge handler.
|
|
75
|
+
|
|
76
|
+
1. **Install the package** (from your project root, next to `package.json`):
|
|
77
|
+
```bash
|
|
78
|
+
npm install @aryanbansal-launch/edge-utils
|
|
79
|
+
```
|
|
80
|
+
2. **Scaffold the edge function file** (skip if `functions/[proxy].edge.js` already exists):
|
|
81
|
+
```bash
|
|
82
|
+
npx create-launch-edge
|
|
83
|
+
```
|
|
84
|
+
This creates `functions/` and a starter `functions/[proxy].edge.js` if missing.
|
|
85
|
+
3. **Open** `functions/[proxy].edge.js` in your editor.
|
|
86
|
+
4. **Import** only what you need from the package, for example:
|
|
87
|
+
```javascript
|
|
88
|
+
import { blockAICrawlers, redirectIfMatch, passThrough } from "@aryanbansal-launch/edge-utils";
|
|
89
|
+
```
|
|
90
|
+
5. **Wire your handler**: call each utility in order; when a function returns a `Response`, return it immediately; otherwise continue until `passThrough(request)` (or your own `fetch`) sends traffic to the origin.
|
|
91
|
+
6. **Review the API** (optional):
|
|
92
|
+
```bash
|
|
93
|
+
npx launch-help
|
|
94
|
+
```
|
|
95
|
+
7. **Deploy** your site through **Contentstack Launch** using your normal workflow (CLI or UI). Launch runs `functions/[proxy].edge.js` at the edge before traffic hits your app.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Scenario 2: Test edge utilities locally (Wrangler / Miniflare)
|
|
100
|
+
|
|
101
|
+
You want to **try** a preset (redirect, JSON route, basic auth, bots, or Next.js RSC) **on your machine** before deploying.
|
|
102
|
+
|
|
103
|
+
1. **Install the package**:
|
|
104
|
+
```bash
|
|
105
|
+
npm install @aryanbansal-launch/edge-utils
|
|
106
|
+
```
|
|
107
|
+
2. **One-time scaffold** (creates `functions/dev-worker.edge.js` and `wrangler.toml` if they are missing; safe to run again):
|
|
108
|
+
```bash
|
|
109
|
+
npx create-launch-edge
|
|
110
|
+
```
|
|
111
|
+
3. **Start the local wizard** (pick a preset interactively):
|
|
112
|
+
```bash
|
|
113
|
+
npx launch-edge-local
|
|
114
|
+
```
|
|
115
|
+
Or:
|
|
116
|
+
```bash
|
|
117
|
+
npx create-launch-edge local
|
|
118
|
+
```
|
|
119
|
+
4. **Enter a number** `1`–`5` for the preset. If `[proxy].edge.js` already exists, confirm overwrite when prompted (`y`).
|
|
120
|
+
5. **Align the backend URL** with your app: open `wrangler.toml` and set `[vars] BACKEND_URL` to your dev server (default `http://127.0.0.1:3000`). Change the port if your app uses something else (for example `5173` for Vite).
|
|
121
|
+
6. **Start your app** in another terminal (for example `npm run dev`) so it listens on that host/port.
|
|
122
|
+
7. **Start the local Worker** from the **same project root** as `wrangler.toml`:
|
|
123
|
+
```bash
|
|
124
|
+
npx launch-edge-test-local
|
|
125
|
+
```
|
|
126
|
+
This runs the bundled `wrangler dev`. Extra args are supported, e.g. `npx launch-edge-test-local --port 8788` or `--var BACKEND_URL=http://127.0.0.1:5173`.
|
|
127
|
+
8. **Open the URL** Wrangler prints (often `http://localhost:8787`) and the path the wizard suggests (for example `/api/edge-ping` for the JSON preset).
|
|
128
|
+
9. **Optional checks**: see `npx launch-help` under **Local testing** for curl / bot tests.
|
|
129
|
+
|
|
130
|
+
**Note:** After you change **source** in `@aryanbansal-launch/edge-utils`, run `npm run build` in the package before linking or publishing so `dist/` matches `src/`.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### Scenario 3: Redirects, rewrites, and cache priming (`launch.json`)
|
|
135
|
+
|
|
136
|
+
You want **config-driven** redirects, rewrites, or cache priming **without** coding them in the edge file—Launch reads **`launch.json`** at the project root.
|
|
137
|
+
|
|
138
|
+
1. **Install the package** (includes the `launch-config` CLI):
|
|
139
|
+
```bash
|
|
140
|
+
npm install @aryanbansal-launch/edge-utils
|
|
141
|
+
```
|
|
142
|
+
2. **Run the interactive configurator** from your **project root**:
|
|
143
|
+
```bash
|
|
144
|
+
npx launch-config
|
|
145
|
+
```
|
|
146
|
+
3. **Follow the prompts** to add:
|
|
147
|
+
- **Redirects** (one-by-one or bulk),
|
|
148
|
+
- **Rewrites** (source path → destination),
|
|
149
|
+
- **Cache priming URLs** (relative paths only, as required by Launch).
|
|
150
|
+
4. **Bulk import** (optional): choose CSV or JSON when the CLI asks, and provide a file path to import many redirects at once.
|
|
151
|
+
5. **Confirm** `launch.json` is created or updated at the **root** of your Launch project (alongside `package.json`).
|
|
152
|
+
6. **Deploy** through Contentstack Launch so the new configuration is applied.
|
|
153
|
+
|
|
154
|
+
**Alternative (code):** you can build `launch.json` in code with `generateLaunchConfig` from this package and write the file yourself—see the **Configuration** subsection in the [Complete API Reference](#-complete-api-reference) below.
|
|
155
|
+
|
|
156
|
+
**Using both:** keep bulk static rules in `launch.json` and use `functions/[proxy].edge.js` for dynamic logic (geo, cookies, A/B tests). They can coexist on the same project.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
54
160
|
## 🔄 Usage Flow
|
|
55
161
|
|
|
56
162
|
### Understanding Edge Functions
|
|
@@ -291,7 +397,7 @@ Add Basic Authentication to protect environments.
|
|
|
291
397
|
**Parameters:**
|
|
292
398
|
- `request` (Request) - The incoming request object
|
|
293
399
|
- `options` (object)
|
|
294
|
-
- `hostnameIncludes` (string) -
|
|
400
|
+
- `hostnameIncludes` (string) - Substring match against the request URL hostname, the `Host` header, or `X-Forwarded-Host` (without port). `rewriteRequestToOrigin` sets `X-Forwarded-Host` when the URL is rewritten so `hostnameIncludes: "localhost"` works with `npx launch-edge-test-local` while `BACKEND_URL` uses `127.0.0.1`.
|
|
295
401
|
- `username` (string) - Username for authentication
|
|
296
402
|
- `password` (string) - Password for authentication
|
|
297
403
|
- `realm` (string, optional) - Auth realm name (default: "Protected Area")
|
|
@@ -299,7 +405,7 @@ Add Basic Authentication to protect environments.
|
|
|
299
405
|
**Returns:** `Promise<Response> | null`
|
|
300
406
|
- Returns `401 Unauthorized` if auth fails
|
|
301
407
|
- Returns authenticated response if credentials valid
|
|
302
|
-
- Returns `null` if hostname
|
|
408
|
+
- Returns `null` if neither the URL hostname nor the `Host` header matches `hostnameIncludes`
|
|
303
409
|
|
|
304
410
|
**Example:**
|
|
305
411
|
```javascript
|
|
@@ -387,14 +493,164 @@ if (redirect) return redirect;
|
|
|
387
493
|
- A/B testing redirects
|
|
388
494
|
- Maintenance mode redirects
|
|
389
495
|
|
|
390
|
-
**When to Use:**
|
|
391
|
-
- **Edge Functions (this utility)**: Dynamic redirects requiring logic (cookies, headers, geo)
|
|
392
|
-
- **launch.json**: Static path-to-path redirects (better performance)
|
|
393
|
-
|
|
394
496
|
**Learn More:** [Edge URL Redirects](https://www.contentstack.com/docs/developers/launch/edge-url-redirects)
|
|
395
497
|
|
|
396
498
|
---
|
|
397
499
|
|
|
500
|
+
### 📊 Redirects: Config vs Edge Functions
|
|
501
|
+
|
|
502
|
+
Contentstack Launch offers **two ways** to handle redirects. Choose the right approach based on your needs:
|
|
503
|
+
|
|
504
|
+
#### Option 1: Config-Based Redirects (`launch.json`)
|
|
505
|
+
|
|
506
|
+
**Best for:** Static, predictable redirects that don't require logic
|
|
507
|
+
|
|
508
|
+
**Pros:**
|
|
509
|
+
- ⚡ **Faster**: No edge function execution overhead
|
|
510
|
+
- 🎯 **Simpler**: Pure configuration, no code needed
|
|
511
|
+
- 📦 **Bulk-friendly**: Easy to manage hundreds/thousands of redirects
|
|
512
|
+
- 🔧 **Easy updates**: Use `npx launch-config` CLI with CSV/JSON import
|
|
513
|
+
|
|
514
|
+
**Cons:**
|
|
515
|
+
- ❌ No dynamic logic (can't check cookies, headers, geo, etc.)
|
|
516
|
+
- ❌ No conditional redirects based on request data
|
|
517
|
+
- ❌ Limited to exact path or wildcard matching
|
|
518
|
+
|
|
519
|
+
**Setup:**
|
|
520
|
+
```bash
|
|
521
|
+
# Interactive CLI
|
|
522
|
+
npx launch-config
|
|
523
|
+
|
|
524
|
+
# Or bulk import from CSV/JSON
|
|
525
|
+
npx launch-config
|
|
526
|
+
# Choose option 2 or 3 for bulk import
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Example `launch.json`:**
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"redirects": [
|
|
533
|
+
{
|
|
534
|
+
"source": "/old-blog/:slug",
|
|
535
|
+
"destination": "/blog/:slug",
|
|
536
|
+
"statusCode": 301
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"source": "/products/old-*",
|
|
540
|
+
"destination": "/products/new-*",
|
|
541
|
+
"statusCode": 308
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**When to use:**
|
|
548
|
+
- ✅ SEO migrations with hundreds of URL changes
|
|
549
|
+
- ✅ Simple path rewrites (e.g., `/old-path` → `/new-path`)
|
|
550
|
+
- ✅ Bulk product SKU redirects
|
|
551
|
+
- ✅ Static site restructuring
|
|
552
|
+
- ✅ Predictable, rule-based redirects
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
#### Option 2: Edge Function Redirects (This Package)
|
|
557
|
+
|
|
558
|
+
**Best for:** Dynamic redirects requiring logic or request inspection
|
|
559
|
+
|
|
560
|
+
**Pros:**
|
|
561
|
+
- 🧠 **Smart**: Access cookies, headers, geo-location, user-agent
|
|
562
|
+
- 🎨 **Flexible**: Complex conditional logic
|
|
563
|
+
- 🔄 **Dynamic**: Redirect based on A/B tests, feature flags, user data
|
|
564
|
+
- 🌍 **Contextual**: Different redirects per country/region
|
|
565
|
+
|
|
566
|
+
**Cons:**
|
|
567
|
+
- 🐌 Slightly slower (edge function execution)
|
|
568
|
+
- 🔧 Requires code changes
|
|
569
|
+
- 📝 More complex for simple redirects
|
|
570
|
+
|
|
571
|
+
**Setup:**
|
|
572
|
+
```javascript
|
|
573
|
+
import { redirectIfMatch } from '@aryanbansal-launch/edge-utils';
|
|
574
|
+
|
|
575
|
+
export default async function handler(request) {
|
|
576
|
+
// Dynamic redirect based on cookie
|
|
577
|
+
const cookie = request.headers.get('cookie');
|
|
578
|
+
if (cookie?.includes('beta=true')) {
|
|
579
|
+
return Response.redirect(new URL('/beta-features', request.url), 302);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Geo-based redirect
|
|
583
|
+
const country = request.headers.get('x-cs-country');
|
|
584
|
+
if (country === 'FR') {
|
|
585
|
+
return Response.redirect('https://fr.mysite.com', 302);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Conditional redirect
|
|
589
|
+
const redirect = redirectIfMatch(request, {
|
|
590
|
+
path: "/old-page",
|
|
591
|
+
to: "/new-page",
|
|
592
|
+
method: "GET",
|
|
593
|
+
status: 301
|
|
594
|
+
});
|
|
595
|
+
if (redirect) return redirect;
|
|
596
|
+
|
|
597
|
+
return passThrough(request);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**When to use:**
|
|
602
|
+
- ✅ Geo-location based redirects
|
|
603
|
+
- ✅ A/B testing redirects
|
|
604
|
+
- ✅ Cookie/session-based routing
|
|
605
|
+
- ✅ User-agent specific redirects (mobile vs desktop)
|
|
606
|
+
- ✅ Feature flag redirects
|
|
607
|
+
- ✅ Maintenance mode with exceptions
|
|
608
|
+
- ✅ Complex conditional logic
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
#### Quick Decision Guide
|
|
613
|
+
|
|
614
|
+
| Scenario | Use Config | Use Edge Function |
|
|
615
|
+
|----------|-----------|-------------------|
|
|
616
|
+
| 500+ simple URL redirects | ✅ | ❌ |
|
|
617
|
+
| SEO migration from old site | ✅ | ❌ |
|
|
618
|
+
| Redirect based on country | ❌ | ✅ |
|
|
619
|
+
| A/B test routing | ❌ | ✅ |
|
|
620
|
+
| Cookie-based redirects | ❌ | ✅ |
|
|
621
|
+
| Mobile vs desktop routing | ❌ | ✅ |
|
|
622
|
+
| Simple path changes | ✅ | ❌ |
|
|
623
|
+
| Maintenance mode (all users) | ✅ | ❌ |
|
|
624
|
+
| Maintenance mode (except admins) | ❌ | ✅ |
|
|
625
|
+
| Bulk product SKU changes | ✅ | ❌ |
|
|
626
|
+
|
|
627
|
+
**💡 Pro Tip:** You can use **both** approaches together! Use `launch.json` for bulk static redirects and edge functions for dynamic logic.
|
|
628
|
+
|
|
629
|
+
**Example Combined Approach:**
|
|
630
|
+
```javascript
|
|
631
|
+
// launch.json - handles 1000+ static SEO redirects
|
|
632
|
+
{
|
|
633
|
+
"redirects": [
|
|
634
|
+
{ "source": "/old-blog/:slug", "destination": "/blog/:slug", "statusCode": 301 }
|
|
635
|
+
// ... 1000 more redirects
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// functions/[proxy].edge.js - handles dynamic logic
|
|
640
|
+
export default async function handler(request) {
|
|
641
|
+
// Geo-based redirect (dynamic)
|
|
642
|
+
const country = request.headers.get('x-cs-country');
|
|
643
|
+
if (country === 'FR') {
|
|
644
|
+
return Response.redirect('https://fr.mysite.com', 302);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Static redirects handled by launch.json automatically
|
|
648
|
+
return passThrough(request);
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
398
654
|
### ⚛️ Next.js Optimization
|
|
399
655
|
|
|
400
656
|
#### `handleNextJS_RSC(request, options)`
|
|
@@ -772,6 +1028,60 @@ export default async function handler(request, context) {
|
|
|
772
1028
|
|
|
773
1029
|
---
|
|
774
1030
|
|
|
1031
|
+
## 🧪 Local testing (Wrangler / Miniflare)
|
|
1032
|
+
|
|
1033
|
+
Test your `functions/[proxy].edge.js` chain **locally** without deploying. Wrangler’s dev server uses **Miniflare**, which runs a Workers-compatible runtime on your machine.
|
|
1034
|
+
|
|
1035
|
+
### Interactive wizard (easiest)
|
|
1036
|
+
|
|
1037
|
+
From your **project root** (where `package.json` lives):
|
|
1038
|
+
|
|
1039
|
+
```bash
|
|
1040
|
+
npx create-launch-edge local
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
Or the short alias:
|
|
1044
|
+
|
|
1045
|
+
```bash
|
|
1046
|
+
npx launch-edge-local
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
You’ll get a numbered menu (redirect, JSON route, basic auth, bot block, or Next.js RSC). Pick one; the CLI writes `functions/[proxy].edge.js` for that scenario, ensures `dev-worker.edge.js` and `wrangler.toml` exist, then prints a short checklist (start your app, run the dev server, open the test URL). **Wrangler** is installed automatically as a dependency of this package. If `[proxy].edge.js` already exists, you’re asked before it’s overwritten.
|
|
1050
|
+
|
|
1051
|
+
### Start the local Worker (no `wrangler` typing)
|
|
1052
|
+
|
|
1053
|
+
From the **project root** (next to `wrangler.toml`):
|
|
1054
|
+
|
|
1055
|
+
```bash
|
|
1056
|
+
npx launch-edge-test-local
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
This runs the bundled `wrangler dev` for you. Extra arguments are forwarded (for example `--port 8788` or `--var BACKEND_URL=http://127.0.0.1:5173`). It is equivalent to `npx wrangler dev`.
|
|
1060
|
+
|
|
1061
|
+
### Manual setup
|
|
1062
|
+
|
|
1063
|
+
1. Run `npx create-launch-edge` (or ensure you have `functions/dev-worker.edge.js` and `wrangler.toml`). The init script creates these only if they are missing; it does **not** overwrite an existing `wrangler.toml`.
|
|
1064
|
+
2. In `wrangler.toml`, set `[vars] BACKEND_URL` to your local app origin (default `http://127.0.0.1:3000`). Wrangler is **already a dependency** of `@aryanbansal-launch/edge-utils`—you do not need `npm install -D wrangler` separately unless you want a different version pinned at the project root.
|
|
1065
|
+
3. Start your app on that port, then run `npx launch-edge-test-local` (or `npx wrangler dev`) from the **project root**.
|
|
1066
|
+
4. Open the URL Wrangler prints (for example `http://localhost:8787`). Traffic flows: browser → local Worker → `rewriteRequestToOrigin` → your handler → `fetch` to `BACKEND_URL`.
|
|
1067
|
+
|
|
1068
|
+
Override the backend URL for a single session if needed:
|
|
1069
|
+
|
|
1070
|
+
```bash
|
|
1071
|
+
npx launch-edge-test-local --var BACKEND_URL=http://127.0.0.1:5173
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
### In-repo example
|
|
1075
|
+
|
|
1076
|
+
See [`examples/local-dev/`](examples/local-dev/) for a minimal runnable project (`npm install` in that folder, then `npm run dev` while a server listens on port 3000).
|
|
1077
|
+
|
|
1078
|
+
### Caveats
|
|
1079
|
+
|
|
1080
|
+
- **Hostname-based rules** (`protectWithBasicAuth`): matching uses both the request URL host and the `Host` header, so `hostnameIncludes: "localhost"` works with `npx launch-edge-test-local` even when the rewritten URL points at `127.0.0.1` (BACKEND_URL).
|
|
1081
|
+
- **Geo and client IP** (`getGeoHeaders`, `getClientIP`): these read request headers. Miniflare does not inject Cloudflare `cf` metadata the way production does; values may be empty unless you set headers yourself or configure Wrangler where supported.
|
|
1082
|
+
|
|
1083
|
+
---
|
|
1084
|
+
|
|
775
1085
|
## 🛠️ CLI Commands
|
|
776
1086
|
|
|
777
1087
|
### `npx create-launch-edge`
|
|
@@ -806,12 +1116,24 @@ Next Steps:
|
|
|
806
1116
|
|
|
807
1117
|
---
|
|
808
1118
|
|
|
1119
|
+
### `npx launch-edge-test-local`
|
|
1120
|
+
|
|
1121
|
+
Starts **Wrangler dev** using the Wrangler bundled with this package—no need to type `npx wrangler dev`. Run from the directory that contains `wrangler.toml` (usually your app root).
|
|
1122
|
+
|
|
1123
|
+
```bash
|
|
1124
|
+
npx launch-edge-test-local
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
Arguments are passed through to `wrangler dev` (for example `--port 8788` or `--var BACKEND_URL=http://127.0.0.1:5173`).
|
|
1128
|
+
|
|
1129
|
+
---
|
|
1130
|
+
|
|
809
1131
|
### `npx launch-config`
|
|
810
1132
|
|
|
811
|
-
Interactive CLI to manage `launch.json` configuration.
|
|
1133
|
+
Interactive CLI to manage `launch.json` configuration with support for bulk imports.
|
|
812
1134
|
|
|
813
1135
|
**What it does:**
|
|
814
|
-
- Add/manage redirects
|
|
1136
|
+
- Add/manage redirects (one-by-one or bulk import)
|
|
815
1137
|
- Configure rewrites
|
|
816
1138
|
- Set up cache priming URLs
|
|
817
1139
|
- Preserves existing configuration
|
|
@@ -825,13 +1147,14 @@ npx launch-config
|
|
|
825
1147
|
```
|
|
826
1148
|
🚀 Launch Configuration Generator
|
|
827
1149
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1150
|
+
How would you like to add redirects?
|
|
1151
|
+
1) One by one (interactive)
|
|
1152
|
+
2) Bulk import from CSV file
|
|
1153
|
+
3) Bulk import from JSON file
|
|
1154
|
+
4) Skip
|
|
1155
|
+
Choose (1-4): 2
|
|
1156
|
+
Enter CSV file path (e.g., ./redirects.csv): ./redirects.csv
|
|
1157
|
+
✔ Imported 150 redirects from CSV.
|
|
835
1158
|
|
|
836
1159
|
Do you want to add a Rewrite? (y/n): y
|
|
837
1160
|
Source path (e.g., /api/*): /api/*
|
|
@@ -845,6 +1168,47 @@ Enter URLs separated by commas (e.g., /home,/about,/shop): /,/about,/products
|
|
|
845
1168
|
✅ Successfully updated launch.json!
|
|
846
1169
|
```
|
|
847
1170
|
|
|
1171
|
+
#### Bulk Import Formats
|
|
1172
|
+
|
|
1173
|
+
**CSV Format** (`redirects.csv`):
|
|
1174
|
+
```csv
|
|
1175
|
+
source,destination,statusCode
|
|
1176
|
+
/old-blog/post-1,/blog/post-1,301
|
|
1177
|
+
/old-blog/post-2,/blog/post-2,301
|
|
1178
|
+
/products/old-sku-123,/products/new-sku-456,308
|
|
1179
|
+
/legacy/*,/new/*,301
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
**JSON Format** (`redirects.json`):
|
|
1183
|
+
```json
|
|
1184
|
+
[
|
|
1185
|
+
{
|
|
1186
|
+
"source": "/old-blog/post-1",
|
|
1187
|
+
"destination": "/blog/post-1",
|
|
1188
|
+
"statusCode": 301
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"source": "/old-blog/post-2",
|
|
1192
|
+
"destination": "/blog/post-2",
|
|
1193
|
+
"statusCode": 301
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
"source": "/products/old-sku-123",
|
|
1197
|
+
"destination": "/products/new-sku-456",
|
|
1198
|
+
"statusCode": 308
|
|
1199
|
+
}
|
|
1200
|
+
]
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
**Use Cases for Bulk Import:**
|
|
1204
|
+
- Migrating from another platform with hundreds of URLs
|
|
1205
|
+
- SEO redirects from spreadsheet/database exports
|
|
1206
|
+
- Bulk product SKU changes
|
|
1207
|
+
- Content restructuring with many path changes
|
|
1208
|
+
|
|
1209
|
+
**Example Files:**
|
|
1210
|
+
See `examples/redirects.csv` and `examples/redirects.json` in this package for ready-to-use templates.
|
|
1211
|
+
|
|
848
1212
|
**Learn More:**
|
|
849
1213
|
- [Static Redirects](https://www.contentstack.com/docs/developers/launch/edge-url-redirects)
|
|
850
1214
|
- [Cache Priming](https://www.contentstack.com/docs/developers/launch/cache-priming)
|
|
@@ -865,6 +1229,27 @@ npx launch-help
|
|
|
865
1229
|
- Return types and examples
|
|
866
1230
|
- CLI commands
|
|
867
1231
|
- Quick links to documentation
|
|
1232
|
+
## 📖 API Reference
|
|
1233
|
+
|
|
1234
|
+
### 🧰 Local development
|
|
1235
|
+
|
|
1236
|
+
- **`rewriteRequestToOrigin(request, backendOrigin)`**: Builds a new `Request` whose URL points at `backendOrigin` while preserving path, query, and body. Used by `functions/dev-worker.edge.js` so `passThrough` reaches your local server.
|
|
1237
|
+
|
|
1238
|
+
### 🛡️ Security
|
|
1239
|
+
- **`blockAICrawlers(request, bots?)`**: Blocks common AI crawlers.
|
|
1240
|
+
- **`ipAccessControl(request, { allow?, deny? })`**: Simple IP-based firewall.
|
|
1241
|
+
|
|
1242
|
+
### 🔐 Authentication
|
|
1243
|
+
- **`protectWithBasicAuth(request, options)`**: Prompt for credentials based on hostname.
|
|
1244
|
+
|
|
1245
|
+
### 🔀 Redirection
|
|
1246
|
+
- **`redirectIfMatch(request, options)`**: Perform SEO-friendly redirects at the edge.
|
|
1247
|
+
|
|
1248
|
+
### 📍 Geo Location
|
|
1249
|
+
- **`getGeoHeaders(request)`**: Returns an object with `country`, `region`, `city`, `latitude`, `longitude`.
|
|
1250
|
+
|
|
1251
|
+
### ⚛️ Next.js
|
|
1252
|
+
- **`handleNextJS_RSC(request, { affectedPaths })`**: Resolves RSC header issues on Contentstack Launch.
|
|
868
1253
|
|
|
869
1254
|
---
|
|
870
1255
|
|