Problem
The TypeScript SDK (sdk/typescript/) exists as a documented skeleton but has zero implementation. All four source files contain
only JSDoc comments:
src/frame.ts — no functions implemented
src/transport.ts — no functions implemented
src/models.ts — no types defined
src/firewall.ts — no class implemented
More critically, now that canonical request signing has been implemented in the Python SDK and Go sidecar (#32), the TypeScript SDK must implement the same canonicalization from the start —
otherwise it will hit the exact HMAC mismatch bug that #32 was filed to fix.
The Canonicalization Problem for TypeScript
JavaScript's JSON.stringify does not sort keys by default:
// TypeScript/JavaScript — key order is insertion order
JSON.stringify({"b": 2, "a": 1})
// → '{"b":2,"a":1}' ← NOT sorted
// Python SDK after #32 — sorted keys
json.dumps({"b": 2, "a": 1}, sort_keys=True)
// → '{"a":1,"b":2}' ← sorted
If frame.ts signs with JSON.stringify without sorting, the
TypeScript SDK will produce a different HMAC than the Python SDK
for semantically identical payloads — the Go sidecar will reject
every TypeScript request with HMAC verification failed.
The fix must be built in from day one:
// Correct canonical serialization for TypeScript
function canonicalPayload(payload: Buffer): Buffer {
const parsed = JSON.parse(payload.toString('utf-8'))
const canonical = JSON.stringify(parsed, Object.keys(parsed).sort())
return Buffer.from(canonical, 'utf-8')
}
Note: JSON.stringify with a sorted replacer array handles
top-level key sorting but does NOT recursively sort nested object
keys. A recursive sort is required to match Go and Python behaviour.
What Needs to Be Implemented
src/models.ts
Decision enum: ALLOW = 0x00, SANITISE = 0x01, BLOCK = 0x02
SanitiseResult interface matching Python SanitiseResult
ChunkResult interface matching Python ChunkResult
FirewallError and FirewallConnectionError classes
src/frame.ts
canonicalPayload(payload: Buffer): Buffer — recursive JSON
key sort, compact encoding, rejects invalid JSON
signedMessage(version, length, nonce, payload): Buffer —
identical layout to Python/Go: version(1B) || length(4B BE)
|| nonce(16B) || canonical_payload
encodeRequest(payload: Buffer, key: Buffer): Buffer
decodeRequest(data: Buffer): RequestFrame
encodeResponse(decision, sanitised?): Buffer
decodeResponse(data: Buffer): ResponseFrame
FrameError and CanonicaliseError classes
src/transport.ts
Transport class with async send(payload: Buffer): Promise<ResponseFrame>
- Unix Domain Socket via
net.createConnection
- Exponential backoff retry matching Python SDK (3 attempts,
0.1s/0.2s/0.4s)
FirewallConnectionError after exhausting retries
src/firewall.ts
Firewall class with all four async hook methods
onPrompt(text: string): Promise<Decision | SanitiseResult>
onContext(chunks: string[]): Promise<ChunkResult[]>
onToolCall(name: string, params: Record<string, unknown>): Promise<Decision | SanitiseResult>
onMemory(key: string, value: string, op?: string): Promise<Decision | SanitiseResult>
- HMAC key resolution from
ACF_HMAC_KEY environment variable
Test Requirements
A test suite under sdk/typescript/tests/ using Node's built-in
test runner (already configured in package.json).
Minimum coverage required:
Frame tests (tests/frame.test.ts):
- Magic byte, version, length field, nonce uniqueness
- HMAC validity round-trip
- Cross-SDK interop: fixed nonce + different key order → identical
HMAC (the core canonicalization guarantee)
- Canonical length in signed message vs raw input length
- Unicode normalization:
\u0041 → A (must match Go behaviour)
- Invalid JSON rejected with
CanonicaliseError
- Recursive key sort for nested objects
Model tests (tests/models.test.ts):
Decision enum values match wire protocol bytes
FirewallConnectionError is subclass of FirewallError
Acceptance Criteria
Relates To
Notes on Recursive Key Sorting
The naive JSON.stringify(parsed, Object.keys(parsed).sort())
only sorts top-level keys. Nested objects are not sorted. A
recursive implementation is required:
function sortKeysRecursive(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(sortKeysRecursive)
}
if (value !== null && typeof value === 'object') {
return Object.keys(value as object)
.sort()
.reduce((acc, key) => {
acc[key] = sortKeysRecursive(
(value as Record<string, unknown>)[key]
)
return acc
}, {} as Record<string, unknown>)
}
return value
}
function canonicalPayload(payload: Buffer): Buffer {
let parsed: unknown
try {
parsed = JSON.parse(payload.toString('utf-8'))
} catch (e) {
throw new CanonicaliseError(
`payload is not valid JSON: ${e}`
)
}
const sorted = sortKeysRecursive(parsed)
const canonical = JSON.stringify(sorted)
return Buffer.from(canonical, 'utf-8')
}
This matches the recursive behaviour of Go's encoding/json.Marshal
on map[string]interface{} and Python's json.dumps(sort_keys=True).
Problem
The TypeScript SDK (
sdk/typescript/) exists as a documented skeleton but has zero implementation. All four source files containonly JSDoc comments:
src/frame.ts— no functions implementedsrc/transport.ts— no functions implementedsrc/models.ts— no types definedsrc/firewall.ts— no class implementedMore critically, now that canonical request signing has been implemented in the Python SDK and Go sidecar (#32), the TypeScript SDK must implement the same canonicalization from the start —
otherwise it will hit the exact HMAC mismatch bug that #32 was filed to fix.
The Canonicalization Problem for TypeScript
JavaScript's
JSON.stringifydoes not sort keys by default:If
frame.tssigns withJSON.stringifywithout sorting, theTypeScript SDK will produce a different HMAC than the Python SDK
for semantically identical payloads — the Go sidecar will reject
every TypeScript request with HMAC verification failed.
The fix must be built in from day one:
Note:
JSON.stringifywith a sorted replacer array handlestop-level key sorting but does NOT recursively sort nested object
keys. A recursive sort is required to match Go and Python behaviour.
What Needs to Be Implemented
src/models.tsDecisionenum:ALLOW = 0x00,SANITISE = 0x01,BLOCK = 0x02SanitiseResultinterface matching PythonSanitiseResultChunkResultinterface matching PythonChunkResultFirewallErrorandFirewallConnectionErrorclassessrc/frame.tscanonicalPayload(payload: Buffer): Buffer— recursive JSONkey sort, compact encoding, rejects invalid JSON
signedMessage(version, length, nonce, payload): Buffer—identical layout to Python/Go: version(1B) || length(4B BE)
|| nonce(16B) || canonical_payload
encodeRequest(payload: Buffer, key: Buffer): BufferdecodeRequest(data: Buffer): RequestFrameencodeResponse(decision, sanitised?): BufferdecodeResponse(data: Buffer): ResponseFrameFrameErrorandCanonicaliseErrorclassessrc/transport.tsTransportclass with asyncsend(payload: Buffer): Promise<ResponseFrame>net.createConnection0.1s/0.2s/0.4s)
FirewallConnectionErrorafter exhausting retriessrc/firewall.tsFirewallclass with all four async hook methodsonPrompt(text: string): Promise<Decision | SanitiseResult>onContext(chunks: string[]): Promise<ChunkResult[]>onToolCall(name: string, params: Record<string, unknown>): Promise<Decision | SanitiseResult>onMemory(key: string, value: string, op?: string): Promise<Decision | SanitiseResult>ACF_HMAC_KEYenvironment variableTest Requirements
A test suite under
sdk/typescript/tests/using Node's built-intest runner (already configured in
package.json).Minimum coverage required:
Frame tests (
tests/frame.test.ts):HMAC (the core canonicalization guarantee)
\u0041→A(must match Go behaviour)CanonicaliseErrorModel tests (
tests/models.test.ts):Decisionenum values match wire protocol bytesFirewallConnectionErroris subclass ofFirewallErrorAcceptance Criteria
src/files fully implementedcanonicalPayload()recursively sorts keys at all nesting levelsHMAC signatures for semantically identical JSON
\u0041Unicode escape normalizes toA— matching Go/Pythonnpm test(
crypto,net,buffer)Relates To
this PR must match)
docs/architecture.md— wire protocol specificationNotes on Recursive Key Sorting
The naive
JSON.stringify(parsed, Object.keys(parsed).sort())only sorts top-level keys. Nested objects are not sorted. A
recursive implementation is required:
This matches the recursive behaviour of Go's
encoding/json.Marshalon
map[string]interface{}and Python'sjson.dumps(sort_keys=True).