@aryanbansal-launch/edge-utils 0.1.12 → 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.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shortcut for: npx launch-init local
4
+ */
5
+ process.argv.splice(2, 0, 'local');
6
+ import('./launch-init.js').catch((err) => {
7
+ console.error(err);
8
+ process.exit(1);
9
+ });
@@ -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
+ });
@@ -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}
@@ -177,6 +187,34 @@ ${colors.dim}──────────────────────
177
187
  Configure: redirects, rewrites, cache priming
178
188
  Supports bulk import from CSV/JSON files
179
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/
217
+
180
218
  ${colors.cyan}npx launch-help${colors.reset}
181
219
  Display this help guide
182
220
 
@@ -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
- // Final Summary Messages based on the state
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\n`);
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
- init();
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();
@@ -1,6 +1,13 @@
1
1
  export function protectWithBasicAuth(request, options) {
2
2
  const url = new URL(request.url);
3
- if (!url.hostname.includes(options.hostnameIncludes)) {
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
@@ -1,3 +1,4 @@
1
+ export * from "./dev/rewrite-origin.js";
1
2
  export * from "./response/json.js";
2
3
  export * from "./response/passthrough.js";
3
4
  export * from "./redirect/redirect.js";
@@ -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,11 @@
1
+ {
2
+ "name": "local-dev-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "launch-edge-test-local"
7
+ },
8
+ "dependencies": {
9
+ "@aryanbansal-launch/edge-utils": "file:../.."
10
+ }
11
+ }
@@ -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,6 @@
1
+ name = "edge-utils-local-dev-example"
2
+ main = "worker.ts"
3
+ compatibility_date = "2024-01-01"
4
+
5
+ [vars]
6
+ BACKEND_URL = "http://127.0.0.1:3000"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryanbansal-launch/edge-utils",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -15,11 +15,16 @@
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
30
  "bin",
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) - Protect URLs containing this hostname
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 doesn't match
408
+ - Returns `null` if neither the URL hostname nor the `Host` header matches `hostnameIncludes`
303
409
 
304
410
  **Example:**
305
411
  ```javascript
@@ -922,6 +1028,60 @@ export default async function handler(request, context) {
922
1028
 
923
1029
  ---
924
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
+
925
1085
  ## 🛠️ CLI Commands
926
1086
 
927
1087
  ### `npx create-launch-edge`
@@ -956,6 +1116,18 @@ Next Steps:
956
1116
 
957
1117
  ---
958
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
+
959
1131
  ### `npx launch-config`
960
1132
 
961
1133
  Interactive CLI to manage `launch.json` configuration with support for bulk imports.
@@ -1057,6 +1229,27 @@ npx launch-help
1057
1229
  - Return types and examples
1058
1230
  - CLI commands
1059
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.
1060
1253
 
1061
1254
  ---
1062
1255