@aryanbansal-launch/edge-utils 0.1.10 → 0.1.12

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.
@@ -66,19 +66,64 @@ async function run() {
66
66
  }
67
67
 
68
68
  // 1. Redirects
69
- while (true) {
70
- const addRedirect = await question(`Do you want to add a Redirect?${config.redirects.length > 0 ? ' another?' : ''} (y/n): `);
71
- if (addRedirect.toLowerCase() !== 'y') break;
72
-
73
- const source = await question(' Source path (e.g., /source): ');
74
- const destination = await question(' Destination path (e.g., /destination): ');
75
- const code = await question(' Status code (default 308): ');
76
- config.redirects.push({
77
- source,
78
- destination,
79
- statusCode: parseInt(code) || 308
80
- });
81
- console.log(`${colors.green} ✔ Redirect added.${colors.reset}`);
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,192 @@
1
+ #!/usr/bin/env node
2
+
3
+ const colors = {
4
+ reset: '\x1b[0m',
5
+ bright: '\x1b[1m',
6
+ green: '\x1b[32m',
7
+ yellow: '\x1b[33m',
8
+ cyan: '\x1b[36m',
9
+ blue: '\x1b[34m',
10
+ magenta: '\x1b[35m',
11
+ dim: '\x1b[2m'
12
+ };
13
+
14
+ console.log(`
15
+ ${colors.bright}${colors.cyan}╔════════════════════════════════════════════════════════════════════╗
16
+ ║ 🚀 Launch Edge Utils - Complete Reference Guide ║
17
+ ╚════════════════════════════════════════════════════════════════════╝${colors.reset}
18
+
19
+ ${colors.bright}${colors.yellow}📦 AVAILABLE METHODS${colors.reset}
20
+ ${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
21
+
22
+ ${colors.bright}${colors.green}1. Security & Access Control${colors.reset}
23
+
24
+ ${colors.cyan}blockAICrawlers(request, bots?)${colors.reset}
25
+ Block AI crawlers and bots from accessing your site
26
+ ${colors.dim}Parameters:${colors.reset}
27
+ - request: Request object
28
+ - bots: string[] (optional) - Custom bot list
29
+ ${colors.dim}Default Blocked Bots:${colors.reset}
30
+ claudebot, gptbot, googlebot, bingbot, ahrefsbot,
31
+ yandexbot, semrushbot, mj12bot, facebookexternalhit, twitterbot
32
+ ${colors.dim}Returns:${colors.reset} Response | null
33
+ ${colors.dim}Example:${colors.reset}
34
+ const response = blockAICrawlers(request);
35
+ if (response) return response;
36
+
37
+ ${colors.cyan}blockDefaultDomains(request, options?)${colors.reset}
38
+ Block access via default Launch domains (*.contentstackapps.com)
39
+ ${colors.dim}Parameters:${colors.reset}
40
+ - request: Request object
41
+ - options: { domainToBlock?: string }
42
+ ${colors.dim}Returns:${colors.reset} Response | null
43
+ ${colors.dim}Example:${colors.reset}
44
+ const response = blockDefaultDomains(request);
45
+ if (response) return response;
46
+
47
+ ${colors.cyan}ipAccessControl(request, options)${colors.reset}
48
+ Whitelist or blacklist IPs at the edge
49
+ ${colors.dim}Parameters:${colors.reset}
50
+ - request: Request object
51
+ - options: { allow?: string[], deny?: string[] }
52
+ ${colors.dim}Returns:${colors.reset} Response | null
53
+ ${colors.dim}Example:${colors.reset}
54
+ const response = ipAccessControl(request, {
55
+ allow: ["203.0.113.10", "198.51.100.5"]
56
+ });
57
+ if (response) return response;
58
+
59
+ ${colors.cyan}protectWithBasicAuth(request, options)${colors.reset}
60
+ Add Basic Authentication to protect staging/dev environments
61
+ ${colors.dim}Parameters:${colors.reset}
62
+ - request: Request object
63
+ - options: {
64
+ hostnameIncludes: string,
65
+ username: string,
66
+ password: string,
67
+ realm?: string
68
+ }
69
+ ${colors.dim}Returns:${colors.reset} Promise<Response> | null
70
+ ${colors.dim}Example:${colors.reset}
71
+ const auth = await protectWithBasicAuth(request, {
72
+ hostnameIncludes: "staging.myapp.com",
73
+ username: "admin",
74
+ password: "securepass123"
75
+ });
76
+ if (auth && auth.status === 401) return auth;
77
+
78
+ ${colors.bright}${colors.green}2. Routing & Redirects${colors.reset}
79
+
80
+ ${colors.cyan}redirectIfMatch(request, options)${colors.reset}
81
+ Conditional redirects based on path and method
82
+ ${colors.dim}Parameters:${colors.reset}
83
+ - request: Request object
84
+ - options: {
85
+ path: string,
86
+ method?: string,
87
+ to: string,
88
+ status?: number
89
+ }
90
+ ${colors.dim}Returns:${colors.reset} Response | null
91
+ ${colors.dim}Example:${colors.reset}
92
+ const redirect = redirectIfMatch(request, {
93
+ path: "/old-page",
94
+ to: "/new-page",
95
+ status: 301
96
+ });
97
+ if (redirect) return redirect;
98
+
99
+ ${colors.bright}${colors.green}3. Next.js Optimization${colors.reset}
100
+
101
+ ${colors.cyan}handleNextJS_RSC(request, options)${colors.reset}
102
+ Fix Next.js React Server Components header issues
103
+ ${colors.dim}Parameters:${colors.reset}
104
+ - request: Request object
105
+ - options: { affectedPaths: string[] }
106
+ ${colors.dim}Returns:${colors.reset} Promise<Response> | null
107
+ ${colors.dim}Example:${colors.reset}
108
+ const rsc = await handleNextJS_RSC(request, {
109
+ affectedPaths: ["/shop", "/about", "/products"]
110
+ });
111
+ if (rsc) return rsc;
112
+
113
+ ${colors.bright}${colors.green}4. Geo-Location${colors.reset}
114
+
115
+ ${colors.cyan}getGeoHeaders(request)${colors.reset}
116
+ Extract geo-location data from Launch headers
117
+ ${colors.dim}Parameters:${colors.reset}
118
+ - request: Request object
119
+ ${colors.dim}Returns:${colors.reset} {
120
+ country: string | null,
121
+ region: string | null,
122
+ city: string | null,
123
+ latitude: string | null,
124
+ longitude: string | null
125
+ }
126
+ ${colors.dim}Example:${colors.reset}
127
+ const geo = getGeoHeaders(request);
128
+ if (geo.country === "US") {
129
+ // Custom logic for US visitors
130
+ }
131
+
132
+ ${colors.bright}${colors.green}5. Response Utilities${colors.reset}
133
+
134
+ ${colors.cyan}jsonResponse(body, init?)${colors.reset}
135
+ Create JSON responses easily
136
+ ${colors.dim}Parameters:${colors.reset}
137
+ - body: Record<string, unknown>
138
+ - init: ResponseInit (optional)
139
+ ${colors.dim}Returns:${colors.reset} Response
140
+ ${colors.dim}Example:${colors.reset}
141
+ return jsonResponse({ status: "ok", data: [...] });
142
+
143
+ ${colors.cyan}passThrough(request)${colors.reset}
144
+ Forward request to origin server
145
+ ${colors.dim}Parameters:${colors.reset}
146
+ - request: Request object
147
+ ${colors.dim}Returns:${colors.reset} Promise<Response>
148
+ ${colors.dim}Example:${colors.reset}
149
+ return passThrough(request);
150
+
151
+ ${colors.bright}${colors.green}6. Configuration${colors.reset}
152
+
153
+ ${colors.cyan}generateLaunchConfig(options)${colors.reset}
154
+ Generate launch.json configuration programmatically
155
+ ${colors.dim}Parameters:${colors.reset}
156
+ - options: {
157
+ redirects?: LaunchRedirect[],
158
+ rewrites?: LaunchRewrite[],
159
+ cache?: { cachePriming?: { urls: string[] } }
160
+ }
161
+ ${colors.dim}Returns:${colors.reset} LaunchConfig
162
+ ${colors.dim}Example:${colors.reset}
163
+ const config = generateLaunchConfig({
164
+ redirects: [{ source: "/old", destination: "/new", statusCode: 301 }],
165
+ cache: { cachePriming: { urls: ["/", "/about"] } }
166
+ });
167
+
168
+ ${colors.bright}${colors.yellow}🛠️ CLI COMMANDS${colors.reset}
169
+ ${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
170
+
171
+ ${colors.cyan}npx create-launch-edge${colors.reset}
172
+ Initialize edge functions with boilerplate code
173
+ Creates: functions/[proxy].edge.js
174
+
175
+ ${colors.cyan}npx launch-config${colors.reset}
176
+ Interactive CLI to manage launch.json
177
+ Configure: redirects, rewrites, cache priming
178
+ Supports bulk import from CSV/JSON files
179
+
180
+ ${colors.cyan}npx launch-help${colors.reset}
181
+ Display this help guide
182
+
183
+ ${colors.bright}${colors.yellow}📚 QUICK LINKS${colors.reset}
184
+ ${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
185
+
186
+ ${colors.blue}Documentation:${colors.reset} https://github.com/AryanBansal-launch/launch-edge-utils
187
+ ${colors.blue}NPM Package:${colors.reset} https://www.npmjs.com/package/@aryanbansal-launch/edge-utils
188
+ ${colors.blue}Launch Docs:${colors.reset} https://www.contentstack.com/docs/developers/launch
189
+
190
+ ${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
191
+ `);
192
+
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Generates a complete Gemini AI Chatbot widget script to be injected at the Edge.
3
+ */
4
+ export function getGeminiChatbotScript(options) {
5
+ const { apiKey, botName = "Gemini Assistant", welcomeMessage = "Hi! How can I help you today?", primaryColor = "#1a73e8", } = options;
6
+ return `
7
+ <script>
8
+ (function() {
9
+ // Create styles
10
+ const style = document.createElement('style');
11
+ style.textContent = \`
12
+ #gemini-chat-widget {
13
+ position: fixed;
14
+ bottom: 20px;
15
+ right: 20px;
16
+ z-index: 9999;
17
+ font-family: sans-serif;
18
+ }
19
+ #gemini-chat-button {
20
+ width: 60px;
21
+ height: 60px;
22
+ border-radius: 50%;
23
+ background-color: ${primaryColor};
24
+ color: white;
25
+ border: none;
26
+ cursor: pointer;
27
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ font-size: 24px;
32
+ }
33
+ #gemini-chat-window {
34
+ display: none;
35
+ width: 350px;
36
+ height: 500px;
37
+ background: white;
38
+ border-radius: 12px;
39
+ box-shadow: 0 8px 24px rgba(0,0,0,0.2);
40
+ flex-direction: column;
41
+ overflow: hidden;
42
+ margin-bottom: 15px;
43
+ }
44
+ #gemini-chat-header {
45
+ padding: 15px;
46
+ background: ${primaryColor};
47
+ color: white;
48
+ font-weight: bold;
49
+ display: flex;
50
+ justify-content: space-between;
51
+ }
52
+ #gemini-chat-messages {
53
+ flex: 1;
54
+ padding: 15px;
55
+ overflow-y: auto;
56
+ background: #f7f7f7;
57
+ }
58
+ .gemini-msg {
59
+ margin-bottom: 10px;
60
+ padding: 8px 12px;
61
+ border-radius: 8px;
62
+ max-width: 80%;
63
+ word-wrap: break-word;
64
+ }
65
+ .gemini-msg-bot { background: white; align-self: flex-start; border: 1px solid #eee; }
66
+ .gemini-msg-user { background: ${primaryColor}; color: white; align-self: flex-end; margin-left: auto; }
67
+ #gemini-chat-input-area {
68
+ padding: 10px;
69
+ border-top: 1px solid #eee;
70
+ display: flex;
71
+ }
72
+ #gemini-chat-input {
73
+ flex: 1;
74
+ border: 1px solid #ddd;
75
+ padding: 8px;
76
+ border-radius: 4px;
77
+ outline: none;
78
+ }
79
+ \`;
80
+ document.head.appendChild(style);
81
+
82
+ // Create HTML
83
+ const container = document.createElement('div');
84
+ container.id = 'gemini-chat-widget';
85
+ container.innerHTML = \`
86
+ <div id="gemini-chat-window">
87
+ <div id="gemini-chat-header">
88
+ <span>${botName}</span>
89
+ <button onclick="document.getElementById('gemini-chat-window').style.display='none'" style="background:none; border:none; color:white; cursor:pointer;">×</button>
90
+ </div>
91
+ <div id="gemini-chat-messages">
92
+ <div class="gemini-msg gemini-msg-bot">${welcomeMessage}</div>
93
+ </div>
94
+ <div id="gemini-chat-input-area">
95
+ <input type="text" id="gemini-chat-input" placeholder="Type a message...">
96
+ </div>
97
+ </div>
98
+ <button id="gemini-chat-button">💬</button>
99
+ \`;
100
+ document.body.appendChild(container);
101
+
102
+ const btn = document.getElementById('gemini-chat-button');
103
+ const win = document.getElementById('gemini-chat-window');
104
+ const input = document.getElementById('gemini-chat-input');
105
+ const msgContainer = document.getElementById('gemini-chat-messages');
106
+
107
+ btn.onclick = () => {
108
+ win.style.display = win.style.display === 'flex' ? 'none' : 'flex';
109
+ if (win.style.display === 'flex') input.focus();
110
+ };
111
+
112
+ async function sendMessage() {
113
+ const text = input.value.trim();
114
+ if (!text) return;
115
+
116
+ input.value = '';
117
+ appendMessage('user', text);
118
+
119
+ const loadingMsg = appendMessage('bot', '...');
120
+
121
+ try {
122
+ const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}', {
123
+ method: 'POST',
124
+ headers: { 'Content-Type': 'application/json' },
125
+ body: JSON.stringify({
126
+ contents: [{ parts: [{ text }] }]
127
+ })
128
+ });
129
+ const data = await response.json();
130
+ const botReply = data.candidates[0].content.parts[0].text;
131
+ loadingMsg.textContent = botReply;
132
+ } catch (e) {
133
+ loadingMsg.textContent = 'Sorry, I am having trouble connecting.';
134
+ }
135
+ msgContainer.scrollTop = msgContainer.scrollHeight;
136
+ }
137
+
138
+ function appendMessage(role, text) {
139
+ const div = document.createElement('div');
140
+ div.className = 'gemini-msg gemini-msg-' + role;
141
+ div.textContent = text;
142
+ msgContainer.appendChild(div);
143
+ msgContainer.scrollTop = msgContainer.scrollHeight;
144
+ return div;
145
+ }
146
+
147
+ input.onkeypress = (e) => { if (e.key === 'Enter') sendMessage(); };
148
+ })();
149
+ </script>
150
+ \`;
151
+ }
152
+ `;
153
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Utility to inject HTML/scripts into a Response body using TransformStream.
3
+ * This is high-performance as it streams the modification without loading the full body into memory.
4
+ */
5
+ class HTMLRewriterStream extends TransformStream {
6
+ constructor(contentToInject, position = 'before-body-end') {
7
+ const encoder = new TextEncoder();
8
+ const decoder = new TextDecoder();
9
+ super({
10
+ transform(chunk, controller) {
11
+ let html = decoder.decode(chunk);
12
+ if (position === 'before-body-end' && html.includes('</body>')) {
13
+ html = html.replace('</body>', `${contentToInject}</body>`);
14
+ }
15
+ else if (position === 'after-head-start' && html.includes('<head>')) {
16
+ html = html.replace('<head>', `<head>${contentToInject}`);
17
+ }
18
+ controller.enqueue(encoder.encode(html));
19
+ },
20
+ });
21
+ }
22
+ }
23
+ /**
24
+ * Injects a script or HTML snippet into a Response.
25
+ * @param response The original Response object
26
+ * @param options Injection options
27
+ */
28
+ export async function injectScript(response, options) {
29
+ const contentType = response.headers.get('content-type');
30
+ // Only inject into HTML responses
31
+ if (!contentType || !contentType.includes('text/html')) {
32
+ return response;
33
+ }
34
+ const contentToInject = options.scriptUrl
35
+ ? `<script src="${options.scriptUrl}" async defer></script>`
36
+ : (options.script || '');
37
+ if (!contentToInject)
38
+ return response;
39
+ const rewriter = new HTMLRewriterStream(contentToInject, options.position);
40
+ return new Response(response.body?.pipeThrough(rewriter), {
41
+ status: response.status,
42
+ statusText: response.statusText,
43
+ headers: response.headers,
44
+ });
45
+ }
@@ -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.10",
3
+ "version": "0.1.12",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -13,17 +13,33 @@
13
13
  },
14
14
  "main": "dist/index.js",
15
15
  "bin": {
16
- "create-launch-edge": "./bin/launch-init.js",
17
- "launch-config": "./bin/launch-config.js"
16
+ "create-launch-edge": "bin/launch-init.js",
17
+ "launch-config": "bin/launch-config.js",
18
+ "launch-help": "bin/launch-help.js"
18
19
  },
19
20
  "exports": {
20
21
  ".": "./dist/index.js"
21
22
  },
22
23
  "files": [
23
24
  "dist",
24
- "bin"
25
+ "bin",
26
+ "examples"
25
27
  ],
26
28
  "scripts": {
27
- "build": "tsc"
28
- }
29
+ "build": "tsc",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "keywords": [
33
+ "contentstack",
34
+ "launch",
35
+ "edge-functions",
36
+ "edge",
37
+ "middleware",
38
+ "security",
39
+ "nextjs",
40
+ "redirect",
41
+ "geo-location",
42
+ "basic-auth",
43
+ "ip-access"
44
+ ]
29
45
  }