Build a Companion

A Companion is a phone (or any device) that pairs with a running Surface session and talks to its workspace. The headless client does everything protocol-shaped; you provide crypto, transports, and UI.

npm install @schwaizer/ocp-companion

1. Provide a DeviceKey

The pairing model binds tokens to a P-256 device key (proof of possession). The client never touches crypto directly — you inject it:

import type { DeviceKey } from '@schwaizer/ocp-companion';

const deviceKey: DeviceKey = {
  publicJwk: async () => ({ kty: 'EC', crv: 'P-256', x, y }),
  signCompactJws: async (payload) => /* ES256 compact JWS by the PRIVATE key */,
  sha256Base64url: async (input) => /* base64url(sha256(input)) */,
};

On React Native, @noble/curves + a keystore-backed scalar works today; hardware-backed keys (Secure Enclave / StrongBox) slot in behind the same interface. In node, WebCrypto or jose is enough.

2. Pair

The Surface shows a QR encoding ocp://pair?d=<JWS>. Decode the deeplink, validate the issuer against YOUR allowlist, then claim:

import { OcpCompanionClient } from '@schwaizer/ocp-companion';

const client = new OcpCompanionClient({ deviceKey });
client.onStatus = (s) => render(s);        // claiming → waiting_approval → …
client.onTurnEvent = (e) => transcript(e); // input/delta/citation/done/error

const res = await client.pair({ token, iss }, { name: 'My Phone', platform: 'ios' });

pair() resolves only after a human approves on the Surface — that approval is mandatory in the protocol, not a UI nicety. Denials are uniform.

3. Connect and talk

if (res.ok) await client.connect();  // device-key PoP → session.linked
const turnId = client.ask('what changed this week?');
// deltas/citations stream to onTurnEvent; a new ask() barge-ins the old turn

The client owns heartbeats (10 s), silent token refresh at 2/3 TTL (refreshNow() on app foreground), and one automatic resume per drop inside the Hub's 60 s grace window. Deliberate close() never resumes.

4. What your UI must handle

  • waiting_approval (tell the user to look at the Surface screen)
  • denied — uniform; never speculate why
  • suspended → auto-resume → linked again (show a reconnecting state)
  • ui.request confirms if you render them — voice "yes" can answer a browser-initiated confirm and vice versa