From ddca32a68f20aab258a57587f843b546e4b54268 Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Tue, 11 Aug 2026 11:02:48 +0530 Subject: [PATCH 1/7] feat(backend): add Simkl proxy endpoints for Netlify and Supabase --- .../netlify/functions/_backend.js | 74 +++++++++ .../netlify/functions/simkl-proxy.js | 3 + supabase/functions/simkl-proxy/index.ts | 151 ++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 netlify-auth-site/netlify/functions/simkl-proxy.js create mode 100644 supabase/functions/simkl-proxy/index.ts diff --git a/netlify-auth-site/netlify/functions/_backend.js b/netlify-auth-site/netlify/functions/_backend.js index 030a113ac..c4989acfc 100644 --- a/netlify-auth-site/netlify/functions/_backend.js +++ b/netlify-auth-site/netlify/functions/_backend.js @@ -1386,6 +1386,79 @@ async function handleTraktProxy(event) { } } +const SIMKL_ALLOWED_PATHS = [ + "/oauth/pin", + "/oauth/token", + "/scrobble/", + "/sync/" +]; + +async function handleSimklProxy(event) { + const preflight = options(event); + if (preflight) return preflight; + try { + assertAppRequest(event); + const pathParam = event.queryStringParameters?.path || ""; + const method = String(event.queryStringParameters?.method || "GET").toUpperCase(); + if (!pathParam) return json(400, { error: "Missing path parameter" }); + if (!SIMKL_ALLOWED_PATHS.some((allowed) => pathParam.startsWith(allowed))) { + return json(403, { error: "Path not allowed" }); + } + const clientId = process.env.SIMKL_CLIENT_ID || ""; + const clientSecret = process.env.SIMKL_CLIENT_SECRET || ""; + if (!clientId) throw new Error("Simkl credentials not configured"); + const simklUrl = new URL(`https://api.simkl.com${pathParam}`); + Object.entries(event.queryStringParameters || {}).forEach(([key, value]) => { + if (key !== "path" && key !== "method" && value !== undefined && value !== null) { + simklUrl.searchParams.set(key, String(value)); + } + }); + + let requestBody = undefined; + if (method === "POST" || method === "DELETE") { + let body = {}; + try { + body = event.body + ? JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString("utf8") : event.body) + : {}; + } catch { + body = {}; + } + if (pathParam.includes("/oauth/token")) { + body.client_id = clientId; + if (clientSecret) body.client_secret = clientSecret; + } + requestBody = Object.keys(body).length > 0 ? JSON.stringify(body) : undefined; + } + + const headers = { + "content-type": "application/json", + "simkl-api-key": clientId + }; + const userToken = event.headers["x-user-token"] || event.headers["X-User-Token"]; + if (userToken) headers.authorization = `Bearer ${userToken}`; + + const response = await fetch(simklUrl, { method, headers, body: requestBody }); + const text = await response.text(); + let data; + try { + data = text ? JSON.parse(text) : { status: response.status }; + } catch { + data = text ? { raw: text } : { status: response.status }; + } + return { + statusCode: response.status, + headers: { + ...JSON_HEADERS, + "cache-control": "no-store" + }, + body: JSON.stringify(data) + }; + } catch (error) { + return json(502, { error: errorMessage(error) }); + } +} + function payloadMetrics(payload) { const root = typeof payload === "string" ? JSON.parse(payload) : payload; const profiles = Array.isArray(root.profiles) ? root.profiles : null; @@ -2314,6 +2387,7 @@ module.exports = { handleCloudAuthReset, handleTmdbProxy, handleTraktProxy, + handleSimklProxy, handleTvAuthApprove, handleTvAuthComplete, handleTvAuthStart, diff --git a/netlify-auth-site/netlify/functions/simkl-proxy.js b/netlify-auth-site/netlify/functions/simkl-proxy.js new file mode 100644 index 000000000..a7dece5b2 --- /dev/null +++ b/netlify-auth-site/netlify/functions/simkl-proxy.js @@ -0,0 +1,3 @@ +const { handleSimklProxy } = require("./_backend"); + +exports.handler = handleSimklProxy; diff --git a/supabase/functions/simkl-proxy/index.ts b/supabase/functions/simkl-proxy/index.ts new file mode 100644 index 000000000..ff9c3e0e5 --- /dev/null +++ b/supabase/functions/simkl-proxy/index.ts @@ -0,0 +1,151 @@ +// Simkl API Proxy - Secured with rate limiting and path allowlist +// Deploy with: npx supabase functions deploy simkl-proxy +// Set secrets: +// npx supabase secrets set SIMKL_CLIENT_ID=your_id +// npx supabase secrets set SIMKL_CLIENT_SECRET=your_secret +// npx supabase secrets set APP_ANON_KEY=your_anon_key + +import { serve } from "https://deno.land/std@0.168.0/http/server.ts" + +const SIMKL_BASE_URL = "https://api.simkl.com" + +const RATE_LIMIT = 100 +const RATE_WINDOW_MS = 60 * 1000 +const rateLimitMap = new Map() + +const ALLOWED_PATHS = [ + '/oauth/pin', + '/oauth/token', + '/scrobble/', + '/sync/' +] + +function isPathAllowed(path: string): boolean { + return ALLOWED_PATHS.some(allowed => path.startsWith(allowed)) +} + +function getClientIP(req: Request): string { + return req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + req.headers.get('x-real-ip') || + req.headers.get('cf-connecting-ip') || + 'unknown' +} + +function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number } { + const now = Date.now() + const record = rateLimitMap.get(ip) + + if (!record || now > record.resetTime) { + rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_WINDOW_MS }) + return { allowed: true, remaining: RATE_LIMIT - 1, resetIn: RATE_WINDOW_MS } + } + + if (record.count >= RATE_LIMIT) { + return { allowed: false, remaining: 0, resetIn: record.resetTime - now } + } + + record.count++ + return { allowed: true, remaining: RATE_LIMIT - record.count, resetIn: record.resetTime - now } +} + +const DEFAULT_ALLOWED_ORIGINS = (Deno.env.get('CORS_ALLOWED_ORIGINS') || 'https://auth.arvio.tv,https://arvio.tv').split(',').map(s => s.trim()).filter(Boolean) + +function corsHeaders(req: Request) { + const origin = req.headers.get('origin') || '' + const allowed = DEFAULT_ALLOWED_ORIGINS + const allowOrigin = allowed.includes(origin) ? origin : 'null' + return { + 'Access-Control-Allow-Origin': allowOrigin, + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-user-token', + } +} + +serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders(req) }) + } + + try { + const clientIP = getClientIP(req) + const rateCheck = checkRateLimit(clientIP) + if (!rateCheck.allowed) { + return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), { + headers: { ...corsHeaders(req), 'Content-Type': 'application/json' }, + status: 429, + }) + } + + const SIMKL_CLIENT_ID = Deno.env.get('SIMKL_CLIENT_ID') + if (!SIMKL_CLIENT_ID) { + throw new Error('Simkl credentials not configured') + } + + const url = new URL(req.url) + const path = url.searchParams.get('path') + const method = url.searchParams.get('method') || 'GET' + + if (!path || !isPathAllowed(path)) { + return new Response(JSON.stringify({ error: 'Path not allowed' }), { + headers: { ...corsHeaders(req), 'Content-Type': 'application/json' }, + status: 403, + }) + } + + const simklUrl = new URL(`${SIMKL_BASE_URL}${path}`) + url.searchParams.forEach((value, key) => { + if (key !== 'path' && key !== 'method') { + simklUrl.searchParams.set(key, value) + } + }) + + const headers: Record = { + 'Content-Type': 'application/json', + 'simkl-api-key': SIMKL_CLIENT_ID, + } + + const userToken = req.headers.get('x-user-token') + if (userToken) { + headers['Authorization'] = `Bearer ${userToken}` + } + + let body: string | undefined + if (method === 'POST' || method === 'DELETE') { + let reqBody: Record = {} + try { + reqBody = await req.json() + } catch { + // empty body + } + if (path.includes('/oauth/token')) { + reqBody.client_id = SIMKL_CLIENT_ID + const SIMKL_CLIENT_SECRET = Deno.env.get('SIMKL_CLIENT_SECRET') + if (SIMKL_CLIENT_SECRET) reqBody.client_secret = SIMKL_CLIENT_SECRET + } + body = Object.keys(reqBody).length > 0 ? JSON.stringify(reqBody) : undefined + } + + const response = await fetch(simklUrl.toString(), { + method: method, + headers: headers, + body: body, + }) + + const responseText = await response.text() + let data + try { + data = responseText ? JSON.parse(responseText) : { status: response.status } + } catch { + data = responseText ? { raw: responseText } : { status: response.status } + } + + return new Response(JSON.stringify(data), { + headers: { ...corsHeaders(req), 'Content-Type': 'application/json' }, + status: response.status, + }) + } catch (error) { + return new Response(JSON.stringify({ error: (error as Error).message }), { + headers: { ...corsHeaders(req), 'Content-Type': 'application/json' }, + status: 500, + }) + } +}) From 63dedde5693bb006231f72313c471dd424b3f312 Mon Sep 17 00:00:00 2001 From: Arvin Date: Tue, 11 Aug 2026 10:44:34 +0200 Subject: [PATCH 2/7] fix(auth): return cloud login to web previews --- netlify-auth-site/index.html | 18 ++++--- netlify-auth-site/tests/redirect-uri.test.js | 53 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 netlify-auth-site/tests/redirect-uri.test.js diff --git a/netlify-auth-site/index.html b/netlify-auth-site/index.html index 737a4e89d..6a52018b7 100644 --- a/netlify-auth-site/index.html +++ b/netlify-auth-site/index.html @@ -543,12 +543,18 @@

Account Access

if (!uri) return false; try { const url = new URL(uri); - return ( - url.hostname === "arvio.tv" || - url.hostname.endsWith(".arvio.tv") || - url.hostname === "localhost" || - url.hostname === "127.0.0.1" - ); + const isLocal = + (url.protocol === "http:" || url.protocol === "https:") && + (url.hostname === "localhost" || url.hostname === "127.0.0.1"); + const isArvioDomain = + url.protocol === "https:" && + (url.hostname === "arvio.tv" || url.hostname.endsWith(".arvio.tv")); + const isArvioWebPreview = + url.protocol === "https:" && + (url.hostname === "arvio-web.netlify.app" || + url.hostname.endsWith("--arvio-web.netlify.app")); + + return !url.username && !url.password && (isLocal || isArvioDomain || isArvioWebPreview); } catch { return false; } diff --git a/netlify-auth-site/tests/redirect-uri.test.js b/netlify-auth-site/tests/redirect-uri.test.js new file mode 100644 index 000000000..9eb98d472 --- /dev/null +++ b/netlify-auth-site/tests/redirect-uri.test.js @@ -0,0 +1,53 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const vm = require("node:vm"); + +function loadRedirectValidator() { + const html = fs.readFileSync(path.join(__dirname, "..", "index.html"), "utf8"); + const start = html.indexOf(" function isValidRedirectUri(uri) {"); + const end = html.indexOf(" const statusEl", start); + assert.notEqual(start, -1, "redirect validator should exist in auth portal"); + assert.notEqual(end, -1, "redirect validator should have a stable end marker"); + + const context = { URL }; + vm.runInNewContext(`${html.slice(start, end)}\nvalidator = isValidRedirectUri;`, context); + return context.validator; +} + +const isValidRedirectUri = loadRedirectValidator(); + +test("accepts ARVIO production, local, and web preview callback URLs", () => { + const allowed = [ + "https://web.arvio.tv/", + "https://arvio.tv/", + "https://arvio-web.netlify.app/", + "https://simkl-web--arvio-web.netlify.app/", + "https://deploy-preview-554--arvio-web.netlify.app/", + "https://devserver-simkl-web--arvio-web.netlify.app/", + "http://localhost:3000/", + "http://127.0.0.1:3000/" + ]; + + for (const uri of allowed) { + assert.equal(isValidRedirectUri(uri), true, uri); + } +}); + +test("rejects unrelated, deceptive, and insecure callback URLs", () => { + const rejected = [ + "https://example.com/", + "https://arvio-web.netlify.app.example.com/", + "https://arvio-web--attacker.netlify.app/", + "http://simkl-web--arvio-web.netlify.app/", + "https://user:password@web.arvio.tv/", + "javascript:alert(1)", + "not-a-url", + "" + ]; + + for (const uri of rejected) { + assert.equal(isValidRedirectUri(uri), false, uri); + } +}); From 193b292bf788e56f1d72a8d5b224db6b64a29d8d Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Wed, 12 Aug 2026 09:46:45 +0530 Subject: [PATCH 3/7] fix(backend): expand SIMKL_ALLOWED_PATHS and improve error status code handling --- netlify-auth-site/netlify/functions/_backend.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/netlify-auth-site/netlify/functions/_backend.js b/netlify-auth-site/netlify/functions/_backend.js index c4989acfc..bd4b4c6ef 100644 --- a/netlify-auth-site/netlify/functions/_backend.js +++ b/netlify-auth-site/netlify/functions/_backend.js @@ -1390,7 +1390,11 @@ const SIMKL_ALLOWED_PATHS = [ "/oauth/pin", "/oauth/token", "/scrobble/", - "/sync/" + "/sync/", + "/search/", + "/movies/", + "/tv/", + "/anime/" ]; async function handleSimklProxy(event) { @@ -1455,7 +1459,8 @@ async function handleSimklProxy(event) { body: JSON.stringify(data) }; } catch (error) { - return json(502, { error: errorMessage(error) }); + const status = error.statusCode || 502; + return json(status, { error: errorMessage(error) }); } } From 68ff7761f86faebcaf485b78a459e789aeab83c7 Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Wed, 12 Aug 2026 08:51:31 +0530 Subject: [PATCH 4/7] fix(backend): update local server and assertAppRequest for local proxy testing --- netlify-auth-site/local-server.js | 93 +++++++++++++++++++ .../netlify/functions/_backend.js | 1 + 2 files changed, 94 insertions(+) create mode 100644 netlify-auth-site/local-server.js diff --git a/netlify-auth-site/local-server.js b/netlify-auth-site/local-server.js new file mode 100644 index 000000000..558a485d6 --- /dev/null +++ b/netlify-auth-site/local-server.js @@ -0,0 +1,93 @@ +const http = require("http"); +const url = require("url"); +const fs = require("fs"); +const path = require("path"); + +function loadSecrets() { + process.env.IS_LOCAL_DEV = "true"; + process.env.APP_ANON_KEY = (process.env.APP_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3OiOiJzdXBhYmFzZSIsInJlZiI6InpyZHd2b3J0Y2Zub3lrbHR6dXFmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjY3NDU4NzMsImV4cCI6MjA4MjMyMTg3M30.YfKZbSwxGs6_xMd6jkDtn1PKkfuyOHo9qVhUvFRddGU").trim().replace(/\r/g, ""); + try { + const secretsPath = path.join(__dirname, "..", "secrets.properties"); + if (fs.existsSync(secretsPath)) { + const content = fs.readFileSync(secretsPath, "utf8"); + content.split("\n").forEach((line) => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith("#")) { + const parts = trimmed.split("="); + const key = parts[0]?.trim(); + const val = parts.slice(1).join("=").trim().replace(/\r/g, ""); + if (key && val && !val.startsWith("your-")) { + process.env[key] = val; + } + } + }); + } + } catch (e) { + // Ignore secrets loading error + } +} + +// Initial load +loadSecrets(); + +const { handleSimklProxy } = require("./netlify/functions/_backend.js"); + +const PORT = process.env.PORT || 8888; + +const server = http.createServer(async (req, res) => { + // Re-load secrets dynamically on incoming requests + loadSecrets(); + + // CORS headers + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, apikey, x-user-token"); + + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname || ""; + + // Read request body + let body = ""; + req.on("data", (chunk) => { body += chunk; }); + req.on("end", async () => { + try { + const event = { + httpMethod: req.method, + headers: req.headers, + queryStringParameters: parsedUrl.query || {}, + body: body || null, + isBase64Encoded: false + }; + + if (pathname.includes("simkl-proxy")) { + const result = await handleSimklProxy(event); + const status = result.statusCode || 200; + console.log(`[${req.method}] ${pathname} -> ${status}`); + if (status >= 400) { + console.warn(` ⚠️ Error (${status}):`, result.body || "(empty body)"); + } + res.writeHead(status, result.headers || { "Content-Type": "application/json" }); + res.end(result.body || ""); + } else { + console.log(`[${req.method}] ${pathname} -> 404`); + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: `Path ${pathname} not found on local server` })); + } + } catch (err) { + console.error("Error handling local request:", err); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: err.message || "Internal server error" })); + } + }); +}); + +server.listen(PORT, "0.0.0.0", () => { + console.log(`Local Netlify Auth Backend running at http://0.0.0.0:${PORT}`); + console.log(`Simkl Proxy endpoint available at http://0.0.0.0:${PORT}/.netlify/functions/simkl-proxy`); +}); diff --git a/netlify-auth-site/netlify/functions/_backend.js b/netlify-auth-site/netlify/functions/_backend.js index bd4b4c6ef..1c4e4ce21 100644 --- a/netlify-auth-site/netlify/functions/_backend.js +++ b/netlify-auth-site/netlify/functions/_backend.js @@ -75,6 +75,7 @@ function appAnonKey() { } function assertAppRequest(event) { + if (process.env.IS_LOCAL_DEV === "true") return; const expected = appAnonKey(); if (!expected) { throw new Error("APP_ANON_KEY is not configured"); From 3a7c36b8be09ca47151948c7d29562a423394319 Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Wed, 12 Aug 2026 09:57:25 +0530 Subject: [PATCH 5/7] fix(backend): add query client_id fallback in handleSimklProxy --- netlify-auth-site/netlify/functions/_backend.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netlify-auth-site/netlify/functions/_backend.js b/netlify-auth-site/netlify/functions/_backend.js index 1c4e4ce21..5894d9238 100644 --- a/netlify-auth-site/netlify/functions/_backend.js +++ b/netlify-auth-site/netlify/functions/_backend.js @@ -1409,8 +1409,8 @@ async function handleSimklProxy(event) { if (!SIMKL_ALLOWED_PATHS.some((allowed) => pathParam.startsWith(allowed))) { return json(403, { error: "Path not allowed" }); } - const clientId = process.env.SIMKL_CLIENT_ID || ""; - const clientSecret = process.env.SIMKL_CLIENT_SECRET || ""; + const clientId = process.env.SIMKL_CLIENT_ID || event.queryStringParameters?.client_id || ""; + const clientSecret = process.env.SIMKL_CLIENT_SECRET || event.queryStringParameters?.client_secret || ""; if (!clientId) throw new Error("Simkl credentials not configured"); const simklUrl = new URL(`https://api.simkl.com${pathParam}`); Object.entries(event.queryStringParameters || {}).forEach(([key, value]) => { From 7002359b9e4ca6befa72bc2f7ed33433817a140d Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Wed, 12 Aug 2026 10:10:39 +0530 Subject: [PATCH 6/7] fix(backend): add getHeader for case-insensitive header verification in assertAppRequest --- netlify-auth-site/netlify/functions/_backend.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/netlify-auth-site/netlify/functions/_backend.js b/netlify-auth-site/netlify/functions/_backend.js index 5894d9238..372e1b0f2 100644 --- a/netlify-auth-site/netlify/functions/_backend.js +++ b/netlify-auth-site/netlify/functions/_backend.js @@ -74,14 +74,22 @@ function appAnonKey() { return process.env.APP_ANON_KEY || ""; } +function getHeader(headers = {}, name = "") { + const target = String(name || "").toLowerCase(); + for (const [k, v] of Object.entries(headers || {})) { + if (String(k || "").toLowerCase() === target) return String(v || ""); + } + return ""; +} + function assertAppRequest(event) { if (process.env.IS_LOCAL_DEV === "true") return; const expected = appAnonKey(); if (!expected) { throw new Error("APP_ANON_KEY is not configured"); } - const apiKey = String(event.headers.apikey || event.headers.Apikey || "").trim(); - const auth = event.headers.authorization || event.headers.Authorization || ""; + const apiKey = getHeader(event.headers, "apikey").trim(); + const auth = getHeader(event.headers, "authorization").trim(); const bearer = auth.match(/^Bearer\s+(.+)$/i)?.[1]?.trim() || ""; if (apiKey === expected || bearer === expected) return; const error = new Error("Unauthorized"); @@ -1440,7 +1448,7 @@ async function handleSimklProxy(event) { "content-type": "application/json", "simkl-api-key": clientId }; - const userToken = event.headers["x-user-token"] || event.headers["X-User-Token"]; + const userToken = getHeader(event.headers, "x-user-token").trim(); if (userToken) headers.authorization = `Bearer ${userToken}`; const response = await fetch(simklUrl, { method, headers, body: requestBody }); From 2cb8f831e995b186e0583af4605cbbfa24d942aa Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Wed, 12 Aug 2026 17:36:39 +0530 Subject: [PATCH 7/7] chore(backend): remove local testing server script --- netlify-auth-site/local-server.js | 93 ------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 netlify-auth-site/local-server.js diff --git a/netlify-auth-site/local-server.js b/netlify-auth-site/local-server.js deleted file mode 100644 index 558a485d6..000000000 --- a/netlify-auth-site/local-server.js +++ /dev/null @@ -1,93 +0,0 @@ -const http = require("http"); -const url = require("url"); -const fs = require("fs"); -const path = require("path"); - -function loadSecrets() { - process.env.IS_LOCAL_DEV = "true"; - process.env.APP_ANON_KEY = (process.env.APP_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3OiOiJzdXBhYmFzZSIsInJlZiI6InpyZHd2b3J0Y2Zub3lrbHR6dXFmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjY3NDU4NzMsImV4cCI6MjA4MjMyMTg3M30.YfKZbSwxGs6_xMd6jkDtn1PKkfuyOHo9qVhUvFRddGU").trim().replace(/\r/g, ""); - try { - const secretsPath = path.join(__dirname, "..", "secrets.properties"); - if (fs.existsSync(secretsPath)) { - const content = fs.readFileSync(secretsPath, "utf8"); - content.split("\n").forEach((line) => { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith("#")) { - const parts = trimmed.split("="); - const key = parts[0]?.trim(); - const val = parts.slice(1).join("=").trim().replace(/\r/g, ""); - if (key && val && !val.startsWith("your-")) { - process.env[key] = val; - } - } - }); - } - } catch (e) { - // Ignore secrets loading error - } -} - -// Initial load -loadSecrets(); - -const { handleSimklProxy } = require("./netlify/functions/_backend.js"); - -const PORT = process.env.PORT || 8888; - -const server = http.createServer(async (req, res) => { - // Re-load secrets dynamically on incoming requests - loadSecrets(); - - // CORS headers - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, apikey, x-user-token"); - - if (req.method === "OPTIONS") { - res.writeHead(204); - res.end(); - return; - } - - const parsedUrl = url.parse(req.url, true); - const pathname = parsedUrl.pathname || ""; - - // Read request body - let body = ""; - req.on("data", (chunk) => { body += chunk; }); - req.on("end", async () => { - try { - const event = { - httpMethod: req.method, - headers: req.headers, - queryStringParameters: parsedUrl.query || {}, - body: body || null, - isBase64Encoded: false - }; - - if (pathname.includes("simkl-proxy")) { - const result = await handleSimklProxy(event); - const status = result.statusCode || 200; - console.log(`[${req.method}] ${pathname} -> ${status}`); - if (status >= 400) { - console.warn(` ⚠️ Error (${status}):`, result.body || "(empty body)"); - } - res.writeHead(status, result.headers || { "Content-Type": "application/json" }); - res.end(result.body || ""); - } else { - console.log(`[${req.method}] ${pathname} -> 404`); - res.writeHead(404, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: `Path ${pathname} not found on local server` })); - } - } catch (err) { - console.error("Error handling local request:", err); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: err.message || "Internal server error" })); - } - }); -}); - -server.listen(PORT, "0.0.0.0", () => { - console.log(`Local Netlify Auth Backend running at http://0.0.0.0:${PORT}`); - console.log(`Simkl Proxy endpoint available at http://0.0.0.0:${PORT}/.netlify/functions/simkl-proxy`); -});