/** 값이 아니라 **채워져 있었다는 사실**만 나가는 칸들. */ export declare const SECRET_KEYS: readonly ["token", "hookSecret", "intakeSecret", "password", "apiKey"]; export interface PortableConnection { source: string; system?: string; siteName?: string; adapterType: string; /** 비밀값을 뺀 연결 설정. */ config: Record; /** 내보낼 때 채워져 있던 비밀 칸 이름 — 값은 없습니다. */ secretsRequired: string[]; /** 이 연결이 무엇을 세우나. */ scope?: unknown; } export interface ConnectionBundle { kind: 'operato-twin/connections'; version: 1; exportedAt: string; /** 어디서 내보냈나 — 사람이 나중에 「이게 어느 배포 것인가」를 물을 때 답합니다. */ exportedFrom?: { domain?: string; note?: string; }; connections: PortableConnection[]; } /** * 연결 행들을 옮길 수 있는 모양으로 — **비밀값을 빼고**. * * 빼는 판정을 이름으로 합니다. 목록으로만 하면 커넥터가 새 비밀 칸을 더할 때 그 값이 조용히 * 파일로 나갑니다 — 이름 규칙을 함께 두어 **새 칸이 기본으로 안 나가게** 합니다. */ export declare function exportConnections(rows: readonly { source: string; system?: string; siteName?: string; adapterType: string; connectionConfig?: any; scopeSpec?: any; }[], meta: { at: string; domain?: string; note?: string; }): ConnectionBundle; export type ImportAction = { action: 'create'; connection: PortableConnection; missingSecrets: string[]; } | { action: 'update'; connection: PortableConnection; missingSecrets: string[]; keptSecrets: string[]; } | { action: 'skip'; source: string; why: string; }; export interface ImportPlan { actions: ImportAction[]; /** 사람이 연결 화면에서 채워야 하는 것 — 이것이 비어야 「시드만으로 끝」입니다. */ /** * 사람이 환경에 넣어야 하는 비밀값. * * `env` 를 함께 냅니다 — **그 이름을 사람이 손으로 적기 때문**입니다. 화면이 이름을 따로 만들면 * 규칙이 두 곳에 살고, 갈리는 날 「넣었는데 안 읽힌다」가 됩니다. 실제로 났습니다(2026-09-06): * 화면이 `TWIN_SECRET__MES-MIRATEK__HOOKSECRET` 을 알려 줬는데 서버가 읽는 이름은 * `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET` 이었습니다. 하이픈과 낙타 경계 둘 다 달랐습니다. * * `keys` 와 순서가 같습니다 — `keys[i]` 의 이름이 `env[i]` 입니다. */ needsHuman: { source: string; keys: string[]; env: string[]; }[]; /** 들여온 **뒤에** 두 연결이 같은 곳에 조치를 내게 되나 — 있으면 들여오면 안 됩니다. */ collisions: { intake: string; sources: string[]; }[]; errors: string[]; } /** * 들여오기 **계획**을 먼저 냅니다 — 쓰기 전에 무엇이 일어날지 사람이 봅니다. * * ── 이미 있는 연결을 덮지 않습니다 ────────────────────────────────────────── * 같은 이름이 이미 있으면 **선언만 갱신하고 비밀값은 그대로 둡니다.** 사람이 화면에서 넣은 값을 * 파일이 지우면, 파일 하나 들여왔다가 도는 연결이 멈춥니다. 데모 시드도 같은 규율을 적어 두었습니다 * (「옆 설정을 지우지 않는다 — 시드는 자기가 아는 것만 채운다」). * * ── 없는 비밀값을 지어내지 않습니다 ───────────────────────────────────────── * 빈 값으로 채워 두면 연결이 서고 **동기화할 때 처음 실패합니다.** 그때는 무엇이 없어서인지 화면에서 * 멉니다. 여기서 이름을 대고 멈추는 것이 낫습니다. */ export declare function planImport(bundle: unknown, existing: readonly { source: string; connectionConfig?: any; }[], /** 지금 환경이 줄 수 있는 비밀값 이름들 — 값은 안 받습니다(이 함수는 값을 만지지 않습니다). */ availableSecrets?: (source: string, key: string) => boolean): ImportPlan; /** 사람이 읽는 한 줄 — 화면과 로그가 같은 문장을 쓰게. */ export declare function sayPlan(plan: ImportPlan): string; /** * 비밀값이 사는 **환경 변수 이름** — `mes-miratek` · `hookSecret` → `TWIN_SECRET__MES_MIRATEK__HOOK_SECRET`. * * 패키지에는 비밀 칸의 이름만 있고 값은 환경에서 옵니다. 그 이름을 **사람이 손으로 적으므로** 규칙이 * 흔들리면 「넣었는데 안 읽힌다」가 됩니다 — 오늘 종일 잡은 「양쪽 다 초록인데 안 간다」의 설정 쪽 * 버전입니다. 그래서 이 규칙을 한 곳에 두고 시험으로 못 박습니다. * * 낙타 이름의 경계를 밑줄로 가릅니다 — 안 그러면 `HOOKSECRET` 과 `HOOK_SECRET` 이 갈려 한쪽만 읽힙니다. */ export declare function secretEnvName(source: string, key: string): string; /** * **두 연결이 같은 곳에 조치를 내지 않나** — 같으면 다른 공장의 작업지시가 한 공장에 섭니다. * * ── 왜 이 검사가 생겼나 (2026-09-05) ──────────────────────────────────────── * `mes-miratek` 이 서면서 한 커넥터가 처음으로 **공장 둘**을 달았습니다. 조치가 나가는 주소는 * `intakeSiteId` 가 정하는데(`operato-plant.ts:389`), 그 값이 트윈의 사이트 이름을 **이깁니다** — * 트윈에서는 `mes-line-a` 인데 MES 의 도메인은 `system` 이라 그 override 가 필요합니다. * * 그래서 **연결을 베껴 만들고 그 칸만 안 고치면** 두 트윈의 조치가 같은 공장으로 갑니다. 오류가 * 안 납니다 — 저쪽은 정상적인 작업지시로 받고 **실제로 만듭니다.** * * 게이트에 이미 「사이트를 모르면 보내지 않는다」가 있는데(`no-site`), 그 이유가 적혀 있는 문장이 * **「잘못된 공장이 실제로 만든다」** 입니다. 이것은 그 둘째 길입니다 — 사이트를 아는데 **틀린 * 사이트를 압니다.** * * 파일로 연결을 만들 수 있게 되면서 더 쉬워졌습니다: 패키지를 베껴 이름만 바꾸면 받는 자리가 * 그대로 따라옵니다. */ export declare function intakeCollisions(connections: readonly { source: string; adapterType?: string; /** 패키지·계획이 쓰는 이름. */ config?: Record | null; /** **DB 행이 쓰는 이름** — `TwinReference` 의 칸이다. 둘 다 받는다(ADR-0053 ①). */ connectionConfig?: Record | null; }[]): { intake: string; sources: string[]; }[]; /** * **화면 저장의 문** — 이 연결을 이 설정으로 저장하면 다른 연결과 받는 사이트가 겹치나 (ADR-0046 ②-1). * * 들여오기(`planImport`)는 `intakeCollisions` 로 거부하는데 화면 저장(`saveTwinReference`)은 부르지 않아서, * 연결을 베껴 이름만 바꾸면 경고 0 으로 두 트윈의 조치가 한 공장에 섰다. 판정은 같은 함수 하나로 한다. * 경고가 아니라 거부이고, 사람이 그 자리에서 고치도록 **이미 그 사이트를 쓰는 연결의 이름**을 싣는다. */ export declare function refuseIntakeCollision(source: string, config: Record | undefined, others: readonly { source: string; siteName?: string | null; connectionConfig?: Record | null; }[]): { code: 'intake-site-taken'; message: string; params: { site: string; connection: string; }; } | null;