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:
Elie Habib
2026-03-16 11:52:56 +04:00
committed by GitHub
parent be93a940a3
commit bdd8743a26
19 changed files with 122 additions and 237 deletions

9
api/_json-response.js Normal file
View File

@@ -0,0 +1,9 @@
export function jsonResponse(body, status, headers = {}) {
return new Response(JSON.stringify(body), {
status,
headers: {
'Content-Type': 'application/json',
...headers,
},
});
}

View File

@@ -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',
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': String(reset),
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
...corsHeaders,
},
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,
});
}

View File

@@ -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);
}
};
}

22
api/bootstrap.js vendored
View File

@@ -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: {
...cors,
'Content-Type': 'application/json',
'Cache-Control': cacheControl,
'CDN-Cache-Control': (tier && TIER_CDN_CACHE[tier]) || TIER_CDN_CACHE.fast,
},
return jsonResponse({ data, missing }, 200, {
...cors,
'Cache-Control': cacheControl,
'CDN-Cache-Control': (tier && TIER_CDN_CACHE[tier]) || TIER_CDN_CACHE.fast,
});
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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',
'Cache-Control': 'public, max-age=300, s-maxage=3600, stale-if-error=3600',
'Access-Control-Allow-Origin': '*',
},
return jsonResponse({ country }, 200, {
'Cache-Control': 'public, max-age=300, s-maxage=3600, stale-if-error=3600',
'Access-Control-Allow-Origin': '*',
});
}

View File

@@ -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,
},
});
);
}

View File

@@ -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();

View File

@@ -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,
},
});
);
}

View File

@@ -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),
});

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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',
'Cache-Control': 's-maxage=3600, stale-while-revalidate=1800, stale-if-error=3600',
...corsHeaders,
},
return jsonResponse(data, 200, {
'Cache-Control': 's-maxage=3600, stale-while-revalidate=1800, stale-if-error=3600',
...corsHeaders,
});
}

View File

@@ -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: {
...cors,
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
},
return jsonResponse({ overall, seeds, checkedAt: now }, httpStatus, {
...cors,
'Cache-Control': 'no-cache',
});
}

View File

@@ -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 });
}
}

View File

@@ -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',
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600',
'Access-Control-Allow-Origin': '*',
},
}, 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);
}
}