Files
openclaw/extensions/reef/protocol/friendcode.ts
Peter Steinberger 882b2fe900 feat(channels): bundle Reef guarded claw-to-claw channel (#106232)
* feat(channels): bundle Reef guarded claw-to-claw channel

Moves the Reef channel extension from openclaw/reef into the bundled extensions tree with the wire protocol vendored under extensions/reef/protocol. Includes channelConfigs manifest metadata, runtime status reporting via setStatus/buildAccountSnapshot, abort-aware inbox shutdown, monotonic device-auth timestamps, and immutable-model guard admission (dated snapshots plus documented gpt-5.6 ids).

* fix(reef): pin zod exactly per dependency pin guard

* style(reef): satisfy extension lint gates

Curly braces in vendored protocol, explicit type re-exports, typed catch callbacks, Object.assign over map-spread, abort-aware loop restructure.

* fix(reef): CI gates — knip workspace, boundary tsconfig, facade imports, cycle break, contract baselines

- knip: reef workspace project includes vendored protocol/ (owns noble deps)
- tsconfig: conform to canonical extension package-boundary include/exclude; add @openclaw/plugin-sdk devDependency
- imports: channel-inbound/channel-outbound facades instead of deprecated subpaths
- protocol: home ReplayStore contract in envelope.ts to break the envelope<->replay type cycle
- baselines: reef in unguarded runtime-api list; regenerate bundled channel config metadata

* feat(reef): plugin catalog cover art, channel label, changelog revert

* fix(reef): drop unused error-class exports, register lint suppression

* fix(reef): knip entry surface for vendored protocol, explicit WebSocketLike export
2026-07-13 05:17:44 -07:00

74 lines
2.2 KiB
TypeScript

import { hmac } from "@noble/hashes/hmac.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { randomBytes } from "@noble/hashes/utils.js";
import { base64url, equalBytes, fromBase64url } from "./encoding.js";
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
export interface FriendCode {
code: string;
expiry: number;
nonce: string;
}
export function mintFriendCode(
deviceSecret: Uint8Array,
options: { expiry: number; nonce?: Uint8Array; rng?: (length: number) => Uint8Array },
): FriendCode {
if (deviceSecret.length < 32) {
throw new Error("invalid friend code input");
}
const nonce = options.nonce ?? (options.rng ?? randomBytes)(16);
if (!Number.isSafeInteger(options.expiry) || options.expiry < 0 || nonce.length < 8) {
throw new Error("invalid friend code input");
}
const digest = friendCodeDigest(deviceSecret, options.expiry, nonce);
return { code: crockford40(digest), expiry: options.expiry, nonce: base64url(nonce) };
}
export function verifyFriendCode(
code: FriendCode,
deviceSecret: Uint8Array,
options: { now?: number; clockSkewSeconds?: number } = {},
): boolean {
if (deviceSecret.length < 32) {
return false;
}
try {
const now = options.now ?? Math.floor(Date.now() / 1000);
if (code.expiry + (options.clockSkewSeconds ?? 0) < now) {
return false;
}
const expected = mintFriendCode(deviceSecret, {
expiry: code.expiry,
nonce: fromBase64url(code.nonce),
});
return equalBytes(
new TextEncoder().encode(expected.code),
new TextEncoder().encode(code.code.toUpperCase()),
);
} catch {
return false;
}
}
function friendCodeDigest(secret: Uint8Array, expiry: number, nonce: Uint8Array): Uint8Array {
const input = new Uint8Array(8 + nonce.length);
new DataView(input.buffer).setBigUint64(0, BigInt(expiry), false);
input.set(nonce, 8);
return hmac(sha256, secret, input);
}
function crockford40(bytes: Uint8Array): string {
let value = 0n;
for (const byte of bytes.slice(0, 5)) {
value = (value << 8n) | BigInt(byte);
}
let output = "";
for (let index = 0; index < 8; index++) {
output = CROCKFORD[Number(value & 31n)]! + output;
value >>= 5n;
}
return output;
}