Skip to content

Conversation

@ebonura-fastly
Copy link
Contributor

@ebonura-fastly ebonura-fastly commented Dec 8, 2025

Summary

Adds three new properties to the ClientInfo object to bring the JavaScript SDK to parity with the Rust and Go SDKs for client fingerprinting capabilities:

  • tlsJA4 - JA4 TLS fingerprint (human-readable string format)
  • h2Fingerprint - HTTP/2 fingerprint for HTTP/2 connections
  • ohFingerprint - Original Header fingerprint based on header order/presence

Implementation

The implementation follows the existing pattern established by tlsJA3MD5 and other ClientInfo properties, spanning four layers:

1. WASM Host Import (runtime/fastly/host-api/fastly.h)

Added declarations for the three new host calls:

WASM_IMPORT("fastly_http_req", "downstream_tls_ja4")
WASM_IMPORT("fastly_http_req", "downstream_client_h2_fingerprint")
WASM_IMPORT("fastly_http_req", "downstream_client_oh_fingerprint")

2. C++ Host API Wrapper (runtime/fastly/host-api/host_api_fastly.h + host_api.cpp)

Added wrapper functions that handle buffer allocation, error conversion, and optional-none cases:

static Result<std::optional<HostString>> http_req_downstream_tls_ja4();
static Result<std::optional<HostString>> http_req_downstream_client_h2_fingerprint();
static Result<std::optional<HostString>> http_req_downstream_client_oh_fingerprint();

3. JavaScript Bindings (runtime/fastly/builtins/fetch-event.h + fetch-event.cpp)

  • Added slots to ClientInfo::Slots enum: JA4, H2Fingerprint, OHFingerprint
  • Added getter methods: tls_ja4_get, h2_fingerprint_get, oh_fingerprint_get
  • Added helper functions for lazy initialization
  • Registered properties in ClientInfo::properties[]

4. TypeScript Types (types/globals.d.ts)

Added type declarations to the ClientInfo interface:

readonly tlsJA4: string | null;
readonly h2Fingerprint: string | null;
readonly ohFingerprint: string | null;

Files Changed

File Changes
runtime/fastly/host-api/fastly.h WASM import declarations
runtime/fastly/host-api/host_api_fastly.h C++ wrapper method declarations
runtime/fastly/host-api/host_api.cpp C++ wrapper implementations
runtime/fastly/builtins/fetch-event.h JS binding declarations and slots
runtime/fastly/builtins/fetch-event.cpp JS getter implementations and property registration
types/globals.d.ts TypeScript type definitions
CHANGELOG.md Added unreleased section

Testing

Local Build

Built the js-compute-runtime from source:

cd js-compute-runtime/runtime/fastly
./build-release.sh

This compiles the C++ runtime (StarlingMonkey/SpiderMonkey) and outputs fastly.wasm to the repo root (js-compute-runtime/fastly.wasm).

Local Test Service

Created a separate test Compute service (js-test/) that links to the local build:

Directory structure:

mss-js-logging/
├── js-compute-runtime/    # Cloned repo with our changes
│   ├── fastly.wasm        # Built runtime
│   ├── package.json       # Defines @fastly/js-compute package
│   └── runtime/fastly/    # C++ source code
└── js-test/               # Test Compute service
    ├── package.json       # Links to local runtime
    ├── src/index.js       # Test code
    └── bin/main.wasm      # Compiled output

js-test/package.json - Uses file: protocol to link to local build:

{
  "dependencies": {
    "@fastly/js-compute": "file:../js-compute-runtime"
  },
  "scripts": {
    "build": "js-compute-runtime src/index.js bin/main.wasm"
  }
}

When npm install runs, it symlinks node_modules/@fastly/js-compute../js-compute-runtime, so the test service uses our modified runtime.

js-test/src/index.js:

addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));

async function handleRequest(event) {
  const client = event.client;
  return new Response(JSON.stringify({
    clientIP: client.address,
    tlsJA3MD5: client.tlsJA3MD5,
    tlsJA4: client.tlsJA4,
    h2Fingerprint: client.h2Fingerprint,
    ohFingerprint: client.ohFingerprint,
    tlsProtocol: client.tlsProtocol,
    tlsCipher: client.tlsCipherOpensslName,
  }, null, 2), {
    headers: { "Content-Type": "application/json" }
  });
}

Build and Run Locally

cd js-test
npm install                  # Symlinks to local js-compute-runtime
npm run build                # Compiles src/index.js → bin/main.wasm using local runtime
fastly compute serve         # Runs locally with Viceroy
curl http://127.0.0.1:7676/  # Test the endpoint

Local results:

  • All properties exist on the ClientInfo object
  • Properties return null locally (expected - Viceroy doesn't do TLS termination)

Edge Deployment

cd js-test
fastly compute publish       # Packages bin/main.wasm and deploys to Fastly edge

Deployed to Fastly edge and verified all fingerprints return real values:

Test endpoint: https://certainly-well-stingray.edgecompute.app/

Sample response:

{
  "clientIP": "xxx.xxx.xxx.xxx",
  "tlsJA3MD5": "cd08e31494f9531f560d64c695473da9",
  "tlsJA4": "t13d1517h2_8daaf6152771_b0da82dd1658",
  "h2Fingerprint": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
  "ohFingerprint": "RkRGRjAwMDEwMDAw...",
  "tlsProtocol": "TLSv1.3",
  "tlsCipher": "TLS_AES_128_GCM_SHA256"
}

Integration Tests

Added integration tests for the three new properties in integration-tests/js-compute/fixtures/app/src/client.js:

Route Test
/client/tlsJA4 Returns null locally, string on edge
/client/h2Fingerprint Returns null locally, null or string on edge (HTTP/1.1 connections won't have h2 fingerprint)
/client/ohFingerprint Returns null locally, string on edge

Tests follow the existing pattern for other ClientInfo TLS properties (tlsJA3MD5, tlsProtocol, etc.).

Run locally:

npm run test:integration -- --local "/client/"
Results:
✔ GET /client/tlsJA3MD5
✔ GET /client/tlsClientHello
✔ GET /client/tlsClientCertificate
✔ GET /client/tlsCipherOpensslName
✔ GET /client/tlsProtocol
✔ GET /client/tlsJA4
✔ GET /client/h2Fingerprint
✔ GET /client/ohFingerprint

Checklist

  • Follows existing code patterns (modeled after tlsJA3MD5)
  • TypeScript types updated
  • CHANGELOG.md updated
  • Built and tested locally
  • Verified on Fastly edge deployment
  • Integration tests added

…gerprint)

Add three new properties to ClientInfo for client fingerprinting:
- tlsJA4: JA4 TLS fingerprint (human-readable string format)
- h2Fingerprint: HTTP/2 fingerprint
- ohFingerprint: Original Header fingerprint

These properties provide parity with the Rust and Go Compute SDKs.
Add tests for tlsJA4, h2Fingerprint, and ohFingerprint properties
to ensure they return null locally and string on edge.
Document tlsJA4, h2Fingerprint, and ohFingerprint properties
in FetchEvent.mdx.
Maintainers will handle versioning on merge.
@TartanLlama TartanLlama enabled auto-merge (squash) December 9, 2025 19:09
@TartanLlama TartanLlama merged commit 9390e8c into main Dec 9, 2025
26 of 29 checks passed
@TartanLlama TartanLlama deleted the MCSOC-3074 branch December 9, 2025 19:21
This was referenced Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants