
얇은 TypeScript + 제로 의존성 Python 클라이언트와 레시피를 통해 페이로드 바인딩된 패스키 승인으로 고위험 작업을 게이트합니다.
가볍고, 제로 의존성인 TypeScript 클라이언트 for Cosignet — 사람-인-더-루프 승인 방식으로 고위험 AI 에이전트 행동에 대해, 페이로드에 바인딩된 패스키 서명을 제공합니다.
위험한 작업이 실행되기 전에 사람을 루프에 넣으세요: 작업을 일시 중지하고, 특정 사람으로부터 명시적인 패스키 승인(Face ID / Touch ID / Windows Hello / 보안 키)을 받은 다음, 정확한 페이로드에 바인딩된 서명된 결정이 있을 때만 계속 진행합니다. — 이후에 작업을 변경하면 서명이 더 이상 일치하지 않습니다. Cosignet은 승인 및 증명 계층이며, 실행기나 정책 엔진이 아닙니다. 전역 fetch + Web Crypto를 지원하는 모든 환경에서 작동합니다: Node 18+, Cloudflare Workers, Deno, 브라우저.
상태: 얼리 액세스. npm에
@cosignet/sdk로 게시되었습니다.
npm install @cosignet/sdk
import { Cosignet } from '@cosignet/sdk';
const cosignet = new Cosignet({ apiKey: process.env.COSIGNET_API_KEY! });
const decision = await cosignet.requestApproval(
{
username: 'alex',
action: 'Wire transfer to vendor',
payload: { to: 'acct_8821', amount_usd: 4200, memo: 'INV-2025-118' },
notify: 'telegram_or_email',
},
{ onCreated: (c) => console.log('Approve here:', c.url) },
);
if (decision.status === 'approved') {
// 계속 진행 — decision.rawAssertion은 서명된 증명입니다
} else {
// 'rejected' | 'expired' | 'pending' (타임아웃)
}
requestApproval은 자체 아웃바운드 연결(~25초 단위)을 통해 롱 폴링을 수행하므로, CLI 도구 및 NAT/방화벽 뒤의 잠긴 VPC에서도 작동합니다 — 인바운드 웹훅, 개방형 포트, 공개 IP가 필요하지 않습니다.
시스템이 서명된 승인을 얻을 수 없으면, 게이트된 작업을 실행해서는 안 됩니다. 사용 불가능이 자동 승인으로 이어져서는 안 됩니다. 모든 타임아웃, 네트워크 오류, 5xx를 "승인되지 않음"으로 처리하세요.
구현 참고:
전체 가용성 및 비상 절차 가이드는 Cosignet을 사용할 수 없을 때를 참조하세요.
const created = await cosignet.createConfirmation({ username, action, payload });
const status = await cosignet.getConfirmation(created.id, { wait: 25 }); // long-poll
idempotencyKey를 전달하면 (Idempotency-Key 헤더로 전송됨) — 예를 들어 네트워크 끊김으로 롱 폴링이 중단된 후 재시도할 때 — 동일한 키와 동일한 action/payload로 원래 확인(idempotent: true)을 반환하고, 중복을 생성하거나 승인자에게 다시 알리지 않습니다. 다른 매개변수로 키를 재사용하면 422로 거부됩니다.
await cosignet.createConfirmation({
username: 'alex',
action: 'Wire transfer to vendor',
payload: { to: 'acct_8821', amount_usd: 4200 },
idempotencyKey: 'wire-INV-2025-118', // 논리적 작업당 안정적
});
returnUrl을 전달하면 요청이 해결된(승인/거부/만료) 후 승인 페이지에 "<호스트>로 돌아가기" 버튼이 표시됩니다 — 이는 사람이 타임아웃된 작업을 다시 트리거하기 위해 당신의 앱으로 돌아갈 수 있게 해 줍니다. https URL이어야 하며, 클릭스루 링크로만 렌더링됩니다(서버 측에서 가져오지 않음).
await cosignet.requestApproval({
username: 'alex',
action: 'Wire transfer to vendor',
payload: { to: 'acct_8821', amount_usd: 4200 },
returnUrl: 'https://app.example.com/approvals',
});
notify ('none' | 'telegram' | 'email' | 'telegram_or_email')는 특정 서명자에 대한 개인 연락 핑을 제어합니다:
telegram — 승인자의 연결된 Telegram으로 DM (telegram_or_email은 승인자가 채팅을 연결했을 때 이를 선호합니다).email — 승인자 자신의 주소로 이메일. 우선순위: 대시보드에서 승인자에 대해 설정한 직접 이메일(전송 전용), 그 다음 확인된 회원 이메일, 마지막 수단으로 계정 연락처 주소.telegram_or_email — Telegram이 연결되어 있으면 Telegram, 그렇지 않으면 위의 이메일.none — 개인 핑 없음.별도로, 팀 연락은 공유 채널로 보내는 Slack 브로드캐스트이며, 대시보드에서 계정별로 구성됩니다. 이는 추가적이며 구성되면 항상 발송됩니다, notify와 무관합니다(따라서 notify:'none'도 Slack에 게시됩니다). 승인자는 대시보드의 승인자 섹션에서 Telegram과 이메일을 설정합니다.
public: true공개에 대한 참고: 투명성 로그에 커밋된 이메일은 계정의 지정된 확인된 이메일(대시보드 설정에서 선택한 책임 당사자 — 개별 서명자가 아님)입니다. 직접 알림 이메일은 전송 전용이며 공개 해시에 절대 사용되지 않습니다.
모든 채널은 링크 전용입니다: 알림에는 승인 URL만 포함되며, action 또는 payload는 절대 포함되지 않습니다. 승인은 항상 확인 페이지에서 승인자의 페이로드 바인딩 패스키가 필요합니다 — 채널 내에서 승인하는 방식은 없습니다.
verifyWebhookSignature는 원시 요청 본문의 16진 HMAC-SHA256을 재계산하고, 이를 Cosignet-Signature 헤더와 상수 시간 비교합니다. 받은 정확한 바이트를 전달하세요 — JSON을 다시 직렬화하면 서명이 변경됩니다.
import { verifyWebhookSignature } from '@cosignet/sdk';
const ok = await verifyWebhookSignature({
body: rawBody, // raw string, not re-parsed JSON
signature: req.headers['cosignet-signature'],
secret: process.env.COSIGNET_WEBHOOK_SECRET!,
timestamp: req.headers['cosignet-timestamp'], // optional replay protection
toleranceSeconds: 300, // optional
});
if (!ok) return res.status(401).end();
호출자 측 체크리스트는 docs/verification.md를 참조하세요: approved인 경우에만 계속 진행하고, 결정을 실행하려는 작업과 비교하며, 거부/만료/타임아웃을 하드 스탑으로 처리하세요.
new Cosignet({ apiKey, baseUrl?, fetch? })createConfirmation(input) → CreatedConfirmationgetConfirmation(id, { wait? }) → ConfirmationrequestApproval(input, { timeoutMs?, onCreated? }) → ConfirmationverifyWebhookSignature({ body, signature, secret, timestamp?, toleranceSeconds? }) → Promise<boolean>기본적으로 투명성 로그는 페이로드를 숨기고 승인자 신원을 노출하지 않습니다. 개별 승인을 공개하려면 createConfirmation/requestApproval에서 public: true를 설정하세요: 원시 action + payload가 검증 번들에서 읽을 수 있게 되고, 계정의 지정된 확인된 이메일(대시보드 설정에서 선택한 책임 당사자 — 개별 서명자가 아님)의 PBKDF2 해시가 Merkle 리프에 커밋되어, 누구나 후보 주소를 이에 대해 확인할 수 있습니다.
await cosignet.requestApproval({
username: 'alice',
action: 'Publish Q3 board resolution',
payload: { docId: 'res-2026-Q3' },
public: true, // permanent, irreversible — see caveats below
});
examples/ — 복사-붙여넣기 가능한 cURL, Node, Python, GitLab CI, MCP/도구-래퍼, Worker 예제.security/ — 페이로드 바인딩, WebAuthn 증명 모델, 위협 모델, 투명성 로그, 책임 있는 공개.패스키 승인으로 고위험 작업 게이트:
recipes/ai-agent-gate — AI 코딩 에이전트(Claude Code)가 위험한 셸 명령을 실행하기 전에 사람이 공동 서명할 때까지 일시 중지.recipes/cli-approval — 모든 명령(예: terraform destroy)을 래핑.recipes/ci-cd — 승인 시 파이프라인(GitHub Actions / GitLab CI) 차단.recipes/backend-service — 서비스에서 되돌릴 수 없는 작업을 일시 중지.[email protected]으로 이메일; 취약점에 대해 공개 이슈를 제출하지 마세요.[email protected].MIT © Cosignet
| 신호 | 의미 | 당신의 조치 |
|---|
status: approved (서명 검증됨) | 사람이 이 정확한 페이로드를 승인함 | 진행 |
status: rejected | 사람이 거부함 | 실행하지 말고 요청자에게 전달 |
status: expired | 아무도 제시간에 결정하지 않음 | 실행하지 말고 여전히 필요하면 다시 요청 |
| Timeout, network error, 5xx | 알 수 없는 상태 | 실행하지 말고 백오프로 재시도; N번 실패 후 알림 |