mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 15:56:10 +00:00
* 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
74 lines
2.2 KiB
TypeScript
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;
|
|
}
|