glassprint · Concepts

Architecture — Archetype B

glassprint is built on what we call Archetype B: the browser collects, the engine identifies. The client SDK (@noidme/glassprint) never computes the visitorId. It reads one signal per granted probe, seals the payload, and POSTs it. The engine — running either embedded in your own backend (the primary model) or as the standalone glassprint-server (the alternative) — decrypts, adds network-tier signals that JavaScript can never see, resolves identity, and returns the authoritative typed result.

Two ways to run the same engine

Both deployment modes implement the same Archetype B pipeline — the same protocol, the same verdict logic, the same privacy guarantees. They differ only in how you integrate:

  • Embedded engine (primary). Add one library dependency to your Node, Go, Python, or Java backend. The engine auto-mounts all glassprint HTTP endpoints and processes identifications in-process. No separate server to deploy or operate. Your backend is the glassprint server.
  • Standalone server (alternative). Run the glassprint-server container (Docker / Helm / Terraform) as a separate service and point your client at it. Use this for polyglot shops or teams that prefer to keep glassprint as an independent tier.

Why the id is engine-generated

Client-only fingerprints are spoofable and decay in weeks — a forked page can assert any value, and a browser update shifts the hash. By moving id generation to the engine (whether embedded or standalone), glassprint can verify untrusted signals, mix in tier signals the page can't reach, and mint a stable pseudonymous id under a key the client never holds.

The request path

  1. Consent gate (fail-closed). The SDK resolves the on-page noidme.js engine. For each registered probe whose purpose is granted and in declared scope, it constructs the probe and reads one signal. No engine on the page → all purposes denied (consent-engine-absent).
  2. Manifest + receipt. The SDK builds a hash-chained probe manifest, a client-asserted consent receipt, a replay nonce, and a client timestamp.
  3. Seal. The plaintext payload is sealed — AES-256-GCM (symmetric) or HPKE/ECDH-P256 (asymmetric, recommended; the client holds no decrypt key) — and POSTed to the ingest endpoint.
  4. Engine identify. The engine (embedded in your backend or in the standalone server) decrypts, captures network-tier signals (JA4 / HTTP2 / TCP) behind a trust boundary, verifies any declared-agent attestation, resolves identity, classifies, ledgers, and returns the typed IngestResponse. When the engine is embedded, the verdict is also available in-process immediately — no round-trip needed.
The hash-chained manifest is honestly scoped: it proves what was transmitted, not what was read in a forked client. A forked client can assert any consent state — the engine verifies transmission integrity, not that consent was truly obtained. We state this limit in the product rather than hide it.

What the visitorId is

The engine-minted id has four properties that the client could never give you:

  • Salted-HMAC. Derived via HKDF-SHA256 from a master secret — never a raw signal hash that leaks device facts.
  • Epoch-rotated. The id key is keyed on (kid, purpose, epoch), so ids rotate on a schedule rather than persisting as a permanent supercookie.
  • Tenant-bound. The customer key id (kid) is mixed into the HKDF info, so the same device yields a tenant-distinct id per customer — there is no cross-customer supercookie, and no global identity graph.
  • Privacy-mode aware. In mode: 'hashed' the engine stores no raw signal values.

The network trust boundary

JA4 / HTTP2 / TCP forwarding headers are honored only when the immediate socket peer IP is in a configured CIDR allowlist (your edge). An untrusted peer has its forwarding headers stripped and is marked untrusted-direct topology. The engine never derives the client IP from X-Forwarded-For. This is what keeps a high-confidence automation verdict from being spoofed from behind a CDN. The trust boundary is configured the same way in both deployment modes.

The shape of a call

identify() — always resolves, never throws
import { createGlassprint } from '@noidme/glassprint';

const gp = createGlassprint({ kid: 'acme', endpoint: 'https://id.acme.com/_fp/i/v1' });

const r = await gp.identify({ tag: 'checkout' });
// The engine is authoritative: r.class, r.visitorId are minted engine-side (embedded or standalone).
if (r.reason === 'consent-denied') { /* no signal was read */ }
else { useVisitor(r.visitorId, r.class, r.confidence); }
The no-argument form has no transport wired, so identify() returns reason: 'transport-error' and visitorId: null. A working integration supplies a transport (for example createSealedTransport) that seals and POSTs the bundle to your ingest endpoint.