@noidme/glassprint
v0.1.0-beta.0
Published
Consent-native device identification — a fingerprint you can see through. Fail-closed by construction: collects nothing for a declined purpose. Client-collect, server-identify (Archetype B).
Maintainers
Readme
@noidme/glassprint
A fingerprint you can see through. · Identification that asks first.
Consent-native device identification: fraud-grade device identity that cannot read a single signal without passing a fail-closed consent engine (noidme.js). Per EDPB Guidelines 02/2023 (Art 5(3) ePrivacy), fingerprinting requires consent like cookies — glassprint is the only device-identity layer built that way from the first line.
The honest guarantee. We don't call this "bulletproof." Where a guarantee is a client-integrity property, or degrades behind a CDN, or is statistically population-dependent, we say so — in the product and in this README.
Install
npm i @noidme/glassprintESM and CommonJS builds plus TypeScript types ship in the package.
Architecture (Archetype B: client-collect → server-identify)
The visitorId is server-generated, never client-computed — client-only fingerprints are spoofable and decay in weeks. The agent collects signals under consent, seals them, and POSTs to the ingest endpoint, which adds network-tier signals (JA4) invisible to JS, resolves identity, and returns the authoritative typed result.
Quickstart
import { createGlassprint, createSealedTransport } from '@noidme/glassprint';
// 1. A transport MUST be wired. It seals the collected payload and delivers it to
// your ingest endpoint, which returns the authoritative result. Without it,
// identify() resolves with reason:'transport-error' and visitorId:null — the
// agent NEVER fabricates a client-side id.
const transport = createSealedTransport({
key, // per-customer AES-256-GCM CryptoKey (selected by kid)
deliver: async (sealed, config) => {
const res = await fetch(config.endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(sealed),
});
return res.json(); // shape: IngestResponse
},
});
// 2. Construct the agent.
const gp = createGlassprint(
{ kid: 'acme', endpoint: 'https://id.acme.com/_fp/i/v1' },
transport,
);
// 3. Identify. identify() ALWAYS resolves — it never throws.
const r = await gp.identify({ tag: 'checkout' });
if (r.reason === 'consent-denied') {
// No signal was ever read.
} else if (r.reason === 'transport-error') {
// No transport wired (or the network hop failed) → r.visitorId is null.
} else {
useVisitor(r.visitorId); // server-generated pseudonymous id (or null)
r.class; // 'human' | 'declared_agent' | 'undeclared_automation' | 'unknown'
r.suspectScore; // additive composite risk score (with security-fraud consent)
}Without a transport
createGlassprint(config) with no second argument is valid, but every identify() then resolves with reason: 'transport-error' and visitorId: null. This is deliberate: identity is server-authoritative, so a client with no path to the ingest endpoint produces no id rather than a spoofable client-side guess. Wire createSealedTransport to get real results.
Invariants
- Fail-closed read gate — before any canvas/WebGL/audio/sensor read, the purpose is checked against noidme.js. Declined ⇒ no Worker, no probe object. No engine present ⇒ all purposes denied.
identify()resolves, never throws — denied consent / absent engine / no signals / transport error all map to a typedreason(consent-denied|consent-engine-absent|no-signals|transport-error|ok).- Server-generated id only — with no transport wired,
visitorIdisnull; the agent never fabricates a client-side id. - Privacy by default — salted pseudonymous per-origin ids, no raw-signal exfiltration, no global identity graph.
- Three classes, not one id —
human/declared_agent/undeclared_automation/unknown(below the confidence floor we refuse to label).
License
MIT
