agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
305 lines • 13.6 kB
JavaScript
;
/**
* hosting/webSocketFrames — the frame codec the conversation door speaks, and
* nothing else.
*
* Pure bytes in, pure bytes out: no socket, no session, no handler, no port
* vocabulary. That is what makes it testable the only way protocol code is
* worth testing — against the byte sequences the specification itself
* publishes, rather than against a client we also wrote.
*
* ── Why this file exists instead of a dependency ─────────────────────────────
* The alternative was an optional peer dependency carrying a complete
* implementation. It was rejected for a reason that is about honesty rather
* than about lines of code: `capabilities` is declared at CONSTRUCTION and is
* static thereafter, so a host whose conversation door only works when an
* optional package happens to be installed can only either claim
* `'conversation'` and then refuse to do it — the library declaring a promise
* it cannot keep, which is the one thing the capability union forbids — or
* probe `node_modules` and let feature detection depend on install state, so a
* deployment that forgot the dependency silently gets a host that quietly does
* not do conversations. The shipped adapter honours what it declares, always,
* with nothing to install. That was worth these bytes.
*
* ── What is implemented, exactly ─────────────────────────────────────────────
* The server half of RFC 6455 for a text channel: the handshake, text frames,
* continuation frames, ping/pong, and close. Payloads are unmasked on the way
* in (client frames MUST be masked) and written unmasked on the way out (server
* frames MUST NOT be), which is the asymmetry the RFC requires.
*
* ── What is NOT, and how far the verification goes ───────────────────────────
* No extensions and no compression: `permessage-deflate` is negotiated, and
* this door never negotiates it, so the RSV bits must always be zero and a peer
* that sets one is refused. No binary frames — the port carries text, and
* binary is a capability nobody has minted evidence for. No client role.
*
* Verification is: the byte vectors published in RFC 6455 §5.7, which were
* authored by the specification and not by this repository, plus a live
* exchange against the platform's own WebSocket client where the runtime has
* one, plus the conversation conformance suite over a real socket. It is **not**
* run against the Autobahn test suite, and this file does not claim more
* coverage than the tests beside it actually execute.
*
* Pattern: pure codec. Role: innermost ring — imports nothing from this package.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeText = exports.FrameReader = exports.isControlFrame = exports.decodeClose = exports.encodeClose = exports.encodeText = exports.encodeFrame = exports.handshakeResponse = exports.acceptKey = exports.FrameProtocolError = exports.CLOSE_CODE = exports.OPCODE = void 0;
const node_crypto_1 = require("node:crypto");
/** The frame kinds this door speaks. Numbers, because the wire uses numbers. */
exports.OPCODE = {
continuation: 0x0,
text: 0x1,
binary: 0x2,
close: 0x8,
ping: 0x9,
pong: 0xa,
};
/**
* Close codes this door produces. The RFC's own numbers, named — a bare `1009`
* in a branch is a fact nobody can check without opening the specification.
*/
exports.CLOSE_CODE = {
/** Ordinary end: the work is done. */
normal: 1000,
/** The host is shutting down. */
goingAway: 1001,
/** The peer broke the protocol. */
protocolError: 1002,
/** Data this endpoint cannot accept — a binary frame, here. */
unsupportedData: 1003,
/** Reserved: never sent on the wire, used only as "we were not told". */
noStatus: 1005,
/** Text that is not valid UTF-8. */
invalidPayload: 1007,
/** The endpoint's policy ended it — here: the sign-in behind the socket ended. */
policyViolation: 1008,
/** Past a declared ceiling. */
tooBig: 1009,
/**
* Something went wrong on THIS side — the RFC's own word for a condition
* that stopped this endpoint fulfilling the request. Used where the fault is
* demonstrably not the peer's, so that a close code never blames them for it.
*/
internalError: 1011,
};
/**
* A peer that broke the protocol, carrying the close code the RFC names for
* that breakage — so the caller does not have to re-derive which number means
* what at the point it has to write one.
*/
class FrameProtocolError extends Error {
closeCode;
constructor(closeCode, message) {
super(message);
this.name = 'FrameProtocolError';
this.closeCode = closeCode;
}
}
exports.FrameProtocolError = FrameProtocolError;
/** The magic string RFC 6455 §1.3 appends before hashing. Not ours to change. */
const HANDSHAKE_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
/**
* The `Sec-WebSocket-Accept` value for a client's `Sec-WebSocket-Key`:
* base64(sha1(key + GUID)). Deterministic, and pinned against the RFC's own
* worked example.
*/
function acceptKey(clientKey) {
return (0, node_crypto_1.createHash)('sha1')
.update(clientKey + HANDSHAKE_GUID)
.digest('base64');
}
exports.acceptKey = acceptKey;
/**
* The 101 response, as bytes.
*
* `protocol` is echoed only when the caller selected one. Echoing a subprotocol
* the client did not offer would make the client fail the connection, so
* choosing one is the wire's job (it read the offer) and not this function's.
*/
function handshakeResponse(clientKey, protocol) {
const lines = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptKey(clientKey)}`,
];
if (protocol !== undefined && protocol.length > 0) {
lines.push(`Sec-WebSocket-Protocol: ${protocol}`);
}
return Buffer.from(`${lines.join('\r\n')}\r\n\r\n`, 'utf8');
}
exports.handshakeResponse = handshakeResponse;
/**
* One frame, ready to write. **Never masked**: a server that masks its frames
* is a server the client closes on, per RFC 6455 §5.1.
*/
function encodeFrame(opcode, payload, fin = true) {
const length = payload.length;
let header;
if (length < 126) {
header = Buffer.alloc(2);
header[1] = length;
}
else if (length < 0x10000) {
header = Buffer.alloc(4);
header[1] = 126;
header.writeUInt16BE(length, 2);
}
else {
header = Buffer.alloc(10);
header[1] = 127;
// Two 32-bit halves rather than a BigInt write: the high half is always
// zero for anything this door will ever send, and saying so in bytes is
// clearer than converting through a type nothing else here uses.
header.writeUInt32BE(0, 2);
header.writeUInt32BE(length, 6);
}
header[0] = (fin ? 0x80 : 0x00) | opcode;
return Buffer.concat([header, payload]);
}
exports.encodeFrame = encodeFrame;
/** A text frame carrying one whole message. */
function encodeText(text) {
return encodeFrame(exports.OPCODE.text, Buffer.from(text, 'utf8'));
}
exports.encodeText = encodeText;
/**
* A close frame carrying a code and, when there is one, a reason.
*
* The reason is truncated to 123 bytes because a control frame's payload may
* not exceed 125 and the code takes two of them. Truncating a REASON is safe in
* a way truncating a message never is: it is diagnostic prose about an ending
* that has already been decided.
*/
function encodeClose(code, reason) {
const text = reason === undefined ? Buffer.alloc(0) : Buffer.from(reason, 'utf8');
const trimmed = text.length > 123 ? text.subarray(0, 123) : text;
const payload = Buffer.alloc(2 + trimmed.length);
payload.writeUInt16BE(code, 0);
trimmed.copy(payload, 2);
return encodeFrame(exports.OPCODE.close, payload);
}
exports.encodeClose = encodeClose;
/** Read a close frame's payload. An empty payload is "no status", never an error. */
function decodeClose(payload) {
if (payload.length < 2)
return { code: exports.CLOSE_CODE.noStatus };
const code = payload.readUInt16BE(0);
const reason = payload.length > 2 ? payload.subarray(2).toString('utf8') : undefined;
return { code, ...(reason !== undefined && reason.length > 0 && { reason }) };
}
exports.decodeClose = decodeClose;
/** Is this one of the three control opcodes? */
function isControlFrame(opcode) {
return (opcode & 0x8) !== 0;
}
exports.isControlFrame = isControlFrame;
/**
* Turns a stream of TCP chunks into whole frames.
*
* Stateful by necessity — a frame arrives in as many pieces as the network
* feels like, and two frames arrive in one piece just as often. Every branch
* that gives up does so by throwing a {@link FrameProtocolError} carrying the
* code to close with, so the layer above never has to invent one.
*/
class FrameReader {
buffered = Buffer.alloc(0);
maxFrameBytes;
/**
* @param maxFrameBytes The declared ceiling, when there is one. Checked
* against the length in the HEADER — before the payload is buffered, so an
* announced 4GB frame costs nothing to refuse.
*/
constructor(maxFrameBytes) {
this.maxFrameBytes = maxFrameBytes;
}
/** Feed one chunk; get back every frame that completed. */
push(chunk) {
this.buffered = this.buffered.length === 0 ? chunk : Buffer.concat([this.buffered, chunk]);
const frames = [];
for (;;) {
const frame = this.readOne();
if (frame === undefined)
return frames;
frames.push(frame);
}
}
/** One frame, or `undefined` when the bytes for a whole one are not here yet. */
readOne() {
const buffer = this.buffered;
if (buffer.length < 2)
return undefined;
const first = buffer[0];
const second = buffer[1];
const fin = (first & 0x80) !== 0;
const reserved = first & 0x70;
const opcode = first & 0x0f;
const masked = (second & 0x80) !== 0;
let length = second & 0x7f;
let offset = 2;
if (reserved !== 0) {
// RSV bits mean an extension, and this door negotiates none — so a peer
// that set one is describing a frame we would have to guess at.
throw new FrameProtocolError(exports.CLOSE_CODE.protocolError, 'a reserved bit is set, and this door negotiated no extension that would define it');
}
if (!masked) {
// RFC 6455 §5.1: every client frame is masked, and a server that accepts
// an unmasked one is a server that can be fed frames by a naive proxy.
throw new FrameProtocolError(exports.CLOSE_CODE.protocolError, 'a client frame arrived unmasked, which the protocol does not allow');
}
if (isControlFrame(opcode) && (length > 125 || !fin)) {
throw new FrameProtocolError(exports.CLOSE_CODE.protocolError, 'a control frame was fragmented or longer than 125 bytes');
}
if (length === 126) {
if (buffer.length < offset + 2)
return undefined;
length = buffer.readUInt16BE(offset);
offset += 2;
}
else if (length === 127) {
if (buffer.length < offset + 8)
return undefined;
const high = buffer.readUInt32BE(offset);
const low = buffer.readUInt32BE(offset + 4);
if (high !== 0) {
throw new FrameProtocolError(exports.CLOSE_CODE.tooBig, 'a frame announced a length above 4 GiB, which no ceiling here permits');
}
length = low;
offset += 8;
}
if (this.maxFrameBytes !== undefined && length > this.maxFrameBytes) {
// Refused on the ANNOUNCED length, before a byte of payload is kept: a
// ceiling that only notices after buffering is not a ceiling.
throw new FrameProtocolError(exports.CLOSE_CODE.tooBig, `a frame announced ${length} bytes, past the declared maxFrameBytes of ${this.maxFrameBytes}`);
}
const maskOffset = offset;
offset += 4;
if (buffer.length < offset + length)
return undefined;
const payload = Buffer.allocUnsafe(length);
buffer.copy(payload, 0, offset, offset + length);
for (let i = 0; i < length; i++)
payload[i] ^= buffer[maskOffset + (i & 3)];
this.buffered = buffer.subarray(offset + length);
return { fin, opcode, payload };
}
}
exports.FrameReader = FrameReader;
/**
* Decode a whole message's bytes as UTF-8, refusing anything that is not.
*
* Strict on purpose (RFC 6455 §8.1): a text frame that is not valid UTF-8 is a
* protocol violation, and the lenient alternative silently replaces the bad
* bytes with `U+FFFD` — handing the consumer a string the far side never sent
* and no way to know it happened.
*/
function decodeText(payload) {
try {
return new TextDecoder('utf-8', { fatal: true }).decode(payload);
}
catch {
throw new FrameProtocolError(exports.CLOSE_CODE.invalidPayload, 'a text frame carried bytes that are not valid UTF-8');
}
}
exports.decodeText = decodeText;
//# sourceMappingURL=webSocketFrames.js.map