mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
refactor: dedupe edge api json response assembly (#1702)
* refactor: dedupe edge api json response assembly * refactor: expand jsonResponse helper to all edge functions Roll out jsonResponse() from _json-response.js to 16 files (14 handlers + 2 shared helpers), eliminating 55 instances of the new Response(JSON.stringify(...)) boilerplate. Only exception: health.js uses JSON.stringify(body, null, indent) for pretty-print mode, which is incompatible with the helper signature. Replaced local jsonResponse/json() definitions in contact.js, register-interest.js, and cache-purge.js with the shared import.
This commit is contained in:
9
api/_json-response.js
Normal file
9
api/_json-response.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function jsonResponse(body, status, headers = {}) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Ratelimit } from '@upstash/ratelimit';
|
||||
import { Redis } from '@upstash/redis';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
let ratelimit = null;
|
||||
|
||||
@@ -38,16 +39,12 @@ export async function checkRateLimit(request, corsHeaders) {
|
||||
const { success, limit, reset } = await rl.limit(ip);
|
||||
|
||||
if (!success) {
|
||||
return new Response(JSON.stringify({ error: 'Too many requests' }), {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
return jsonResponse({ error: 'Too many requests' }, 429, {
|
||||
'X-RateLimit-Limit': String(limit),
|
||||
'X-RateLimit-Remaining': '0',
|
||||
'X-RateLimit-Reset': String(reset),
|
||||
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
|
||||
...corsHeaders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { validateApiKey } from './_api-key.js';
|
||||
import { checkRateLimit } from './_rate-limit.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export function getRelayBaseUrl() {
|
||||
const relayUrl = process.env.WS_RELAY_URL;
|
||||
@@ -34,29 +35,20 @@ export function createRelayHandler(cfg) {
|
||||
const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');
|
||||
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
if (req.method !== 'GET') {
|
||||
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders);
|
||||
}
|
||||
|
||||
if (cfg.requireApiKey) {
|
||||
const keyCheck = validateApiKey(req);
|
||||
if (keyCheck.required && !keyCheck.valid) {
|
||||
return new Response(JSON.stringify({ error: keyCheck.error }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: keyCheck.error }, 401, corsHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +60,7 @@ export function createRelayHandler(cfg) {
|
||||
const relayBaseUrl = getRelayBaseUrl();
|
||||
if (!relayBaseUrl) {
|
||||
if (cfg.fallback) return cfg.fallback(req, corsHeaders);
|
||||
return new Response(JSON.stringify({ error: 'WS_RELAY_URL is not configured' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'WS_RELAY_URL is not configured' }, 503, corsHeaders);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -108,13 +97,10 @@ export function createRelayHandler(cfg) {
|
||||
} catch (error) {
|
||||
if (cfg.fallback) return cfg.fallback(req, corsHeaders);
|
||||
const isTimeout = error?.name === 'AbortError';
|
||||
return new Response(JSON.stringify({
|
||||
return jsonResponse({
|
||||
error: isTimeout ? 'Relay timeout' : 'Relay request failed',
|
||||
details: error?.message || String(error),
|
||||
}), {
|
||||
status: isTimeout ? 504 : 502,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
}, isTimeout ? 504 : 502, corsHeaders);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
16
api/bootstrap.js
vendored
16
api/bootstrap.js
vendored
@@ -1,5 +1,6 @@
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { validateApiKey } from './_api-key.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
@@ -121,9 +122,7 @@ export default async function handler(req) {
|
||||
|
||||
const apiKeyResult = validateApiKey(req);
|
||||
if (apiKeyResult.required && !apiKeyResult.valid)
|
||||
return new Response(JSON.stringify({ error: apiKeyResult.error }), {
|
||||
status: 401, headers: { ...cors, 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: apiKeyResult.error }, 401, cors);
|
||||
|
||||
const url = new URL(req.url);
|
||||
const tier = url.searchParams.get('tier');
|
||||
@@ -145,10 +144,7 @@ export default async function handler(req) {
|
||||
try {
|
||||
cached = await getCachedJsonBatch(keys);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ data: {}, missing: names }), {
|
||||
status: 200,
|
||||
headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' },
|
||||
});
|
||||
return jsonResponse({ data: {}, missing: names }, 200, { ...cors, 'Cache-Control': 'no-cache' });
|
||||
}
|
||||
|
||||
const data = {};
|
||||
@@ -161,13 +157,9 @@ export default async function handler(req) {
|
||||
|
||||
const cacheControl = (tier && TIER_CACHE[tier]) || 'public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900';
|
||||
|
||||
return new Response(JSON.stringify({ data, missing }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
return jsonResponse({ data, missing }, 200, {
|
||||
...cors,
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': cacheControl,
|
||||
'CDN-Cache-Control': (tier && TIER_CDN_CACHE[tier]) || TIER_CDN_CACHE.fast,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCorsHeaders } from './_cors.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
@@ -25,13 +26,6 @@ function isDurableData(key) {
|
||||
return DURABLE_DATA_PREFIXES.some(p => key.startsWith(p));
|
||||
}
|
||||
|
||||
function json(body, status, corsHeaders) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
}
|
||||
|
||||
function getRedisCredentials() {
|
||||
const url = process.env.UPSTASH_REDIS_REST_URL;
|
||||
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
|
||||
@@ -104,20 +98,20 @@ export default async function handler(req) {
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
return json({ error: 'Method not allowed' }, 405, corsHeaders);
|
||||
return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders);
|
||||
}
|
||||
|
||||
const auth = req.headers.get('authorization') || '';
|
||||
const secret = process.env.RELAY_SHARED_SECRET;
|
||||
if (!secret || !(await timingSafeEqual(auth, `Bearer ${secret}`))) {
|
||||
return json({ error: 'Unauthorized' }, 401, corsHeaders);
|
||||
return jsonResponse({ error: 'Unauthorized' }, 401, corsHeaders);
|
||||
}
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return json({ error: 'Invalid JSON body' }, 422, corsHeaders);
|
||||
return jsonResponse({ error: 'Invalid JSON body' }, 422, corsHeaders);
|
||||
}
|
||||
|
||||
const { keys: explicitKeys, patterns, dryRun = false } = body || {};
|
||||
@@ -125,21 +119,21 @@ export default async function handler(req) {
|
||||
const hasPatterns = Array.isArray(patterns) && patterns.length > 0;
|
||||
|
||||
if (!hasKeys && !hasPatterns) {
|
||||
return json({ error: 'At least one of "keys" or "patterns" required' }, 422, corsHeaders);
|
||||
return jsonResponse({ error: 'At least one of "keys" or "patterns" required' }, 422, corsHeaders);
|
||||
}
|
||||
|
||||
if (hasKeys && explicitKeys.length > MAX_EXPLICIT_KEYS) {
|
||||
return json({ error: `"keys" exceeds max of ${MAX_EXPLICIT_KEYS}` }, 422, corsHeaders);
|
||||
return jsonResponse({ error: `"keys" exceeds max of ${MAX_EXPLICIT_KEYS}` }, 422, corsHeaders);
|
||||
}
|
||||
|
||||
if (hasPatterns && patterns.length > MAX_PATTERNS) {
|
||||
return json({ error: `"patterns" exceeds max of ${MAX_PATTERNS}` }, 422, corsHeaders);
|
||||
return jsonResponse({ error: `"patterns" exceeds max of ${MAX_PATTERNS}` }, 422, corsHeaders);
|
||||
}
|
||||
|
||||
if (hasPatterns) {
|
||||
for (const p of patterns) {
|
||||
if (typeof p !== 'string' || !p.endsWith('*') || p === '*') {
|
||||
return json({ error: `Invalid pattern "${p}": must end with "*" and cannot be bare "*"` }, 422, corsHeaders);
|
||||
return jsonResponse({ error: `Invalid pattern "${p}": must end with "*" and cannot be bare "*"` }, 422, corsHeaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,12 +172,12 @@ export default async function handler(req) {
|
||||
|
||||
if (dryRun) {
|
||||
console.log('[cache-purge]', { mode: 'dry-run', matched: keyList.length, deleted: 0, truncated, dryRun: true, ip, ts });
|
||||
return json({ matched: keyList.length, deleted: 0, keys: keyList, dryRun: true, truncated }, 200, corsHeaders);
|
||||
return jsonResponse({ matched: keyList.length, deleted: 0, keys: keyList, dryRun: true, truncated }, 200, corsHeaders);
|
||||
}
|
||||
|
||||
if (keyList.length === 0) {
|
||||
console.log('[cache-purge]', { mode: 'purge', matched: 0, deleted: 0, truncated, dryRun: false, ip, ts });
|
||||
return json({ matched: 0, deleted: 0, keys: [], dryRun: false, truncated }, 200, corsHeaders);
|
||||
return jsonResponse({ matched: 0, deleted: 0, keys: [], dryRun: false, truncated }, 200, corsHeaders);
|
||||
}
|
||||
|
||||
let deleted = 0;
|
||||
@@ -193,9 +187,9 @@ export default async function handler(req) {
|
||||
deleted = results.reduce((sum, r) => sum + (r.result || 0), 0);
|
||||
} catch (err) {
|
||||
console.log('[cache-purge]', { mode: 'purge-error', matched: keyList.length, error: err.message, ip, ts });
|
||||
return json({ error: 'Redis pipeline failed' }, 502, corsHeaders);
|
||||
return jsonResponse({ error: 'Redis pipeline failed' }, 502, corsHeaders);
|
||||
}
|
||||
|
||||
console.log('[cache-purge]', { mode: 'purge', matched: keyList.length, deleted, truncated, dryRun: false, ip, ts });
|
||||
return json({ matched: keyList.length, deleted, keys: keyList, dryRun: false, truncated }, 200, corsHeaders);
|
||||
return jsonResponse({ matched: keyList.length, deleted, keys: keyList, dryRun: false, truncated }, 200, corsHeaders);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export const config = { runtime: 'edge' };
|
||||
import { ConvexHttpClient } from 'convex/browser';
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { getClientIp, verifyTurnstile } from './_turnstile.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const PHONE_RE = /^[+(]?\d[\d\s()./-]{4,23}\d$/;
|
||||
@@ -95,15 +96,6 @@ function sanitizeForSubject(str, maxLen = 50) {
|
||||
return str.replace(/[\r\n\0]/g, '').slice(0, maxLen);
|
||||
}
|
||||
|
||||
function jsonResponse(body, status, cors) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: cors
|
||||
? { 'Content-Type': 'application/json', ...cors }
|
||||
: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
export default async function handler(req) {
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Non-sebuf: returns XML/HTML, stays as standalone Vercel function
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
// Scrape FwdStart newsletter archive and return as RSS
|
||||
export default async function handler(req) {
|
||||
const cors = getCorsHeaders(req);
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: cors });
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403, cors);
|
||||
}
|
||||
try {
|
||||
const response = await fetch('https://www.fwdstart.me/archive', {
|
||||
@@ -96,15 +97,9 @@ export default async function handler(req) {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('FwdStart scraper error:', error);
|
||||
return new Response(JSON.stringify({
|
||||
return jsonResponse({
|
||||
error: 'Failed to fetch FwdStart archive',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 502,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...cors,
|
||||
},
|
||||
});
|
||||
}, 502, cors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
export default function handler(req) {
|
||||
const cfCountry = req.headers.get('cf-ipcountry');
|
||||
const country = (cfCountry && cfCountry !== 'T1' ? cfCountry : null) || req.headers.get('x-vercel-ip-country') || 'XX';
|
||||
return new Response(JSON.stringify({ country }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
return jsonResponse({ country }, 200, {
|
||||
'Cache-Control': 'public, max-age=300, s-maxage=3600, stale-if-error=3600',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
import { readJsonFromUpstash } from './_upstash-json.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
@@ -64,31 +65,25 @@ export default async function handler(req) {
|
||||
}
|
||||
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
|
||||
}
|
||||
|
||||
const data = await fetchGpsJamData();
|
||||
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ error: 'GPS interference data temporarily unavailable' }), {
|
||||
status: 503,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache, no-store',
|
||||
...corsHeaders,
|
||||
},
|
||||
});
|
||||
return jsonResponse(
|
||||
{ error: 'GPS interference data temporarily unavailable' },
|
||||
503,
|
||||
{ 'Cache-Control': 'no-cache, no-store', ...corsHeaders },
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
return jsonResponse(
|
||||
data,
|
||||
200,
|
||||
{
|
||||
'Cache-Control': 's-maxage=3600, stale-while-revalidate=1800, stale-if-error=3600',
|
||||
...corsHeaders,
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
const BOOTSTRAP_KEYS = {
|
||||
@@ -221,11 +223,11 @@ export default async function handler(req) {
|
||||
const commands = allKeys.map(k => ['GET', k]);
|
||||
results = await redisPipeline(commands);
|
||||
} catch (err) {
|
||||
return new Response(JSON.stringify({
|
||||
return jsonResponse({
|
||||
status: 'REDIS_DOWN',
|
||||
error: err.message,
|
||||
checkedAt: new Date(now).toISOString(),
|
||||
}), { status: 503, headers });
|
||||
}, 503, headers);
|
||||
}
|
||||
|
||||
const keyValues = new Map();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
import { readJsonFromUpstash } from './_upstash-json.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
@@ -43,31 +44,25 @@ export default async function handler(req) {
|
||||
}
|
||||
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
|
||||
}
|
||||
|
||||
const data = await fetchMilitaryFlightsData();
|
||||
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ error: 'Military flight data temporarily unavailable' }), {
|
||||
status: 503,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache, no-store',
|
||||
...corsHeaders,
|
||||
},
|
||||
});
|
||||
return jsonResponse(
|
||||
{ error: 'Military flight data temporarily unavailable' },
|
||||
503,
|
||||
{ 'Cache-Control': 'no-cache, no-store', ...corsHeaders },
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
return jsonResponse(
|
||||
data,
|
||||
200,
|
||||
{
|
||||
'Cache-Control': 's-maxage=120, stale-while-revalidate=60, stale-if-error=300',
|
||||
...corsHeaders,
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRelayHandler } from './_relay.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
@@ -13,14 +14,11 @@ export default createRelayHandler({
|
||||
cacheHeaders: () => ({
|
||||
'Cache-Control': 'public, max-age=60, s-maxage=300, stale-while-revalidate=120, stale-if-error=900',
|
||||
}),
|
||||
fallback: (_req, corsHeaders) => new Response(JSON.stringify({
|
||||
fallback: (_req, corsHeaders) => jsonResponse({
|
||||
configured: false,
|
||||
alerts: [],
|
||||
historyCount24h: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'No data source available',
|
||||
}), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
}),
|
||||
}, 503, corsHeaders),
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ export const config = { runtime: 'edge' };
|
||||
import { ConvexHttpClient } from 'convex/browser';
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { getClientIp, verifyTurnstile } from './_turnstile.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const MAX_EMAIL_LENGTH = 320;
|
||||
@@ -12,15 +13,6 @@ const rateLimitMap = new Map();
|
||||
const RATE_LIMIT = 5;
|
||||
const RATE_WINDOW_MS = 60 * 60 * 1000;
|
||||
|
||||
function jsonResponse(body, status, cors) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: cors
|
||||
? { 'Content-Type': 'application/json', ...cors }
|
||||
: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
function isRateLimited(ip) {
|
||||
const now = Date.now();
|
||||
const entry = rateLimitMap.get(ip);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
@@ -21,10 +22,7 @@ export default async function handler(req) {
|
||||
const lonN = Number(lon);
|
||||
if (!lat || !lon || Number.isNaN(latN) || Number.isNaN(lonN)
|
||||
|| latN < -90 || latN > 90 || lonN < -180 || lonN > 180) {
|
||||
return new Response(JSON.stringify({ error: 'valid lat (-90..90) and lon (-180..180) required' }), {
|
||||
status: 400,
|
||||
headers: { ...cors, 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: 'valid lat (-90..90) and lon (-180..180) required' }, 400, cors);
|
||||
}
|
||||
|
||||
const redisUrl = process.env.UPSTASH_REDIS_REST_URL;
|
||||
@@ -63,10 +61,7 @@ export default async function handler(req) {
|
||||
);
|
||||
|
||||
if (!resp.ok) {
|
||||
return new Response(JSON.stringify({ error: `Nominatim ${resp.status}` }), {
|
||||
status: 502,
|
||||
headers: { ...cors, 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: `Nominatim ${resp.status}` }, 502, cors);
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
@@ -93,9 +88,6 @@ export default async function handler(req) {
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return new Response(JSON.stringify({ error: 'Nominatim request failed' }), {
|
||||
status: 502,
|
||||
headers: { ...cors, 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: 'Nominatim request failed' }, 502, cors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { validateApiKey } from './_api-key.js';
|
||||
import { checkRateLimit } from './_rate-limit.js';
|
||||
import { getRelayBaseUrl, getRelayHeaders, fetchWithTimeout } from './_relay.js';
|
||||
import RSS_ALLOWED_DOMAINS from './_rss-allowed-domains.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
@@ -60,10 +61,7 @@ export default async function handler(req) {
|
||||
const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');
|
||||
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
|
||||
}
|
||||
|
||||
// Handle CORS preflight
|
||||
@@ -71,18 +69,12 @@ export default async function handler(req) {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
if (req.method !== 'GET') {
|
||||
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders);
|
||||
}
|
||||
|
||||
const keyCheck = validateApiKey(req);
|
||||
if (keyCheck.required && !keyCheck.valid) {
|
||||
return new Response(JSON.stringify({ error: keyCheck.error }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: keyCheck.error }, 401, corsHeaders);
|
||||
}
|
||||
|
||||
const rateLimitResponse = await checkRateLimit(req, corsHeaders);
|
||||
@@ -92,10 +84,7 @@ export default async function handler(req) {
|
||||
const feedUrl = requestUrl.searchParams.get('url');
|
||||
|
||||
if (!feedUrl) {
|
||||
return new Response(JSON.stringify({ error: 'Missing url parameter' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Missing url parameter' }, 400, corsHeaders);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -104,10 +93,7 @@ export default async function handler(req) {
|
||||
// Security: Check if domain is allowed (normalize www prefix)
|
||||
const hostname = parsedUrl.hostname;
|
||||
if (!isAllowedDomain(hostname)) {
|
||||
return new Response(JSON.stringify({ error: 'Domain not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Domain not allowed' }, 403, corsHeaders);
|
||||
}
|
||||
|
||||
const isRelayOnly = RELAY_ONLY_DOMAINS.has(hostname);
|
||||
@@ -188,13 +174,10 @@ export default async function handler(req) {
|
||||
} catch (error) {
|
||||
const isTimeout = error.name === 'AbortError';
|
||||
console.error('RSS proxy error:', feedUrl, error.message);
|
||||
return new Response(JSON.stringify({
|
||||
return jsonResponse({
|
||||
error: isTimeout ? 'Feed timeout' : 'Failed to fetch feed',
|
||||
details: error.message,
|
||||
url: feedUrl
|
||||
}), {
|
||||
status: isTimeout ? 504 : 502,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
}, isTimeout ? 504 : 502, corsHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
import { readJsonFromUpstash } from './_upstash-json.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
@@ -33,24 +34,16 @@ export default async function handler(req) {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
|
||||
}
|
||||
const data = await fetchSatelliteData();
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ error: 'Satellite data temporarily unavailable' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache, no-store', ...corsHeaders },
|
||||
return jsonResponse({ error: 'Satellite data temporarily unavailable' }, 503, {
|
||||
'Cache-Control': 'no-cache, no-store', ...corsHeaders,
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
return jsonResponse(data, 200, {
|
||||
'Cache-Control': 's-maxage=3600, stale-while-revalidate=1800, stale-if-error=3600',
|
||||
...corsHeaders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { validateApiKey } from './_api-key.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
@@ -87,9 +88,7 @@ export default async function handler(req) {
|
||||
|
||||
const apiKeyResult = validateApiKey(req);
|
||||
if (apiKeyResult.required && !apiKeyResult.valid)
|
||||
return new Response(JSON.stringify({ error: apiKeyResult.error }), {
|
||||
status: 401, headers: { ...cors, 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: apiKeyResult.error }, 401, cors);
|
||||
|
||||
const now = Date.now();
|
||||
const entries = Object.entries(SEED_DOMAINS);
|
||||
@@ -99,9 +98,7 @@ export default async function handler(req) {
|
||||
try {
|
||||
metaMap = await getMetaBatch(metaKeys);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: 'Redis unavailable' }), {
|
||||
status: 503, headers: { ...cors, 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: 'Redis unavailable' }, 503, cors);
|
||||
}
|
||||
|
||||
const seeds = {};
|
||||
@@ -136,12 +133,8 @@ export default async function handler(req) {
|
||||
|
||||
const httpStatus = overall === 'healthy' ? 200 : overall === 'warning' ? 200 : 503;
|
||||
|
||||
return new Response(JSON.stringify({ overall, seeds, checkedAt: now }), {
|
||||
status: httpStatus,
|
||||
headers: {
|
||||
return jsonResponse({ overall, seeds, checkedAt: now }, httpStatus, {
|
||||
...cors,
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getRelayBaseUrl, getRelayHeaders, fetchWithTimeout } from './_relay.js';
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
@@ -7,27 +8,18 @@ export default async function handler(req) {
|
||||
const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');
|
||||
|
||||
if (isDisallowedOrigin(req)) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
|
||||
}
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
if (req.method !== 'GET') {
|
||||
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders);
|
||||
}
|
||||
|
||||
const relayBaseUrl = getRelayBaseUrl();
|
||||
if (!relayBaseUrl) {
|
||||
return new Response(JSON.stringify({ error: 'WS_RELAY_URL is not configured' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
||||
});
|
||||
return jsonResponse({ error: 'WS_RELAY_URL is not configured' }, 503, corsHeaders);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -65,12 +57,9 @@ export default async function handler(req) {
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeout = error?.name === 'AbortError';
|
||||
return new Response(JSON.stringify({
|
||||
return jsonResponse({
|
||||
error: isTimeout ? 'Relay timeout' : 'Relay request failed',
|
||||
details: error?.message || String(error),
|
||||
}), {
|
||||
status: isTimeout ? 504 : 502,
|
||||
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...corsHeaders },
|
||||
});
|
||||
}, isTimeout ? 504 : 502, { 'Cache-Control': 'no-store', ...corsHeaders });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
import { fetchLatestRelease } from './_github-release.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
|
||||
// Non-sebuf: returns XML/HTML, stays as standalone Vercel function
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
export default async function handler() {
|
||||
try {
|
||||
const release = await fetchLatestRelease('WorldMonitor-Version-Check');
|
||||
if (!release) {
|
||||
return new Response(JSON.stringify({ error: 'upstream' }), {
|
||||
status: 502,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: 'upstream' }, 502);
|
||||
}
|
||||
const tag = release.tag_name ?? '';
|
||||
const version = tag.replace(/^v/, '');
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
return jsonResponse({
|
||||
version,
|
||||
tag,
|
||||
url: release.html_url,
|
||||
prerelease: release.prerelease ?? false,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}, 200, {
|
||||
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: 'fetch_failed' }), {
|
||||
status: 502,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return jsonResponse({ error: 'fetch_failed' }, 502);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user