import type { Setting } from "@prisma/client"; export function normalizeThreadsAppId(value: string | null | undefined): string | undefined { const trimmed = value?.trim(); if (!trimmed) return undefined; if (!/^\d+$/.test(trimmed)) return undefined; return trimmed; } /** Meta App 憑證僅從部署層 .env 讀取(THREADS_APP_ID / THREADS_APP_SECRET)。 */ export function getThreadsAppId(): string | undefined { return normalizeThreadsAppId(process.env.THREADS_APP_ID); } export function getThreadsAppSecret(): string | undefined { return process.env.THREADS_APP_SECRET?.trim() || undefined; } export function isThreadsAppConfigured(): boolean { return !!(getThreadsAppId() && getThreadsAppSecret()); } type ThreadsAccountAuth = { threadsAccessToken: string | null; threadsUserId: string | null; }; export function accountHasThreadsToken(account: ThreadsAccountAuth | null): boolean { return !!(account?.threadsAccessToken?.trim() && account?.threadsUserId?.trim()); } export function getThreadsCredentials( settings: Setting, account: ThreadsAccountAuth | null, overrideToken?: string | null ) { const appId = getThreadsAppId(); const appSecret = getThreadsAppSecret(); const accessToken = (overrideToken ?? account?.threadsAccessToken)?.trim(); const userId = account?.threadsUserId?.trim(); if (!accessToken || !userId) return null; return { appId, appSecret, accessToken, userId, }; } export function maskToken(token?: string | null): string | null { if (!token) return null; if (token.length <= 8) return "••••"; return `••••${token.slice(-6)}`; } export function normalizeAppUrl(value: string | null | undefined): string | undefined { const trimmed = value?.trim(); if (!trimmed) return undefined; try { const url = new URL(trimmed); if (!["http:", "https:"].includes(url.protocol)) return undefined; return `${url.protocol}//${url.host}`.replace(/\/$/, ""); } catch { return undefined; } } export function getAppBaseUrl( request?: Request, appUrlOverride?: string | null ): string { const fromSettings = normalizeAppUrl(appUrlOverride); if (fromSettings) return fromSettings; const fromEnv = normalizeAppUrl(process.env.APP_URL) || normalizeAppUrl(process.env.NEXT_PUBLIC_APP_URL); if (fromEnv) return fromEnv; if (request) { const host = request.headers.get("x-forwarded-host") || request.headers.get("host"); const proto = request.headers.get("x-forwarded-proto") || "http"; if (host) return `${proto}://${host}`; } return "http://localhost:3000"; } export function isPubliclyReachableUrl(url: string): boolean { try { const { hostname } = new URL(url); return !["localhost", "127.0.0.1", "0.0.0.0"].includes(hostname); } catch { return false; } }