Fingerprint Pro Internals
Guide

Wire format

The POST body is a framed, compressed, byte-encoded JSON object. Each layer is the agent's own code, and tools/lib/codec.mjs calls those same functions rather than reimplementing them, so a frame buil

The POST body is a framed, compressed, byte-encoded JSON object. Each layer is the agent's own code, and tools/lib/codec.mjs calls those same functions rather than reimplementing them, so a frame built in Node is byte-for-byte what the browser sends.

The codec is JSON over bytes#

encodeJsonBytes and decodeJsonBytes look like a custom binary format: the writer emits bare type-tag bytes and the reader dispatches on them. The tags are ASCII. 34 is ", 44 is ,, 58 is :, 91 and 93 are the brackets, 123 and 125 the braces, plus the literal byte strings null, true and false. The encoder is JSON.stringify writing into a growable Uint8Array and the decoder is a recursive-descent JSON parser reading one.

Two details are not incidental. The encoder skips properties a predicate rejects rather than relying on undefined, and the decoder defines object properties with Object.defineProperty instead of assigning them, so a __proto__ key or a poisoned setter in a response cannot execute. The same codec decodes the encrypted string tables, so one implementation serves both directions.

Compression#

A body over 1024 bytes goes through CompressionStream("deflate-raw"). The frame tag records which of the two happened; nothing else in the body changes.

Frame#

sealFrame(bytes, marker, padMax, keyLength) produces:

[R] [marker[0]+R] [marker[1]+R] [padLen+R] [padLen random bytes] [key(keyLength)] [payload ^ key]

R is one random byte, every sum is mod 256, padLen is a fresh random value in 0..padMax, and the payload is XORed against the key repeating over its length. The key is in the frame. This is framing, not encryption: it defeats a naive body-content filter and nothing more.

The four parameters are bindings rather than literals in the shipped file, so findFrameProfiles reads them off the call sites:

markerpad maxkey lengthwhat it frames
3,1439telemetry POST, deflate-raw compressed
3,1339telemetry POST, uncompressed
3,737the request-log entry kept in localStorage under <prefix>lr

The agent only ever builds a frame, so the inverse is written here. node tools/codec.mjs --open <file> unseals one, and npm run replay -- --dry builds a frame and reopens it locally to prove the round trip.

Envelope#

buildEnvelope assembles the object. Every key that is not an sN signal:

15 rows
keybuilt from
cthe public API key
tthe tag passed to get()
lidlinkedId
m, linstall-method marker, {m: "l", l: "jsl/4.0.0"} on the loader path
mothe keys of the registered modules
s56the blob the server issued on the GET leg
sc{u}, the agent's own script URL, read out of a thrown Error's fileName, sourceURL or stack
uhURL hashing flag
iiintegration info
gtconstant 1
abA/B selections
hufast ? 0 : eventId ? 1 : undefined
rithe previous event id
epvthe ex module's build marker, 3b947bb
lrthe retry queue's stored request log

Ten of these appear across the three captures: c, m, l, mo, sc, ii, gt, ab, epv, lr. The others are absent when their value is undefined, which on a first collection is most of them. s56 is present too, but the schema counts it with the signals because that is where it sits on the wire.

The signal ids enter through a spread that writes one wire key per collected component, unwrapping each result to its value or to a flattened error. reference/envelope.md is the generated version of this table, and reference/schema.md types every key and every addressable leaf inside it.

s56 is worth separating from the rest: it is not a measurement. The agent fetches it over the GET leg and replays it, and what it does to identity is in 06-identity.

Request path#

The path is derived, not random. buildRequestPath cuts a token into /-joined chunks whose lengths come from bytes of a seed, 4 to 7 characters each, so the same token always produces the same path. That is what makes a replay reach the same endpoint as the browser did.

A capture's own sequence is in reference/endpoints.md. In the Safari capture the tenant answered nothing useful, so the agent's retry queue posted the same frame seven times, which is the retry behaviour visible on the wire.

Diagnostics stream#

Alongside the signals the agent ships a numbered event stream: per-getCall and per-collectCall correlation ids, per-stage timestamps (ls, le, ca, ss, se, sd, gs, ge, cs, ce, fa, ia, vs), visibility-state history, and failed-attempt records. Together with the per-signal duration this makes the timing of collection itself a channel the server receives. Whether the server scores it is not observable from the client.