d3ro-voice/server/cloudflare-site-bridge/src/index.ts
Yun Chan 4aadee8264 fix(edge): route /app and /api on d3ro.chanpaca.net through the NAS tunnel again
Since 2026-09-19 the site bridge worker served every path from Pages, so the
API server on the NAS had no public route: admin login (API_SERVER_URL)
and stt-proxy (D3RO_API_URL) called https://d3ro.chanpaca.net/api/... and
got the landing page's 405.

- The worker passes /app, /api and /health through to the domain origin
  (the kd-nas tunnel) and serves everything else from Pages.
- The kd-nas tunnel sends path ^/app on d3ro.chanpaca.net to the web app
  (NAS 3002); other paths keep going to the API (NAS 5050). No extra
  hostname or DNS record is needed, so WEB_APP_ORIGIN is removed.
- API_PATH_PREFIXES joins WEB_APP_BASE_PATH in packages/core/src/web-urls.ts
  (contract regenerated).

Verified live: /app/login 200, /health and /api/health 200, API login 401 for
an unknown account (was 405), landing/legal/404 unchanged, git/sso/admin
hosts unaffected.
2026-09-26 20:19:27 +09:00

46 lines
1.9 KiB
TypeScript

// server/cloudflare-site-bridge/src/index.ts
// d3ro.chanpaca.net 한 도메인 아래 세 서비스를 잇는 브리지.
//
// /app, /app/* → 도메인 원본(Cloudflare Tunnel kd-nas) → NAS 웹앱(3002)
// /api/*, /health → 도메인 원본(Cloudflare Tunnel kd-nas) → NAS API 서버(5050)
// 그 밖 → 랜딩 사이트(site/, Cloudflare Pages)
//
// 경로 규칙의 정본은 packages/core/src/web-urls.ts 다. 터널 ingress가 path로 웹앱과 API를 나눈다.
// 워커 안에서 같은 요청을 그대로 fetch 하면 워커를 다시 거치지 않고 도메인 원본(터널)으로 간다.
// Pages 커스텀 도메인은 존 DNS에 CNAME을 요구해 d3ro 레코드(터널)와 충돌하므로, 이 워커가
// 랜딩을 Pages에서 가져온다.
import { API_PATH_PREFIXES, WEB_APP_BASE_PATH } from '../../../packages/core/src/web-urls'
const PAGES_ORIGIN = 'https://d3ro.pages.dev'
const ORIGIN_PATH_PREFIXES: readonly string[] = [WEB_APP_BASE_PATH, ...API_PATH_PREFIXES]
function servedByOrigin(pathname: string): boolean {
return ORIGIN_PATH_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))
}
async function fromPages(request: Request): Promise<Response> {
const target = new URL(request.url)
target.protocol = 'https:'
target.hostname = new URL(PAGES_ORIGIN).hostname
target.port = ''
const headers = new Headers(request.headers)
headers.delete('host')
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
return fetch(target.toString(), {
method: request.method,
headers,
body: hasBody ? request.body : undefined,
redirect: 'manual',
})
}
export default {
async fetch(request: Request): Promise<Response> {
const { pathname } = new URL(request.url)
if (servedByOrigin(pathname)) return fetch(request)
return fromPages(request)
},
}