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:
| Module | Exports |
|---|---|
schemas.ts | PROTOCOL_VERSION, every request/response/frame schema (metaResponse, registerDeviceRequest, authTokenRequest, storePutRequest, createChannelRequest, ownerInFrame, peerInFrame, …), id schemas, envelopeSchema, errorBody, wireErrorCode, DEFAULT_LIMITS, paths |
crypto.ts | generateDeviceIdentity, 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.ts | buildShareLink, buildAppShareLink, parseShareLink, parseKeyFragment, scrubShareLinks, SHARE_LINK_SCRUB_RE, CHANNEL_ID_RE, APP_SCHEME |
pow.ts | solvePow, verifyPow, powHash, powPrefix, leadingZeroBits, DEFAULT_POW_DIFFICULTY (16), MAX_POW_DIFFICULTY (28), POW_CONTEXT |
auth.ts | signAuthChallenge, verifyAuthChallenge, authChallengeBytes, normaliseAudience, AUTH_CONTEXT, AUTH_MAX_SKEW_MS (5 min), AUTH_NONCE_BYTES (16), TOKEN_TTL_S (600) |
bytes.ts | toB64url, 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.
Providers and self-hosting
How to pick the official instance, a self-hosted reference server or any conforming URL, what a provider must implement, and how to run and configure the reference server.
Meta
Provider discovery. The host reads it when the user connects a provider and checks `protocol`, `services` and `limits`.