cloud-protocol 包
@pku-harness/cloud-protocol:App、服务商和查看页共用的 zod 线上 schema、路径构造函数、信封加密、分享链接工具、工作量证明和 token 挑战。
@pku-harness/cloud-protocol 是云服务协议的公共实现,包含线上数据的 schema、接口路径、加密、分享链接、工作量证明和 token 挑战。如果你在编写云服务的服务器,或者为插件编写查看页,就会用到它。
这个包用纯 TypeScript 编写,可以在 Hermes(React Native 的 JavaScript 引擎)、Bun 和浏览器里运行。App、参考服务器和查看页引用的是同一份代码,所以三者的线上格式和加密方式始终一致。它依赖 @noble/ciphers、@noble/curves 和 @noble/hashes(锁定版本),并把 zod ^4 作为 peer 依赖。插件一般不需要引用它,因为宿主会替插件完成云服务相关的工作。
它位于 pku-harness-plugins 仓库的 packages/cloud-protocol 目录,没有发布到 npm。你可以在仓库检出里直接使用它(workspace 包名为 @pku-harness/cloud-protocol),也可以把 src/ 目录复制到你的服务器或页面项目中。
模块
下表列出各模块的导出:
| 模块 | 导出 |
|---|---|
schemas.ts | PROTOCOL_VERSION,所有请求、响应和帧的 schema(metaResponse、registerDeviceRequest、authTokenRequest、storePutRequest、createChannelRequest、ownerInFrame、peerInFrame……),各类 id 的 schema,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 分钟)、AUTH_NONCE_BYTES(16)、TOKEN_TTL_S(600) |
bytes.ts | toB64url、fromB64url、isB64url、utf8、fromUtf8、concatBytes、equalBytes、toHex |
线上数据遵循以下约定:二进制数据使用不带填充的 base64url 编码;时间默认是 Unix 毫秒,字段名以 S 结尾的则是秒;解析时会丢弃未知的键。
示例:查看页加入通道并解密
下面的代码运行在查看页里。它从 URL 读出通道 id 和密钥,加入通道,然后解密收到的每一帧。
import {
deriveShareKeys,
envelopeSchema,
fromB64url,
generateBoxKeyPair,
joinResponse,
open,
parseKeyFragment,
paths,
peerInFrame,
channelInfoResponse,
toB64url,
viewerPeerKey,
} from '@pku-harness/cloud-protocol';
// 访问 GET /s/<id> 之后,页面位于 /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); // 机主加密过的 meta(使用前务必校验)
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)));
};参考服务器会把 /s/<id> 重定向到 /w/<plugin>/<page>?c=<id>。浏览器在重定向后会自动保留 #k= 这段 URL 片段,而片段不会发给服务器,所以服务器始终看不到密钥。
有些应用内置的浏览器不支持 WebSocket。这时改用 GET /v1/c/{id}/events?t=(服务器推送事件,Server-Sent Events,SSE)接收数据,用 POST /v1/c/{id}/send 发送数据。完整的查看页可以参考 plugins/card-share/web,它的界面移植自 pkubdcard。
示例:服务器校验 token 请求
下面的代码运行在服务器上。设备用签名挑战换取 token 时,服务器先校验签名和时间,再检查 nonce 是否被用过,最后签发 token。
import { authTokenRequest, fromB64url, verifyAuthChallenge } from '@pku-harness/cloud-protocol';
const body = authTokenRequest.parse(await req.json());
const device = await db.device(body.deviceId); // 注册时保存的 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');
// 签发 token,声明为 { sub: deviceId, plg: plugin, svc: services, iat, exp: iat + 600_000 }全部接口见 API 参考。