PKU harness
Cloud

cloud-protocol package

@pku-harness/cloud-protocol: the zod wire schemas, path builders, envelope crypto, share-link helpers, proof of work and token challenge that the app, providers and viewer pages share.

@pku-harness/cloud-protocol is the shared implementation of the cloud protocol: wire schemas, endpoint paths, encryption, share links, proof of work and the token challenge. You use it when you write a cloud server, or a viewer page for a plugin.

The package is pure TypeScript and runs on Hermes (the JavaScript engine of React Native), Bun and in browsers. The app, the reference server and viewer pages import the same code, so their wire format and encryption always match. It depends on @noble/ciphers, @noble/curves and @noble/hashes (pinned versions), with zod ^4 as a peer dependency. Plugins normally don't import it, because the host does the cloud work for them.

It lives in the packages/cloud-protocol folder of the pku-harness-plugins repository and isn't published to npm. Use it from a checkout (the workspace package is named @pku-harness/cloud-protocol), or copy the src/ folder into your server or page project.

Modules

The table lists what each module exports:

ModuleExports
schemas.tsPROTOCOL_VERSION, every request/response/frame schema (metaResponse, registerDeviceRequest, authTokenRequest, storePutRequest, createChannelRequest, ownerInFrame, peerInFrame, …), id schemas, envelopeSchema, errorBody, wireErrorCode, DEFAULT_LIMITS, paths
crypto.tsgenerateDeviceIdentity, generateSignKeyPair, generateBoxKeyPair, signPublicKey, boxPublicKey, sign, verify, seal / open, sealBytes / openBytes, EnvelopeError, hkdfSha256, sha256, generateShareSecret, deriveShareKeys, joinHash, ownerPeerKey, viewerPeerKey, derivePeerKey, KDF_INFO, KEY_BYTES, NONCE_BYTES, SIG_BYTES
link.tsbuildShareLink, buildAppShareLink, parseShareLink, parseKeyFragment, scrubShareLinks, SHARE_LINK_SCRUB_RE, CHANNEL_ID_RE, APP_SCHEME
pow.tssolvePow, verifyPow, powHash, powPrefix, leadingZeroBits, DEFAULT_POW_DIFFICULTY (16), MAX_POW_DIFFICULTY (28), POW_CONTEXT
auth.tssignAuthChallenge, verifyAuthChallenge, authChallengeBytes, normaliseAudience, AUTH_CONTEXT, AUTH_MAX_SKEW_MS (5 min), AUTH_NONCE_BYTES (16), TOKEN_TTL_S (600)
bytes.tstoB64url, fromB64url, isB64url, utf8, fromUtf8, concatBytes, equalBytes, toHex

Wire data follows these conventions. Binary data is base64url without padding. Times are Unix milliseconds, except fields whose name ends in S, which are seconds. Parsing strips unknown keys.

Example: a viewer page joins a channel and decrypts

This code runs in a viewer page. It reads the channel id and the secret from the URL, joins the channel, and decrypts each frame it receives.

import {
  deriveShareKeys,
  envelopeSchema,
  fromB64url,
  generateBoxKeyPair,
  joinResponse,
  open,
  parseKeyFragment,
  paths,
  peerInFrame,
  channelInfoResponse,
  toB64url,
  viewerPeerKey,
} from '@pku-harness/cloud-protocol';

// after GET /s/<id> the page is at /w/<plugin>/<page>?c=<id>#k=<secret>
const channelId = new URLSearchParams(location.search).get('c')!;
const k = parseKeyFragment(location.hash);
if (!k) throw new Error('link has no key');
const { authKey, encKey } = deriveShareKeys(k);

const info = channelInfoResponse.parse(await (await fetch(paths.publicChannel(channelId))).json());
const meta = open(encKey, info.meta);          // the owner's sealed meta (validate it!)

const me = generateBoxKeyPair();
const joined = joinResponse.parse(
  await (
    await fetch(paths.join(channelId), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ authKey: toB64url(authKey), peerPub: toB64url(me.publicKey) }),
    })
  ).json(),
);

const key = viewerPeerKey(me.secretKey, me.publicKey, fromB64url(info.chanPub), encKey);
const ws = new WebSocket(new URL(paths.peerWs(channelId, joined.peerToken), location.href).href.replace(/^http/, 'ws'));
ws.onmessage = (ev) => {
  const frame = peerInFrame.parse(JSON.parse(ev.data));
  if (frame.t === 'msg') console.log(open(key, envelopeSchema.parse(frame.env)));
};

The reference server redirects /s/<id> to /w/<plugin>/<page>?c=<id>. The browser keeps the #k= URL fragment across the redirect by itself. Fragments are never sent to the server, so the server never sees the secret.

Some in-app browsers don't support WebSocket. They receive data with GET /v1/c/{id}/events?t= (Server-Sent Events, SSE) and send with POST /v1/c/{id}/send instead. For a complete viewer page, see plugins/card-share/web, whose UI is ported from pkubdcard.

Example: a server verifies a token request

This code runs on a server. When a device signs a challenge to get a token, the server checks the signature and the time, checks that the nonce hasn't been used, and then issues the token.

import { authTokenRequest, fromB64url, verifyAuthChallenge } from '@pku-harness/cloud-protocol';

const body = authTokenRequest.parse(await req.json());
const device = await db.device(body.deviceId);           // stored signPub
const check = verifyAuthChallenge(body, fromB64url(device.signPub), { aud: 'https://cloud.example.org' });
if (!check.ok) return error(401, 'unauthorized', check.reason);
if (await nonces.seen(body.nonce)) return error(401, 'unauthorized', 'replay');
// issue a token with claims { sub: deviceId, plg: plugin, svc: services, iat, exp: iat + 600_000 }

The API reference lists every endpoint.

On this page