somratpro Claude Sonnet 4.6 commited on
Commit
c87b389
Β·
1 Parent(s): 2272bb9

remove debug endpoints before release

Browse files

Remove publicly-accessible debug endpoints that exposed sensitive data:
- /app/debug-logs (API keys, Redis oauth tokens, backend logs)
- /app/test-request-token (live OAuth token generation)
- /app/test-twitter (raw API connectivity probe)
- /app/read-x-provider (container source exposure)
- X OAuth callback Redis snapshot logging (/tmp/x-oauth-callbacks.log)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. health-server.js +0 -345
health-server.js CHANGED
@@ -2085,314 +2085,6 @@ const server = http.createServer((req, res) => {
2085
  return;
2086
  }
2087
 
2088
- // ── /app/test-twitter β€” raw connectivity probe to api.twitter.com ────────
2089
- if (pathname === "/app/test-twitter") {
2090
- const https = require("https");
2091
- const lines = [];
2092
- lines.push(`NODE_OPTIONS : ${process.env.NODE_OPTIONS || "(not set)"}`);
2093
- lines.push(`CLOUDFLARE_PROXY_URL : ${process.env.CLOUDFLARE_PROXY_URL || "(not set)"}`);
2094
- lines.push("");
2095
- // Two probes:
2096
- // A) GET api.twitter.com/ β€” basic reachability + proxy detection
2097
- // B) POST oauth/access_token (no auth) β€” 4xx+JSON if IP allowed, 500 HTML if IP blocked
2098
- const makeProbe = (opts, postBody) => new Promise((resolve) => {
2099
- const t0 = Date.now();
2100
- const req = https.request({ ...opts, timeout: 10000 }, (r) => {
2101
- let body = "";
2102
- r.setEncoding("utf8");
2103
- r.on("data", (c) => { body += c; if (body.length > 800) r.destroy(); });
2104
- r.on("close", () => resolve({ status: r.statusCode, headers: r.headers, body: body.trim(), ms: Date.now() - t0 }));
2105
- r.on("end", () => resolve({ status: r.statusCode, headers: r.headers, body: body.trim(), ms: Date.now() - t0 }));
2106
- });
2107
- req.on("error", (e) => resolve({ error: e.message, ms: Date.now() - t0 }));
2108
- req.on("timeout", () => { req.destroy(); resolve({ error: "TIMEOUT 10s", ms: Date.now() - t0 }); });
2109
- if (postBody) req.write(postBody);
2110
- req.end();
2111
- });
2112
-
2113
- (async () => {
2114
- // Probe A
2115
- lines.push("── Probe A: GET https://api.twitter.com/ ──");
2116
- const a = await makeProbe({ hostname: "api.twitter.com", path: "/", method: "GET",
2117
- headers: { "User-Agent": "curl/8.0", "Accept": "*/*" } });
2118
- if (a.error) {
2119
- lines.push(`ERROR: ${a.error}`);
2120
- } else {
2121
- lines.push(`Status : ${a.status} (${a.ms}ms)`);
2122
- for (const [k, v] of Object.entries(a.headers)) lines.push(` ${k}: ${v}`);
2123
- lines.push(`cf-ray in headers? : ${a.headers["cf-ray"] ? "YES β†’ request went through Cloudflare" : "no"}`);
2124
- lines.push(`Body (first 300) : ${a.body.slice(0, 300)}`);
2125
- }
2126
-
2127
- // Probe B
2128
- lines.push("");
2129
- lines.push("── Probe B: POST https://api.twitter.com/oauth/access_token (no auth) ──");
2130
- lines.push("Expected: 400/401 JSON if IP allowed by X, 500 HTML if IP blocked");
2131
- const postBody = "oauth_verifier=test";
2132
- const b = await makeProbe({ hostname: "api.twitter.com", path: "/oauth/access_token",
2133
- method: "POST",
2134
- headers: { "User-Agent": "curl/8.0", "Content-Type": "application/x-www-form-urlencoded",
2135
- "Content-Length": Buffer.byteLength(postBody) } }, postBody);
2136
- if (b.error) {
2137
- lines.push(`ERROR: ${b.error}`);
2138
- } else {
2139
- lines.push(`Status : ${b.status} (${b.ms}ms)`);
2140
- for (const [k, v] of Object.entries(b.headers)) lines.push(` ${k}: ${v}`);
2141
- const isHtml = b.body.includes("<html") || b.body.includes("<!DOCTYPE");
2142
- lines.push(`Body type : ${isHtml ? "HTML ← IP probably blocked" : "text/JSON ← IP allowed"}`);
2143
- lines.push(`Body (first 500) : ${b.body.slice(0, 500)}`);
2144
- }
2145
-
2146
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
2147
- res.end(lines.join("\n"));
2148
- })();
2149
- return;
2150
- }
2151
-
2152
- // ── /app/test-request-token β€” Test generateAuthLink via CF proxy ─────────
2153
- if (pathname === "/app/test-request-token" || pathname === "/app/test-request-token/") {
2154
- (async () => {
2155
- const lines = ["=== TEST: generateAuthLink via CF proxy ===", ""];
2156
- try {
2157
- const apiKey = process.env.X_API_KEY || "";
2158
- const apiSecret = process.env.X_API_SECRET || "";
2159
- lines.push(`X_API_KEY : ${apiKey.slice(0,6)}... (len=${apiKey.length})`);
2160
- lines.push(`X_API_SECRET : ${apiSecret.slice(0,6)}... (len=${apiSecret.length})`);
2161
- lines.push(`CLOUDFLARE_PROXY_URL : ${process.env.CLOUDFLARE_PROXY_URL || "(not set)"}`);
2162
- lines.push(`NODE_OPTIONS : ${process.env.NODE_OPTIONS || "(not set)"}`);
2163
- lines.push("");
2164
-
2165
- // Require twitter-api-v2 from Postiz's node_modules
2166
- const { TwitterApi } = require("/app/node_modules/twitter-api-v2");
2167
- const callbackUrl = (process.env.FRONTEND_URL || "https://somratpro-huggingpost.hf.space") + "/integrations/social/x";
2168
- lines.push(`callbackUrl : ${callbackUrl}`);
2169
- lines.push("Calling generateAuthLink...");
2170
-
2171
- const client = new TwitterApi({ appKey: apiKey, appSecret: apiSecret });
2172
- const result = await client.generateAuthLink(callbackUrl, { authAccessType: "write", linkMode: "authenticate", forceLogin: false });
2173
-
2174
- lines.push("");
2175
- lines.push(`oauth_token : ${result.oauth_token ? result.oauth_token.slice(0,20) + "... (len=" + result.oauth_token.length + ")" : "(EMPTY!)"}`);
2176
- lines.push(`oauth_token_secret : ${result.oauth_token_secret ? result.oauth_token_secret.slice(0,10) + "... (len=" + result.oauth_token_secret.length + ")" : "(EMPTY!)"}`);
2177
- lines.push(`url : ${result.url ? result.url.slice(0,80) : "(EMPTY!)"}`);
2178
- lines.push("");
2179
- lines.push("codeVerifier = oauth_token + ':' + oauth_token_secret:");
2180
- const cv = result.oauth_token + ":" + result.oauth_token_secret;
2181
- lines.push(` "${cv.slice(0,40)}..." (len=${cv.length})`);
2182
- if (!result.oauth_token_secret) {
2183
- lines.push(" ⚠ EMPTY SECRET β€” CF proxy or X returning malformed request_token response!");
2184
- } else {
2185
- lines.push(" βœ“ secret present");
2186
- }
2187
- } catch (e) {
2188
- lines.push(`ERROR: ${e.message}`);
2189
- lines.push(e.stack || "");
2190
- }
2191
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
2192
- res.end(lines.join("\n"));
2193
- })();
2194
- return;
2195
- }
2196
-
2197
- // ── /app/read-x-provider β€” Show x.provider.ts source from container ─────
2198
- if (pathname === "/app/read-x-provider") {
2199
- try {
2200
- const { execSync } = require("child_process");
2201
- const rc = (cmd) => { try { return execSync(cmd, { timeout: 10000 }).toString().trim(); } catch(e) { return "(err: " + e.message.slice(0,120) + ")"; } };
2202
- // Find all x.provider files
2203
- const found = rc("find /app -name 'x.provider.*' -o -name 'twitter.provider.*' 2>/dev/null | grep -v node_modules | grep -v .next");
2204
- const lines = ["=== X/Twitter Provider Files ===", found || "(none found)", ""];
2205
- // Also search for 'external:' usage in backend source
2206
- const externalUsage = rc("grep -r 'external:' /app/apps /app/libs --include='*.ts' --include='*.js' -n 2>/dev/null | grep -v node_modules | grep -v .next | grep -v dist | head -30");
2207
- lines.push("=== 'external:' Redis key usage in source ===", externalUsage || "(none)", "");
2208
- // Show content of x.provider file
2209
- const xProviderFile = rc("find /app -name 'x.provider.ts' -not -path '*/node_modules/*' -not -path '*/.next/*' 2>/dev/null | head -1");
2210
- if (xProviderFile && !xProviderFile.startsWith("(err")) {
2211
- lines.push(`=== Content of ${xProviderFile} ===`);
2212
- const content = rc(`cat "${xProviderFile}"`);
2213
- lines.push(content.slice(0, 8000));
2214
- }
2215
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
2216
- res.end(lines.join("\n"));
2217
- } catch(e) {
2218
- res.writeHead(500, { "Content-Type": "text/plain" });
2219
- res.end("Error: " + e.message);
2220
- }
2221
- return;
2222
- }
2223
-
2224
- // ── /app/debug-logs β€” Temporary endpoint for debugging ───────────────────
2225
- if (pathname === "/app/debug-logs") {
2226
- try {
2227
- // tail helper β€” last N lines of a file
2228
- const tailLines = (path, n) => {
2229
- if (!fs.existsSync(path)) return `(no file: ${path})`;
2230
- const lines = fs.readFileSync(path, "utf8").split("\n");
2231
- return lines.slice(-n).join("\n");
2232
- };
2233
-
2234
- const errLog = tailLines("/root/.pm2/logs/backend-error.log", 150);
2235
- const outLog = tailLines("/root/.pm2/logs/backend-out.log", 80);
2236
-
2237
- // masked credential diagnostics β€” first 6 chars + length
2238
- const maskCred = (val) => {
2239
- if (!val) return "(not set)";
2240
- const clean = val.trim();
2241
- if (clean.length === 0) return "(empty)";
2242
- return `${clean.slice(0, 6)}... (len=${clean.length})`;
2243
- };
2244
- const xKey = maskCred(process.env.X_API_KEY);
2245
- const xSecret = maskCred(process.env.X_API_SECRET);
2246
- const frontendUrl = process.env.FRONTEND_URL || "(not set)";
2247
-
2248
- // Heuristic: X Consumer Key is ~25 alphanum, Consumer Secret ~50 alphanum.
2249
- // OAuth 2.0 Client ID is base64url and usually longer (36+).
2250
- const xKeyNote = (() => {
2251
- const v = (process.env.X_API_KEY || "").trim();
2252
- if (!v) return "MISSING";
2253
- if (v.length > 35) return "⚠ LOOKS LIKE OAuth2 CLIENT ID (too long) β€” should be ~25 chars";
2254
- if (v.includes("=") || v.includes(":")) return "⚠ LOOKS WRONG (contains = or :)";
2255
- return "βœ“ length ok";
2256
- })();
2257
- const xSecretNote = (() => {
2258
- const v = (process.env.X_API_SECRET || "").trim();
2259
- if (!v) return "MISSING";
2260
- if (v.length < 40) return "⚠ TOO SHORT β€” Consumer Secret is ~50 chars";
2261
- if (v.length > 70) return "⚠ TOO LONG β€” might be OAuth2 Client Secret or Access Token Secret";
2262
- if (v.includes("=")) return "⚠ CONTAINS = β€” might be base64 encoded (wrong key type)";
2263
- return "βœ“ length ok";
2264
- })();
2265
-
2266
- const credSection = [
2267
- "=== X CREDENTIAL CHECK ===",
2268
- `X_API_KEY : ${xKey} [${xKeyNote}]`,
2269
- `X_API_SECRET : ${xSecret} [${xSecretNote}]`,
2270
- `FRONTEND_URL : ${frontendUrl}`,
2271
- `Expected callback URL: ${frontendUrl}/integrations/social/x`,
2272
- ].join("\n");
2273
-
2274
- const cfProxyLog = fs.existsSync("/tmp/huggingpost-cloudflare-proxy.env")
2275
- ? fs.readFileSync("/tmp/huggingpost-cloudflare-proxy.env", "utf8").replace(/(SECRET|TOKEN)=\S+/gi, "$1=(redacted)")
2276
- : "(file not found β€” proxy not deployed; CLOUDFLARE_WORKERS_TOKEN not set?)";
2277
-
2278
- const cfProxySection = [
2279
- "=== CLOUDFLARE PROXY STATUS ===",
2280
- `CLOUDFLARE_WORKERS_TOKEN : ${process.env.CLOUDFLARE_WORKERS_TOKEN ? "βœ“ set (len=" + process.env.CLOUDFLARE_WORKERS_TOKEN.length + ")" : "βœ— NOT SET β€” proxy never deployed, X traffic goes direct"}`,
2281
- `CLOUDFLARE_PROXY_URL : ${process.env.CLOUDFLARE_PROXY_URL ? maskCred(process.env.CLOUDFLARE_PROXY_URL) : "βœ— NOT SET β€” api.twitter.com calls NOT proxied"}`,
2282
- `CLOUDFLARE_PROXY_DEBUG : ${process.env.CLOUDFLARE_PROXY_DEBUG || "(not set)"}`,
2283
- `NODE_OPTIONS : ${process.env.NODE_OPTIONS || "(not set β€” cloudflare-proxy.js NOT loaded in backend!)"}`,
2284
- `/opt/cloudflare-proxy.js : ${fs.existsSync("/opt/cloudflare-proxy.js") ? "βœ“ exists" : "βœ— MISSING"}`,
2285
- `/tmp/huggingpost-cloudflare-proxy.env contents:`,
2286
- cfProxyLog,
2287
- `cf-proxy-banner-shown : ${fs.existsSync("/tmp/.cf-proxy-banner-shown") ? "βœ“ (proxy init ran at least once)" : "(not found)"}`,
2288
- ].join("\n");
2289
-
2290
- const xCallbackLog = fs.existsSync("/tmp/x-oauth-callbacks.log")
2291
- ? fs.readFileSync("/tmp/x-oauth-callbacks.log", "utf8")
2292
- : "(no X callbacks recorded yet β€” try adding X channel now)";
2293
-
2294
- // Redis check: look up login:* keys and verify codeVerifier structure.
2295
- // Root cause check: oauth_token_secret must be present after split on ':'.
2296
- let redisSection = "=== REDIS codeVerifier CHECK ===\n";
2297
- try {
2298
- const { execSync } = require("child_process");
2299
- const rc = (cmd) => { try { return execSync(cmd, { timeout: 5000 }).toString().trim(); } catch(e) { return "(err: " + e.message.slice(0,60) + ")"; } };
2300
-
2301
- const keyspace = rc("redis-cli -h 127.0.0.1 -p 6379 info keyspace 2>/dev/null");
2302
- redisSection += `Redis keyspace:\n${keyspace || "(empty)"}\n\n`;
2303
-
2304
- // Check DB 0 (default) and DB 1 for all keys
2305
- // Also inspect external:* keys β€” Postiz stores codeVerifier under external:{oauth_token}
2306
- const inspectKeys = (db, pattern) => {
2307
- const dbFlag = db === 0 ? "" : `-n ${db}`;
2308
- const keys = rc(`redis-cli -h 127.0.0.1 -p 6379 ${dbFlag} keys "${pattern}" 2>/dev/null`);
2309
- if (!keys || keys.startsWith("(")) return ` ${pattern}: (none)\n`;
2310
- let out = ` ${pattern} keys found:\n`;
2311
- for (const key of keys.split("\n").filter(Boolean).slice(0, 5)) {
2312
- const type = rc(`redis-cli -h 127.0.0.1 -p 6379 ${dbFlag} type "${key}"`);
2313
- const ttl = rc(`redis-cli -h 127.0.0.1 -p 6379 ${dbFlag} ttl "${key}" 2>/dev/null`);
2314
- let val;
2315
- if (type === "hash") {
2316
- val = rc(`redis-cli -h 127.0.0.1 -p 6379 ${dbFlag} hgetall "${key}" 2>/dev/null`);
2317
- } else if (type === "string") {
2318
- val = rc(`redis-cli -h 127.0.0.1 -p 6379 ${dbFlag} get "${key}" 2>/dev/null`);
2319
- } else {
2320
- out += ` ${key} [type=${type} TTL=${ttl}]: (unhandled type)\n`; continue;
2321
- }
2322
- if (!val) { out += ` ${key} [type=${type} TTL=${ttl}]: VALUE IS EMPTY STRING\n`; continue; }
2323
- if (type === "hash") {
2324
- out += ` ${key} [type=hash TTL=${ttl}]:\n ${val.replace(/\n/g, "\n ").slice(0,300)}\n`;
2325
- continue;
2326
- }
2327
- const colonIdx = val.indexOf(":");
2328
- const token = colonIdx > 0 ? val.slice(0, colonIdx) : "";
2329
- const secret = colonIdx > 0 ? val.slice(colonIdx + 1) : "";
2330
- const extraColons = (val.match(/:/g) || []).length - 1;
2331
- const note = colonIdx < 0 ? "⚠ NO COLON" : extraColons > 0 ? `⚠ ${extraColons} extra colon(s)` : secret.length < 30 ? "⚠ secret short" : "βœ“ ok";
2332
- out += ` ${key} [type=string TTL=${ttl}s]\n token: ${token.slice(0,12)}... (len=${token.length})\n secret: ${secret.slice(0,12)}... (len=${secret.length})\n note: ${note}\n`;
2333
- }
2334
- return out;
2335
- };
2336
-
2337
- for (const db of [0, 1, 2]) {
2338
- const dbFlag = db === 0 ? "" : `-n ${db}`;
2339
- const allKeys = rc(`redis-cli -h 127.0.0.1 -p 6379 ${dbFlag} keys "*" 2>/dev/null`);
2340
- redisSection += `DB ${db}: total keys preview: ${allKeys ? allKeys.split("\n").slice(0,8).join(", ") : "(none)"}\n`;
2341
- redisSection += inspectKeys(db, "login:*");
2342
- redisSection += inspectKeys(db, "external:*");
2343
- redisSection += "\n";
2344
- }
2345
- } catch (e) {
2346
- redisSection += `(redis-cli error: ${e.message})`;
2347
- }
2348
-
2349
- // Grep Postiz source for Redis key patterns + x.provider content
2350
- let sourceSection = "=== POSTIZ SOURCE: Redis key patterns ===\n";
2351
- try {
2352
- const { execSync } = require("child_process");
2353
- const rc2 = (cmd) => { try { return execSync(cmd, { timeout: 10000 }).toString().trim(); } catch(e) { return "(err: " + e.message.slice(0,80) + ")"; } };
2354
- const grepRedis = rc2("grep -rn 'ioRedis.set\\|redis.set' /app/apps/backend/src /app/libs --include='*.ts' 2>/dev/null | grep -v node_modules | grep -E 'login:|external:|refresh:' | head -20");
2355
- sourceSection += `Redis set calls with login:/external: prefix:\n${grepRedis || "(none found)"}\n\n`;
2356
- // Find and show x.provider.ts generateAuthUrl
2357
- const xProvFile = rc2("find /app -name 'x.provider.ts' -not -path '*/node_modules/*' -not -path '*/.next/*' 2>/dev/null | head -1");
2358
- sourceSection += `x.provider.ts path: ${xProvFile || "(not found)"}\n`;
2359
- if (xProvFile && !xProvFile.startsWith("(err")) {
2360
- const xContent = rc2(`grep -n 'generateAuthUrl\\|codeVerifier\\|oauth_token\\|ioRedis\\|redis' "${xProvFile}" | head -40`);
2361
- sourceSection += `x.provider.ts relevant lines:\n${xContent || "(none)"}\n`;
2362
- }
2363
- // Also check integrations controller
2364
- const ctrlSearch = rc2("grep -rn 'external:\\|login:\\|codeVerifier\\|generateAuthUrl' /app/apps/backend/src --include='*.ts' 2>/dev/null | grep -v node_modules | head -30");
2365
- sourceSection += `\nIntegrations controller Redis/codeVerifier usage:\n${ctrlSearch || "(none)"}\n`;
2366
- } catch(e) {
2367
- sourceSection += `(error: ${e.message})`;
2368
- }
2369
-
2370
- const out = [
2371
- credSection,
2372
- "",
2373
- cfProxySection,
2374
- "",
2375
- "=== X OAUTH CALLBACKS (from health-server) ===",
2376
- xCallbackLog,
2377
- "",
2378
- redisSection,
2379
- "",
2380
- sourceSection,
2381
- "",
2382
- "=== BACKEND ERROR LOG (last 150 lines) ===",
2383
- errLog,
2384
- "",
2385
- "=== BACKEND OUT LOG (last 80 lines) ===",
2386
- outLog,
2387
- ].join("\n");
2388
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
2389
- res.end(out);
2390
- } catch (e) {
2391
- res.writeHead(500, { "Content-Type": "text/plain" });
2392
- res.end("Error reading logs: " + e.message);
2393
- }
2394
- return;
2395
- }
2396
 
2397
  // ── Dashboard at exact / ─────────────────────────────────────────────────
2398
  if (pathname === "/" || pathname === "") {
@@ -2472,43 +2164,6 @@ const server = http.createServer((req, res) => {
2472
  // the /app basePath prefix (e.g. /launches, /analytics, /api/...).
2473
  // Redirect those here rather than 404-ing so the browser lands correctly.
2474
 
2475
- // Log OAuth callbacks for debugging.
2476
- if (pathname === "/integrations/social/x") {
2477
- const oauthToken = parsedUrl.searchParams.get("oauth_token") || "(missing)";
2478
- const oauthVerifier = parsedUrl.searchParams.get("oauth_verifier") || "(MISSING!)";
2479
- const denied = parsedUrl.searchParams.get("denied");
2480
- const ts = new Date().toISOString();
2481
- const msg = denied
2482
- ? `[${ts}] X OAuth DENIED β€” denied=${denied}`
2483
- : `[${ts}] X OAuth callback β€” oauth_token=${oauthToken.slice(0,20)}... oauth_verifier=${oauthVerifier === "(MISSING!)" ? "(MISSING! β€” X did not send verifier)" : oauthVerifier.slice(0,10) + "... (present βœ“)"}`;
2484
- console.log(msg);
2485
- // Append to a file the debug-logs endpoint can read.
2486
- try {
2487
- fs.appendFileSync("/tmp/x-oauth-callbacks.log", msg + "\n");
2488
- } catch(_) {}
2489
-
2490
- // Snapshot Redis 100ms, 500ms, 1500ms after callback to catch login: key before it's deleted
2491
- if (!denied && oauthToken !== "(missing)") {
2492
- const snapshots = [];
2493
- const doSnap = (delay) => setTimeout(() => {
2494
- try {
2495
- const { execSync } = require("child_process");
2496
- const rc = (cmd) => { try { return execSync(cmd, { timeout: 3000 }).toString().trim(); } catch(e) { return "(err)"; } };
2497
- const loginVal = rc(`redis-cli -h 127.0.0.1 -p 6379 get "login:${oauthToken}" 2>/dev/null`);
2498
- const extVal = rc(`redis-cli -h 127.0.0.1 -p 6379 get "external:${oauthToken}" 2>/dev/null`);
2499
- const loginTtl = rc(`redis-cli -h 127.0.0.1 -p 6379 ttl "login:${oauthToken}" 2>/dev/null`);
2500
- const extTtl = rc(`redis-cli -h 127.0.0.1 -p 6379 ttl "external:${oauthToken}" 2>/dev/null`);
2501
- const snap = ` [+${delay}ms] login:TOKEN=${loginVal ? loginVal.slice(0,40) + "...(len=" + loginVal.length + ")" : "(empty/missing)"} ttl=${loginTtl} | external:TOKEN=${extVal ? extVal.slice(0,40) + "...(len=" + extVal.length + ")" : "(empty/missing)"} ttl=${extTtl}`;
2502
- snapshots.push(snap);
2503
- fs.appendFileSync("/tmp/x-oauth-callbacks.log", snap + "\n");
2504
- } catch(_) {}
2505
- }, delay);
2506
- doSnap(100);
2507
- doSnap(500);
2508
- doSnap(1500);
2509
- }
2510
- }
2511
-
2512
  res.writeHead(302, {
2513
  Location: "/app" + pathname + (parsedUrl.search || ""),
2514
  });
 
2085
  return;
2086
  }
2087
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2088
 
2089
  // ── Dashboard at exact / ─────────────────────────────────────────────────
2090
  if (pathname === "/" || pathname === "") {
 
2164
  // the /app basePath prefix (e.g. /launches, /analytics, /api/...).
2165
  // Redirect those here rather than 404-ing so the browser lands correctly.
2166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2167
  res.writeHead(302, {
2168
  Location: "/app" + pathname + (parsedUrl.search || ""),
2169
  });