import { contractError } from './errors' import type { Limitation } from './capabilities' import type { PublicOperationOptions } from './operations' import type { BoundedAsyncStream } from './streams' export type SecurityBondState = 'bonded' | 'not-bonded' | 'bonding' | 'unknown' | 'unsupported' export type SecurityEncryptionState = 'encrypted' | 'not-encrypted' | 'unknown' | 'unsupported' export type SecurityAuthenticationState = 'authenticated' | 'unauthenticated' | 'unknown' | 'unsupported' export type SecureConnectionsState = 'yes' | 'no' | 'unknown' | 'unsupported' export interface PeerSecurityState { readonly bond: SecurityBondState readonly encryption: SecurityEncryptionState readonly authentication: SecurityAuthenticationState readonly secureConnections: SecureConnectionsState readonly pairingPossible: boolean | null readonly measuredAtMonotonicMs: number readonly limitations: readonly Limitation[] } export interface PeerSecurityEvent { readonly kind: 'state' readonly peerId: string readonly sequence: number readonly state: PeerSecurityState } export type SecurityPairingChallenge = | { readonly kind: 'confirm' readonly peerId: string readonly challengeId: string readonly deadlineMonotonicMs: number } | { readonly kind: 'confirm-passkey' readonly peerId: string readonly challengeId: string readonly passkey: number readonly deadlineMonotonicMs: number } | { readonly kind: 'display-passkey' readonly peerId: string readonly challengeId: string readonly passkey: number readonly deadlineMonotonicMs: number } | { readonly kind: 'provide-pin' readonly peerId: string readonly challengeId: string readonly deadlineMonotonicMs: number } | { readonly kind: 'provide-passkey' readonly peerId: string readonly challengeId: string readonly deadlineMonotonicMs: number } export type SecurityPairingResponse = | { readonly kind: 'confirm'; readonly confirmed: boolean } | { readonly kind: 'confirm-passkey'; readonly confirmed: boolean } | { readonly kind: 'display-passkey'; readonly acknowledged: boolean } | { readonly kind: 'provide-pin'; readonly pin: string } | { readonly kind: 'provide-passkey'; readonly passkey: string } export interface SecurityPairingAgent { onChallenge(challenge: SecurityPairingChallenge): Promise } export type SecurityPairingCeremony = 'system' | { readonly kind: 'agent'; readonly agent: SecurityPairingAgent } export interface SecurityPairOptions extends PublicOperationOptions { readonly transport: 'le' | 'auto' readonly protection: 'system-default' | 'encrypted' | 'authenticated' readonly ceremony: SecurityPairingCeremony /** * Preferred LE pairing generation. 'prefer' (default) leaves the choice to * the platform; 'require' insists on Secure Connections; 'disallow' requests * LE Legacy pairing for peers that reject Secure Connections. Backends that * cannot honour the request return capability.unsupported. */ readonly secureConnections?: 'require' | 'prefer' | 'disallow' } export type SecurityPairResult = | { readonly outcome: 'paired'; readonly state: PeerSecurityState } | { readonly outcome: 'already-paired'; readonly state: PeerSecurityState } | { readonly outcome: 'repaired'; readonly state: PeerSecurityState } | { readonly outcome: 'rejected'; readonly reason: string | null } | { readonly outcome: 'cancelled' } /** * What a cancellation actually achieved, not what it requested. * * A cancellation can lose the race, and it can also arrive at something that * was never going to bond. Each of those is a different fact and gets its own * word, mirroring `SecurityPairResult` so that a word means the same thing in * both types: * * - `'cancelled'` your cancellation stopped it * - `'not-pairing'` there was nothing to stop * - `'paired'` the bond completed before your cancellation arrived * - `'rejected'` the peer refused it; nobody cancelled anything * * `'paired'` is deliberately not `'already-paired'`, which in `SecurityPairResult` * means the peer was bonded BEFORE the call. A pairing that FAILS is not a * fourth outcome: `cancelPairing()` rejects with the same error the pairing * rejected with, because inventing a resolved outcome for a failure would be * the same substitution this type exists to prevent. */ export type SecurityCancelPairingResult = | { readonly outcome: 'cancelled' } | { readonly outcome: 'not-pairing' } | { readonly outcome: 'paired' } | { readonly outcome: 'rejected'; readonly reason: string | null } export type SecurityUnpairResult = { readonly outcome: 'unpaired' | 'already-unpaired' | 'unsupported' } /** * The cancellation outcome implied by the pairing's OWN result. * * Every backend answers `cancelPairing()` by asking the in-flight pairing what * happened to it, rather than forming a second, independent opinion: two * observations of one fact can disagree, and that disagreement is the defect - * `pair()` reporting `paired` while `cancelPairing()` reported `cancelled` for * the same operation. Reading the pairing's own answer makes them incapable of * disagreeing, and keeps the four backends saying one thing. */ export function cancelOutcomeForPairResult(result: SecurityPairResult): SecurityCancelPairingResult { switch (result.outcome) { // One fact - a bond exists because of this operation - so one word. The // re-pairing distinction stays on `pair()` for callers who need it. case 'paired': case 'already-paired': case 'repaired': return { outcome: 'paired' } // The peer refused. Nobody cancelled anything, and saying otherwise would // claim credit for stopping something that stopped itself. case 'rejected': return { outcome: 'rejected', reason: result.reason } case 'cancelled': return { outcome: 'cancelled' } default: return unreachableCancelPairOutcome(result) } } function unreachableCancelPairOutcome(_result: never): never { // Unreachable for a compliant backend. Typing `_result` as `never` keeps a // new SecurityPairResult variant a compile error; types erase, so a // third-party invented outcome still throws instead of returning `undefined` // and a raw TypeError several frames away. throw contractError('protocol.violation', 'core', 'security.cancel-pairing.outcome') } export interface SecurityBackend { state(peerId: string, options: PublicOperationOptions): Promise watch(peerId: string): BoundedAsyncStream pair(peerId: string, options: SecurityPairOptions): Promise cancelPairing(peerId: string, options: PublicOperationOptions): Promise unpair(peerId: string, options: PublicOperationOptions): Promise close?(): void }