/* * 이 패키지가 여는 HTTP 자리 — 지금은 웹훅 하나다. * * ── 왜 여기인가 ───────────────────────────────────────────────────────────── * 커넥터는 주소를 만들 수 없다. 어댑터 계약에 주소를 등록하는 자리가 없고, 있어서도 안 된다 — 주소는 * 앱의 것이고 커넥터마다 제각기 열면 인증도 제각기가 된다. * * 그래서 입구는 **하나**다. 어느 연결의 것인지는 주소의 이름으로 가려내고, 본문을 우리 어휘로 옮기는 * 것은 그 연결의 커넥터가 한다(§`reference-hook`). * * ── 왜 도메인 공개 라우터인가 ─────────────────────────────────────────────── * 밀어 주는 쪽은 사람이 아니다. 사람 인증(JWT)을 요구하면 연결된 시스템이 우리 토큰을 발급받아 * 갱신하며 관리해야 하고, 대부분의 현장에서 그것 때문에 연동이 서지 않는다. * * 대신 **연결마다 비밀값**을 두고 그것으로 확인한다. 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 — * 인증 없는 입구를 열어 두는 것보다 훅을 못 쓰는 것이 낫다. */ import { liveIngest } from './service/reference/live-ingest.js' import { requestFill } from './service/reference/fill-loop.js' import { handleHook } from './service/reference/reference-hook.js' import { deliverAttention } from './service/reference/attention-lane.js' import { databaseStore } from './service/reference/hook-store.js' process.on('bootstrap-module-domain-public-route' as any, (_app: any, router: any) => { /* * 주소에 트윈까지 적는다 — 어느 트윈에 넣을지 짐작하지 않는다. * * 한 연결이 여러 사이트를 만들면 트윈도 여럿이다. 본문을 보고 고르게 하면 그 판단이 커넥터마다 * 달라지고, 틀리면 다른 공장의 사실이 이 공장에 들어간다. 주소가 말하게 하면 그 일이 없다. */ router.post('/twin/hook/:source/:instanceId', async (ctx: any) => { const domain = ctx.state?.domain if (!domain) { ctx.status = 400 ctx.body = { ok: false, error: 'no domain context' } return } const outcome = await handleHook({ domainId: domain.id, source: String(ctx.params.source ?? ''), instanceId: String(ctx.params.instanceId ?? ''), headers: (ctx.request?.headers ?? {}) as Record, body: ctx.request?.body, /* * 받은 바이트 그대로 — 서명을 확인하려면 이것이 있어야 한다. `koa-bodyparser` 가 파싱한 본문과 * 함께 남겨 준다. 파싱본을 다시 문자열로 만들면 키 순서와 공백이 달라져 서명이 맞지 않는다. */ rawBody: typeof ctx.request?.rawBody === 'string' ? ctx.request.rawBody : undefined, store: databaseStore(), /* * 폴링이 쓰는 길을 그대로 쓴다 — 검증도 유입도 같은 자리다. 그래서 트윈 쪽에서는 물어서 받은 * 것과 밀어 받은 것이 구별되지 않는다. 구별되는 것은 유입 장부의 `sources` 한 줄뿐이다. */ /* * 유입 몸통은 `live-ingest.ts` 한 곳에 있다 — backfill 이 같은 것을 쓴다. 여기에 두면 두 길이 * 서로 다른 규칙으로 받게 되고, 한쪽만 수정되는 날이 온다. */ ingest: (instanceId, records, lane) => lane === 'FACTS' ? liveIngest(domain.id, instanceId, records, 'hook') : deliverAttention({ domainId: domain.id, source: String(ctx.params.source ?? ''), instanceId, records }), /* * 구멍을 본 그 자리에서 바로 받는다 — 번호가 비었다는 것을 아는 가장 이른 순간이다. * 기다리지 않는다(§`requestFill`): 훅의 응답이 늦으면 보내는 쪽이 되풀이한다. */ onGap: scope => requestFill(domain.id, String(ctx.params.instanceId ?? ''), scope) }) ctx.status = outcome.status ctx.body = outcome.body }) }) /* 나머지 셋은 이 패키지가 쓰지 않는다 — 자리는 남겨 둔다(다른 모듈이 같은 신호를 기다린다). */ process.on('bootstrap-module-global-public-route' as any, (_app: any, _router: any) => {}) process.on('bootstrap-module-global-private-route' as any, (_app: any, _router: any) => {}) process.on('bootstrap-module-domain-private-route' as any, (_app: any, _router: any) => {})