UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

104 lines (93 loc) 4.45 kB
/** * # cli/keypair — the registered-agent Ed25519 keypair + request-JWT minter * (kestrel-markets-0t0 SLICE 2, PLAT-ADR-0021). * * The client half of the AAP keypair rung. `kestrel register --keypair` generates an * Ed25519 keypair, enrolls the PUBLIC half with the platform, and stores the PRIVATE * half locally (0600, cli/credentials.ts). Thereafter the CLI authenticates by minting * a SHORT-LIVED (≤5 min), single-use JWT it signs with the private key — a possession * proof DISTINCT from replaying the durable bearer capability. The private key NEVER * leaves the machine; only freshly-minted, minutes-long assertions cross the wire, so a * captured request is worthless in minutes and useless on replay (the platform refuses a * reused `jti`). * * Dependency-light on purpose: WebCrypto (`globalThis.crypto.subtle`, node ≥18 / bun) + * node's `Buffer` for base64url — no third-party crypto, matching the platform's * native-Ed25519 rule. The JWT wire format mirrors the platform verifier (agents/jwt.ts): * header {alg:"EdDSA",typ:"JWT"}, claims { sub, aud, iat, exp, jti }. */ /** An Ed25519 public JWK (OKP) — the half enrolled with the platform. */ export interface Ed25519PublicJwk { readonly kty: "OKP"; readonly crv: "Ed25519"; readonly x: string; } /** An Ed25519 private JWK (OKP) — held locally (0600), never sent. */ export interface Ed25519PrivateJwk extends Ed25519PublicJwk { readonly d: string; } const ED25519 = { name: "Ed25519" } as const; function subtle(): SubtleCrypto { const c = (globalThis as { crypto?: Crypto }).crypto; if (!c?.subtle) { throw new Error("WebCrypto (crypto.subtle) unavailable — need node ≥18 or bun to use --keypair / JWT auth"); } return c.subtle; } function b64url(bytes: Uint8Array): string { return Buffer.from(bytes).toString("base64url"); } /** Minimal structural view of an exported JWK (the OSS tsconfig lib omits the global type). */ type JwkExport = { x?: string; d?: string }; /** Generate a fresh Ed25519 keypair as JWKs (public + private halves). */ export async function generateAgentKeypair(): Promise<{ publicJwk: Ed25519PublicJwk; privateJwk: Ed25519PrivateJwk }> { const kp = (await subtle().generateKey(ED25519, true, ["sign", "verify"])) as CryptoKeyPair; const priv = (await subtle().exportKey("jwk", kp.privateKey)) as JwkExport; const pub = (await subtle().exportKey("jwk", kp.publicKey)) as JwkExport; return { publicJwk: { kty: "OKP", crv: "Ed25519", x: pub.x! }, privateJwk: { kty: "OKP", crv: "Ed25519", x: priv.x!, d: priv.d! }, }; } /** The claims a request JWT carries (times in SECONDS per RFC 7519). */ export interface RequestJwtClaims { readonly sub: string; readonly aud: string; readonly iat: number; readonly exp: number; readonly jti: string; } export interface MintRequestJwtInput { readonly privateJwk: Ed25519PrivateJwk; /** The agent's subject (the platform looks the enrolled key up by it). */ readonly subject: string; /** The audience the platform expects (stored from the enrollment response). */ readonly audience: string; /** TTL in seconds (≤ 300; the platform refuses a longer span). Default 120. */ readonly ttlSec?: number; /** Current time (epoch ms). Default `Date.now()`. */ readonly now?: number; } /** * Mint a fresh, short-lived, single-use request JWT signed with the agent's private key. * Each call yields a UNIQUE `jti` (via `crypto.randomUUID`) and a fresh `iat`/`exp`, so * successive requests never collide with the platform's replay guard. */ export async function mintRequestJwt(input: MintRequestJwtInput): Promise<string> { const nowMs = input.now ?? Date.now(); const iat = Math.floor(nowMs / 1000); const ttl = Math.min(Math.max(input.ttlSec ?? 120, 1), 300); const claims: RequestJwtClaims = { sub: input.subject, aud: input.audience, iat, exp: iat + ttl, jti: `jwt_${(globalThis.crypto as Crypto).randomUUID().replace(/-/g, "")}`, }; const key = await subtle().importKey("jwk", { ...input.privateJwk }, ED25519, false, ["sign"]); const headerB64 = b64url(Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "JWT" }))); const payloadB64 = b64url(Buffer.from(JSON.stringify(claims))); const signingInput = `${headerB64}.${payloadB64}`; const sig = await subtle().sign(ED25519, key, Buffer.from(signingInput)); return `${signingInput}.${b64url(new Uint8Array(sig))}`; }