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.
164 lines (147 loc) • 7.78 kB
text/typescript
/**
* # crypto/sha256 — the one portable, synchronous sha256 the runtime hashes with (kestrel-alw.17)
*
* Content-addressing is woven through the whole grade path as **pure, synchronous** functions:
* {@link ../grade/receipt.ts createReceipt}, {@link ../blotter/project.ts project}, the {@link ../ledger
* ledger}'s `deriveRunId`, and the sim driver's bus hash all take a sha256 of a canonical string and must
* return it inline — no `await`. Bun's `Bun.CryptoHasher` gives exactly that (a synchronous `.digest`), but
* it is a **Bun-only** global: it does not exist in a Cloudflare Worker isolate, which is where the grade
* path must also run (ADR-0006, EPIC kestrel-markets-alw). WebCrypto (`crypto.subtle.digest`) IS present in
* a Worker but is **async**, and threading `await` through every content-address would break the
* pure-function doctrine the determinism check relies on (RUNTIME §0).
*
* The resolution of that sync-vs-async tension is this module: ONE synchronous `sha256(text)` that is
* portable across environments. It uses Bun's native hasher when the `Bun` global is present (the fast path
* under `bun test` / the CLI) and a self-contained pure-JS SHA-256 otherwise (the Worker isolate). Both code
* paths produce **byte-identical** output — a determinism guarantee locked down by a parity test that runs
* BOTH implementations over the same inputs ({@link sha256Pure} is exported so the Worker path is directly
* testable even under Bun). No wall clock, no RNG.
*/
/** The narrow shape of Bun's native `CryptoHasher` this module uses: a fluent `update` (returns the
* hasher so `.update(…).digest(…)` chains) and a hex `digest`. */
interface BunCryptoHasher {
update(input: string): BunCryptoHasher;
digest(enc: "hex"): string;
}
/** Bun's native `CryptoHasher` when the `Bun` global exists (fast path); `undefined` in a Worker isolate. */
const BUN_CRYPTO_HASHER: undefined | (new (algo: string) => BunCryptoHasher) =
(globalThis as { Bun?: { CryptoHasher?: new (algo: string) => BunCryptoHasher } }).Bun?.CryptoHasher;
// ─────────────────────────────────────────────────────────────────────────────
// Pure-JS SHA-256 (the Worker-portable path)
// ─────────────────────────────────────────────────────────────────────────────
/** The SHA-256 round constants (first 32 bits of the fractional parts of the cube roots of the first 64
* primes) — FIPS 180-4. */
const K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);
/** 32-bit right rotate. */
function rotr(x: number, n: number): number {
return (x >>> n) | (x << (32 - n));
}
/**
* Pure-JS SHA-256 over the UTF-8 bytes of `text` → a 64-char lowercase hex digest (FIPS 180-4). This is the
* Worker-portable path (no `Bun` global, no async `crypto.subtle`). Exported so the parity test can exercise
* it directly even under Bun, proving it matches the native hasher byte-for-byte.
*/
export function sha256Pure(text: string): string {
const msg = new TextEncoder().encode(text);
const bitLen = msg.length * 8;
// Pad: append 0x80, then zeros up to 56 mod 64, then the 64-bit big-endian bit length.
const withPad = new Uint8Array(((msg.length + 8) >> 6) * 64 + 64);
withPad.set(msg);
withPad[msg.length] = 0x80;
// 64-bit length; JS bit ops are 32-bit, so write the two 32-bit halves.
const hi = Math.floor(bitLen / 0x100000000);
const lo = bitLen >>> 0;
const lenOff = withPad.length - 8;
withPad[lenOff] = (hi >>> 24) & 0xff;
withPad[lenOff + 1] = (hi >>> 16) & 0xff;
withPad[lenOff + 2] = (hi >>> 8) & 0xff;
withPad[lenOff + 3] = hi & 0xff;
withPad[lenOff + 4] = (lo >>> 24) & 0xff;
withPad[lenOff + 5] = (lo >>> 16) & 0xff;
withPad[lenOff + 6] = (lo >>> 8) & 0xff;
withPad[lenOff + 7] = lo & 0xff;
// Initial hash values (first 32 bits of the fractional parts of the square roots of the first 8 primes).
let h0 = 0x6a09e667;
let h1 = 0xbb67ae85;
let h2 = 0x3c6ef372;
let h3 = 0xa54ff53a;
let h4 = 0x510e527f;
let h5 = 0x9b05688c;
let h6 = 0x1f83d9ab;
let h7 = 0x5be0cd19;
const w = new Uint32Array(64);
for (let off = 0; off < withPad.length; off += 64) {
for (let i = 0; i < 16; i++) {
const j = off + i * 4;
w[i] = ((withPad[j]! << 24) | (withPad[j + 1]! << 16) | (withPad[j + 2]! << 8) | withPad[j + 3]!) >>> 0;
}
for (let i = 16; i < 64; i++) {
const s0 = rotr(w[i - 15]!, 7) ^ rotr(w[i - 15]!, 18) ^ (w[i - 15]! >>> 3);
const s1 = rotr(w[i - 2]!, 17) ^ rotr(w[i - 2]!, 19) ^ (w[i - 2]! >>> 10);
w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
}
let a = h0;
let b = h1;
let c = h2;
let d = h3;
let e = h4;
let f = h5;
let g = h6;
let h = h7;
for (let i = 0; i < 64; i++) {
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
const ch = (e & f) ^ (~e & g);
const t1 = (h + S1 + ch + K[i]! + w[i]!) >>> 0;
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const t2 = (S0 + maj) >>> 0;
h = g;
g = f;
f = e;
e = (d + t1) >>> 0;
d = c;
c = b;
b = a;
a = (t1 + t2) >>> 0;
}
h0 = (h0 + a) >>> 0;
h1 = (h1 + b) >>> 0;
h2 = (h2 + c) >>> 0;
h3 = (h3 + d) >>> 0;
h4 = (h4 + e) >>> 0;
h5 = (h5 + f) >>> 0;
h6 = (h6 + g) >>> 0;
h7 = (h7 + h) >>> 0;
}
return (
hex32(h0) + hex32(h1) + hex32(h2) + hex32(h3) + hex32(h4) + hex32(h5) + hex32(h6) + hex32(h7)
);
}
/** A 32-bit word as 8 lowercase hex chars. */
function hex32(x: number): string {
return (x >>> 0).toString(16).padStart(8, "0");
}
// ─────────────────────────────────────────────────────────────────────────────
// The one portable sha256 the runtime hashes with
// ─────────────────────────────────────────────────────────────────────────────
/**
* The portable, synchronous sha256 of a UTF-8 string → 64-char lowercase hex. Uses Bun's native
* `CryptoHasher` when the `Bun` global is present (fast path under `bun test` / the CLI) and the pure-JS
* {@link sha256Pure} otherwise (a Cloudflare Worker isolate). Both paths are byte-identical (parity test).
* Deterministic — no wall clock, no RNG.
*/
export function sha256(text: string): string {
if (BUN_CRYPTO_HASHER !== undefined) {
return new BUN_CRYPTO_HASHER("sha256").update(text).digest("hex");
}
return sha256Pure(text);
}