mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 22:01:11 +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
144 lines
4.0 KiB
TypeScript
144 lines
4.0 KiB
TypeScript
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
import { canonicalBytes } from "./canonical.js";
|
|
import { base64url, fromBase64url, hex } from "./encoding.js";
|
|
|
|
export interface SigningKeyPair {
|
|
publicKey: string;
|
|
secretKey: string;
|
|
}
|
|
|
|
export interface EncryptionKeyPair {
|
|
publicKey: string;
|
|
secretKey: string;
|
|
}
|
|
|
|
export interface IdentityKeyPair {
|
|
signing: SigningKeyPair;
|
|
encryption: EncryptionKeyPair;
|
|
}
|
|
|
|
export interface RotationStatement {
|
|
newEd25519Pub: string;
|
|
newX25519Pub: string;
|
|
newEpoch: number;
|
|
}
|
|
|
|
export interface SignedRotation extends RotationStatement {
|
|
signature: string;
|
|
}
|
|
|
|
export function generateIdentity(): IdentityKeyPair {
|
|
const signing = ed25519.keygen();
|
|
const encryption = x25519.keygen();
|
|
return {
|
|
signing: { publicKey: base64url(signing.publicKey), secretKey: base64url(signing.secretKey) },
|
|
encryption: {
|
|
publicKey: base64url(encryption.publicKey),
|
|
secretKey: base64url(encryption.secretKey),
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface DeviceRequestSignatureInput {
|
|
method: string;
|
|
path: string;
|
|
ts: number;
|
|
bodySha256: string;
|
|
}
|
|
|
|
export function signDeviceRequest(
|
|
input: DeviceRequestSignatureInput,
|
|
signingSecretKey: string,
|
|
): string {
|
|
if (
|
|
!/^[A-Z]+$/.test(input.method) ||
|
|
!input.path.startsWith("/") ||
|
|
!Number.isSafeInteger(input.ts) ||
|
|
input.ts < 0 ||
|
|
!/^[0-9a-f]{64}$/.test(input.bodySha256)
|
|
) {
|
|
throw new Error("invalid device request signature input");
|
|
}
|
|
return base64url(ed25519.sign(canonicalBytes(input), fromBase64url(signingSecretKey)));
|
|
}
|
|
|
|
export function fingerprint(ed25519PublicKey: string, x25519PublicKey?: string): string {
|
|
const material = x25519PublicKey
|
|
? canonicalBytes({ ed25519: ed25519PublicKey, x25519: x25519PublicKey })
|
|
: fromBase64url(ed25519PublicKey);
|
|
return hex(sha256(material))
|
|
.match(/.{1,4}/g)!
|
|
.join(" ");
|
|
}
|
|
|
|
export function formatHandleEpoch(handle: string, keyEpoch: number): string {
|
|
if (
|
|
!/^[a-z0-9](?:[a-z0-9_-]{0,62})$/i.test(handle) ||
|
|
!Number.isSafeInteger(keyEpoch) ||
|
|
keyEpoch < 1
|
|
) {
|
|
throw new Error("invalid handle or key epoch");
|
|
}
|
|
return `${handle}#${keyEpoch}`;
|
|
}
|
|
|
|
export function parseHandleEpoch(value: string): { handle: string; keyEpoch: number } {
|
|
const match = /^([a-z0-9](?:[a-z0-9_-]{0,62}))#([1-9][0-9]*)$/i.exec(value);
|
|
if (!match) {
|
|
throw new Error("invalid handle#key_epoch");
|
|
}
|
|
const keyEpoch = Number(match[2]);
|
|
if (!Number.isSafeInteger(keyEpoch)) {
|
|
throw new Error("invalid key epoch");
|
|
}
|
|
return { handle: match[1]!, keyEpoch };
|
|
}
|
|
|
|
export function signRotation(statement: RotationStatement, oldSecretKey: string): SignedRotation {
|
|
validateRotation(statement);
|
|
return {
|
|
...statement,
|
|
signature: base64url(ed25519.sign(rotationBytes(statement), fromBase64url(oldSecretKey))),
|
|
};
|
|
}
|
|
|
|
export function verifyRotation(rotation: SignedRotation, oldPublicKey: string): boolean {
|
|
const { signature, ...statement } = rotation;
|
|
try {
|
|
validateRotation(statement);
|
|
return ed25519.verify(
|
|
fromBase64url(signature),
|
|
rotationBytes(statement),
|
|
fromBase64url(oldPublicKey),
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function rotationBytes(statement: RotationStatement): Uint8Array {
|
|
return canonicalBytes({ ...statement, domain: "reef-rotation-v1" });
|
|
}
|
|
|
|
function validateRotation(statement: RotationStatement): void {
|
|
if (
|
|
statement === null ||
|
|
typeof statement !== "object" ||
|
|
Array.isArray(statement) ||
|
|
Object.keys(statement).length !== 3 ||
|
|
!["newEd25519Pub", "newX25519Pub", "newEpoch"].every((key) => Object.hasOwn(statement, key))
|
|
) {
|
|
throw new Error("invalid rotation statement");
|
|
}
|
|
if (!Number.isSafeInteger(statement.newEpoch) || statement.newEpoch < 1) {
|
|
throw new Error("invalid new epoch");
|
|
}
|
|
if (
|
|
fromBase64url(statement.newEd25519Pub).length !== 32 ||
|
|
fromBase64url(statement.newX25519Pub).length !== 32
|
|
) {
|
|
throw new Error("invalid rotation public key");
|
|
}
|
|
}
|