import { type WebhookResponse } from '@operato/ops-contract'; /** 훅 요청의 결과 — 상태 코드와 본문을 함께 정한다(부르는 쪽이 그대로 답한다). */ export interface HookOutcome { status: number; body: WebhookResponse; } /** * 응답 코드 규약 — **계약이 정하고 여기서는 그것을 쓴다**(2026-08-31). * * 예전에는 이 표가 여기 있었고 보내는 쪽(operato-plant)에 또 있었다. 그래서 같은 409 를 양쪽이 다른 * 뜻으로 썼다 — 여기서는 「그 트윈이 실시간으로 돌지 않는다」, 저쪽에서는 「번호가 비었다」였다. * 붙였다면 트윈이 안 도는 동안 보내는 쪽이 같은 구간을 계속 다시 보냈다. * * 판정표는 만드는 쪽과 읽는 쪽이 합의해야 하는 것이라 계약 층에 있다(`@operato/ops-contract`). * `notLive` 가 409 에서 503 으로 옮겨진 이유도 그 파일에 적혀 있다. * * **여기서 이름을 바꿔 다시 내보내지 않는다.** 부르는 쪽은 `WEBHOOK_STATUS` 를 계약에서 바로 가져온다. * 별명을 두면 같은 표에 이름이 둘이 되고, 다음 사람이 어느 쪽이 정본인지 물어야 한다. */ /** 헤더에서 비밀값을 꺼낸다 — 두 가지 방식을 받는다(밀어 주는 쪽의 관습이 갈린다). */ export declare function secretFromHeaders(headers: Record | undefined): string; /** * 비밀값 비교 — **길이가 다르면 바로 거절하고, 같으면 끝까지 비교한다.** * * 앞에서 다른 것을 발견하고 곧바로 답하면 응답 시간이 달라지고, 그 차이로 비밀값을 한 글자씩 알아낼 수 * 있습니다. 그래서 같은 길이면 전부를 비교합니다. */ export declare function secretMatches(given: string, expected: string): boolean; /** * 인증 — **서명이 왔으면 서명을 보고, 아니면 비밀값을 본다.** * * 두 방식을 두는 이유는 밀어 주는 쪽의 사정이 다르기 때문이다. 이미 붙어 있는 커넥터는 비밀값을 헤더에 * 싣는 방식으로 만들어졌고, 새로 만드는 것(operato-plant)은 서명을 쓴다. 서명 쪽이 낫다 — 비밀값을 그대로 * 싣는 방식은 그 요청을 그대로 다시 보내는 것을 막지 못한다. * * **서명이 왔는데 확인할 수 없으면 거절한다.** 받은 바이트를 들고 있지 않으면(`rawBody` 가 없으면) * 파싱한 객체를 다시 문자열로 만들어 확인하고 싶어지는데, 키 순서와 공백이 달라져 어차피 맞지 않는다. * 맞지 않는 것을 통과시키면 서명이 있으나 마나 한 것이 된다. */ export declare function authorizeHook(args: { headers: Record | undefined; /** 받은 바이트 그대로. 서명을 쓰지 않는 연결에서는 없어도 된다. */ rawBody: string | undefined; /** 연결 설정에 저장된 비밀값. 서명과 비밀값이 같은 값을 쓴다. */ secret: string; nowMs: number; }): { ok: true; method: 'signature' | 'secret'; } | { ok: false; reason: string; }; /** * 커서에 적을 열쇠 — **트윈까지 넣는다.** * * ── 왜 (2026-08-31, MES 레인 질문에서 나옴) ─────────────────────────────── * 커서는 **연결마다** 한 행(`TwinReference.liveCursor`)에 산다. 그런데 연결 하나가 트윈 여럿을 * 만든다(`scopeSpec.produced` 가 목록이다). 보내는 쪽의 단위 이름만으로 열쇠를 잡으면, 같은 연결의 * 두 공장이 같은 이름(`EVENTS`)을 쓸 때 **한 계수기를 함께 쓴다.** * * 그러면 A 공장이 5까지 올린 뒤 B 공장이 1을 보내면 이미 본 번호가 되어 버려지고, A 가 6을 보내면 * B 의 번호와 어긋나 끊임없이 「빠졌다」가 된다. 같은 부류가 이 저장소에서 이미 났다 — 두 공장의 * 같은 번호가 한 자리에 겹쳤다. * * 보내는 쪽에 트윈 이름을 넣으라고 하지 않는다. 어디로 갈지는 주소가 이미 말했고, 그것을 본문에도 * 적으라고 하면 둘이 어긋나는 날 어느 쪽을 믿을지 정해야 한다. */ export declare function pushCursorKey(instanceId: string, scope: string): string; /** * 커서에서 그 단위의 마지막 번호를 꺼낸다. * * 커서 모양은 `{ streams: { [흐름]: { since?, seen[], seq? } } }` 이고 **흐름 이름은 어댑터가 정한다** * (§`TwinReference.liveCursor`). 밀어 주는 연결에서는 그 이름이 곧 번호를 매기는 단위다. * * 자리를 따로 만들지 않는 이유는 「재기동을 넘어 사는 진행 위치」가 두 벌이 되기 때문이다. 폴링이 쓰는 * `since` 와 밀어 주기가 쓰는 `seq` 는 둘 다 「이 흐름을 어디까지 읽었나」이고 원본이 되풀어 줄 수 있다. */ export declare function seqOf(cursor: unknown, scope: string): number | undefined; /** 그 단위의 번호만 바꾼 커서를 만든다 — 다른 흐름과 `since`·`seen` 은 그대로 둔다. */ export declare function withSeq(cursor: unknown, scope: string, seq: number, firstSeq?: number): Record; /** * 이 연결이 이 단위를 **몇 번부터 받기 시작했나** — 적힌 적이 없으면 `undefined`. * * 「앞이 비었나」를 답하려면 이 값이 있어야 한다. 없으면 **모른다고 말해야 한다** — 0 으로 답하면 * 1번부터 다 받은 것으로 읽힌다. */ export declare function firstSeqOf(cursor: unknown, scope: string): number | undefined; /** * **번호대로 받을 수 있는 데까지 자른다** — 밀어 받는 길과 backfill 하는 길이 이 함수 하나를 지난다. * * ── 왜 갈라 두나 (2026-08-31) ──────────────────────────────────────────── * 이 판정이 훅 안에 박혀 있었다. backfill(`fetchSince`)이 같은 판정을 해야 하는데, 거기서 다시 만들면 * **두 길이 다른 규칙으로 받는다** — 한쪽만 고쳐지는 날이 오고, 어긋난 그 길에서만 사실이 사라진다. * * 번호 확인을 커넥터가 아니라 프레임워크로 가져온 것과 같은 이유다. 자리가 하나여야 한다. * * ── 첫 구멍에서 멈춘다 ─────────────────────────────────────────────────── * 구멍 뒤의 봉투를 받아들이면 그 구멍이 영영 메워지지 않는다. 보내는 쪽은 `expectedSeq` 부터 다시 * 보내고, 뒤엣것은 그때 함께 온다. */ export interface SequencedTake { /** 받아들인 레코드 — 구멍 앞까지다. */ records: unknown[]; /** 번호를 확인했나. 번호를 매기지 않는 원본은 거짓이고, 그 사실을 응답에 남긴다. */ numbered: boolean; /** 커서에 적을 번호 — 받은 것이 없으면 없다. */ advancedSeq?: number; /** 구멍이 났다 — 그 앞부터 다시 받아야 한다. */ gap?: { expectedSeq: number; lastSeq: number; }; /** * 이 단위의 **첫 봉투였다** — 그 앞으로 몇 개를 못 봤나. * * ── 왜 이 칸이 생겼나 (2026-09-06 실측) ────────────────────────────────── * plant 아웃박스의 1~20 이 400 으로 거절돼 그 연결에 커서가 없었다. 다음에 온 21번이 첫 봉투로 * 앉으며 **커서가 21 로 뛰었다.** 커서가 0 이었다면 같은 21이 구멍이었다. * * 스무 건이 없는데 구멍으로도 안 세어지고, 경고도 없고, backfill 대상도 아니었다 — backfill 은 커서부터 * 묻는다. 그 사실이 아무 데도 안 남았다. * * **손실이라고 말하지 않는다.** 원본이 오래 돌고 있는데 트윈을 나중에 붙이면 그 아래는 애초에 * 안 받기로 한 것이다. 계약은 둘을 구별할 수 없으므로 수만 낸다. * * 0 이면 1번부터 시작한 것이라 이 칸을 내지 않는다 — 말할 것이 없다. */ firstUnseenBefore?: number; /** * 이 묶음의 첫 봉투가 **이 단위의 첫 봉투였다** — 그 번호. * * `firstUnseenBefore` 와 다르다: 이쪽은 1번부터 시작한 연결에도 있다(값 1). 커서에 적어 두면 * 나중에 「앞이 비었나」를 답할 수 있다. */ firstSeq?: number; /** * 이 묶음에서 **가장 낮게 받아들인 번호** — 하나도 못 받았으면 없다. * * `firstSeq` 와 다르다. `firstSeq` 는 「이 단위의 첫 봉투였다」일 때만 있고, 이쪽은 언제나 있다. * * 밑에서부터 다시 받을 때 이것이 필요하다. `from: 0` 으로 부르면 커서가 0 이므로 seq 1 은 * `next` 판정이고 `firstSeq` 가 안 생긴다 — 그런데 우리는 실제로 1번부터 갖게 된다. 그 사실을 * 적어야 「앞이 비었나」에 답할 수 있다. */ lowestSeq?: number; /** * 받을 수 없는 묶음 — 그 이유. * * 번호가 있는 봉투와 없는 봉투가 섞였거나 번호가 정수가 아니다. 둘 다 보내는 쪽의 결함이고, * **반쯤 확인한 것을 확인했다고 말하지 않는다.** */ refused?: string; } export declare function takeInSequence(args: { items: { record: unknown; seq?: number; at?: string; }[]; scope?: string; /** 이 단위에서 마지막으로 받아들인 번호. 받은 적이 없으면 `undefined`. */ lastSeq?: number; }): SequencedTake; /** 이 연결이 이 트윈을 만들었나 — 남의 트윈에 밀어 넣지 못하게. */ export declare function producedInstance(ref: { scopeSpec?: any; } | null | undefined, instanceId: string): { siteId: string; } | undefined;