CarboAny / src /lib /apiAdapter.ts
Esketch's picture
Claude Fable 5
deploy: Campaign Control Plane + Security Hardening Release (Orphan Clean Build v3)
5c98911
Raw History Blame Contribute Delete
5.99 kB
import { supabase } from './supabase'
// --- Strangler Pattern Configuration Switches ---
const USE_GO_GATEWAY: Record<string, boolean> = {
'verify-receipt': false,
'nature-letter': false,
'embed-text': false,
'explain-methodology': false,
'mock-vcs': false,
}
let GO_GATEWAY_URL = 'http://localhost:8080'
const GO_GATEWAY_ENDPOINTS: Record<string, (payload: unknown) => string> = {
'verify-receipt': (payload) => {
const submissionId = getPayloadString(payload, 'submissionId') ?? getPayloadString(payload, 'id')
return submissionId ? `/submissions/${encodeURIComponent(submissionId)}/verify` : '/verify-receipt'
},
'nature-letter': () => '/nature/letter',
'embed-text': () => '/methodologies/reseed',
'explain-methodology': () => '/methodologies/match',
'mock-vcs': () => '/registries/vcs/submit',
}
export function setUseGoGateway(functionName: string, use: boolean): void {
if (functionName in USE_GO_GATEWAY) {
USE_GO_GATEWAY[functionName] = use
}
}
export function setGoGatewayUrl(url: string): void {
GO_GATEWAY_URL = url
}
function getPayloadString(payload: unknown, key: string): string | undefined {
if (!payload || typeof payload !== 'object') return undefined
const value = (payload as Record<string, unknown>)[key]
return typeof value === 'string' && value.trim().length > 0 ? value : undefined
}
function isEnvelopeResponse(value: unknown): value is { data: unknown; error?: unknown } {
return (
!!value &&
typeof value === 'object' &&
'data' in value &&
'meta' in value &&
'error' in value
)
}
function unwrapEnvelope(functionName: string, response: unknown): unknown {
if (!isEnvelopeResponse(response)) return response
if (response.error) {
throw new Error(`[apiAdapter] ${functionName} Gateway 응닡 였λ₯˜: ${String(response.error)}`)
}
return response.data
}
// --- Data Contract Verification Guard (Contract Assertions) ---
/**
* Go Gateway둜 μ΄μ£Όν•˜κ±°λ‚˜ Deno ν•¨μˆ˜ 톡신 μ‹œ 데이터 μŠ€ν‚€λ§ˆ 깨짐(Contract Broken)으둜 μΈν•œ
* ν”„λ‘ νŠΈμ—”λ“œ 폭사λ₯Ό λ§‰λŠ” λŸ°νƒ€μž„ νƒ€μž… 검증 κ°€λ“œ
*/
function assertDataContract(functionName: string, data: unknown): void {
if (!data) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: ${functionName} 응닡이 λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.`)
}
const record = data as Record<string, unknown>
switch (functionName) {
case 'verify-receipt':
if (record.success === undefined && record.status === undefined) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: verify-receipt 응닡에 ν•„μˆ˜ ν•„λ“œ(success)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
if (record.co2Reduction === undefined && record.co2_reduction === undefined) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: verify-receipt 응닡에 ν•„μˆ˜ ν•„λ“œ(co2Reduction)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
break;
case 'nature-letter':
if (!record.body) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: nature-letter 응닡에 ν•„μˆ˜ ν•„λ“œ(body)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
break;
case 'embed-text':
if (!Array.isArray(record.embedding)) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: embed-text 응닡에 ν•„μˆ˜ ν•„λ“œ(embedding)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
break;
case 'explain-methodology':
if (record.eligibilityScore === undefined && record.eligibility_score === undefined) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: explain-methodology 응닡에 ν•„μˆ˜ ν•„λ“œ(eligibilityScore)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
if (!record.explanation) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: explain-methodology 응닡에 ν•„μˆ˜ ν•„λ“œ(explanation)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
break;
case 'mock-vcs':
if (record.success === undefined) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: mock-vcs 응닡에 ν•„μˆ˜ ν•„λ“œ(success)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
if (!record.externalId && !record.external_id) {
throw new Error(`[apiAdapter] API 계약 검증 μ‹€νŒ¨: mock-vcs 응닡에 ν•„μˆ˜ ν•„λ“œ(externalId)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}
break;
default:
// Unrecognized function, bypass contract validation
break;
}
}
// --- Unified API Adapter Caller ---
/**
* 5λŒ€ Supabase Deno Edge Functions ν˜ΈμΆœμ„ 단일 μΆ”μƒν™”ν•˜μ—¬ ν˜ΈμΆœν•˜κ³ 
* Go API κ²Œμ΄νŠΈμ›¨μ΄ 완곡 μ‹œ λΌμš°νŒ…μ„ μ μ§„μ μœΌλ‘œ μ•ˆμ „ 우회(Strangler Pattern)μ‹œν‚΅λ‹ˆλ‹€.
*/
export async function invokeEdgeFunction<TReq, TRes>(
functionName: string,
payload: TReq
): Promise<TRes> {
const useGo = USE_GO_GATEWAY[functionName] || false
if (useGo) {
// 1. Go API Gateway둜 우회 (fetch API)
const endpoint = GO_GATEWAY_ENDPOINTS[functionName]?.(payload) ?? `/${functionName}`
const targetUrl = `${GO_GATEWAY_URL}/api/v1${endpoint}`
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const errText = await response.text()
throw new Error(`[apiAdapter] Go Gateway API 호좜 μ‹€νŒ¨ (${response.status}): ${errText}`)
}
const rawData = await response.json()
const resData = unwrapEnvelope(functionName, rawData)
// λŸ°νƒ€μž„ 데이터 계약 검증
assertDataContract(functionName, resData)
return resData as TRes
} else {
// 2. 기쑴 Supabase Deno Edge Function 호좜
const { data, error } = await supabase.functions.invoke(functionName, {
body: payload as Record<string, unknown>,
})
if (error) {
throw new Error(error.message || String(error))
}
// λŸ°νƒ€μž„ 데이터 계약 검증
const resData = unwrapEnvelope(functionName, data)
assertDataContract(functionName, resData)
return resData as TRes
}
}