NPM-Paket
Das SDK @surtai/guardian-web führt Guardians collect()-Ablauf in jedem modernen Browser aus. Es erfasst Gerätesignale, verschlüsselt sie lokal und liefert ein undurchsichtiges Payload, das dein Backend an Surts Evaluate-Endpoint sendet. Das SDK macht keine Netzwerkaufrufe und hält keinen API-Schlüssel - mit einer Ausnahme: Wenn du ein optionales geolocationJwt übergibst, führt collect() einen einzelnen Best-Effort-Aufruf GET /geolocation/client-ip aus, um die öffentliche IP des Browsers aufzulösen und sie in das Payload einzubetten. Ohne geolocationJwt macht collect() null Netzwerkaufrufe. Das JWT ist ein kurzlebiges Token, das von deinem Backend erzeugt wird, kein API-Schlüssel - es gibt also weiterhin keinen API-Schlüssel im Browser.
npm install @surtai/guardian-web
Anders als die nativen SDKs (iOS / Android / React Native) stellt das Web-SDK keine verify()-Methode bereit. Alle Risikoentscheidungen erfolgen serverseitig, sobald dein Backend das Payload weiterleitet. Siehe Collect (Server-zu-Server) für den vollständigen Backend-Ablauf.
Geräteerkennung (v0.5.0)
Ab v0.5.0 fügt das SDK dem verschlüsselten Payload automatisch zwei Signale hinzu - ohne Codeänderung auf deiner Seite:
- Stabile Geräteerkennung - das SDK erzeugt und persistiert eine install id pro Browser und
nimmt sie in
collect()auf. Surt nutzt sie, um ein wiederkehrendes Gerät über mehrere Besuche hinweg zu erkennen, sodass derselbe Browser demselben Gerät zugeordnet wird, selbst wenn sein roher Fingerabdruck mit anderen Maschinen desselben Modells kollidiert. - Meldung der SDK-Version - das SDK meldet seine eigene Build-Version, sodass Surt jede Transaktion dem exakten SDK-Release zuordnen kann.
Beide reisen innerhalb des verschlüsselten Payloads - du liest oder setzt sie nie. Ein Upgrade des Pakets ist alles, was nötig ist, um zu profitieren; siehe Migration v0.4 → v0.5.
Browser wie Safari löschen skriptbeschreibbaren Speicher nach ~7 Tagen, was die install id rotiert. Damit die Erkennung dort stabil bleibt, kann dein Backend die id in einem First-Party- Cookie persistieren, das über Preflight weitergereicht wird - eine optionale ~5-zeilige Änderung, die im Migrationsleitfaden behandelt wird.
Verwendung
import { collect } from '@surtai/guardian-web';
// Variant 1: no IP lookup, zero network calls.
const { payload } = await collect({ collectLocation: false });
// Variant 2: resolve the browser's public IP into the payload.
// Your backend mints the short-lived JWT via preflight.
const { payload: payloadWithIp } = await collect({
collectLocation: false,
geolocationJwt: jwt,
});
// Forward the payload to your backend.
await fetch('https://your-api.com/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'user_123', payload }),
});
Dein Backend erzeugt das geolocationJwt, indem es Preflight mit deinem sp_live_*-Schlüssel aufruft - siehe Authentifizierung. Übergib das JWT an collect() nur, wenn die öffentliche IP aufgelöst werden soll; andernfalls lass es weg.
Optionen
| Option | Typ | Erforderlich | Standard | Beschreibung |
|---|---|---|---|---|
collectLocation | boolean | Nein | false | Wenn true, wird die Geolocation-API des Browsers angefordert. Das ist die einzige Stelle in collect(), die einen Berechtigungsdialog auslösen kann. |
geolocationJwt | string | Nein | (keiner) | Wenn angegeben, löst collect() die öffentliche IP des Browsers via GET /geolocation/client-ip auf und bettet sie in das Payload ein. Weglassen, um die IP-Abfrage (und alle Netzwerkaufrufe) zu überspringen. |
CollectResult
interface CollectResult {
/** Base64 payload. Pass as `payload.data` in the evaluate request shown below. */
payload: string;
/** What the SDK observed while collecting - additive, safe to ignore. */
diagnostics: {
location?: 'collected' | 'denied' | 'unavailable' | 'timeout' | 'not_requested';
networkIntel?: 'collected' | 'unavailable' | 'not_requested';
warnings: { code: string; signal: string; detail?: string }[];
};
}
Das diagnostics-Objekt zeigt dir, was auf dem Gerät passiert ist - zum Beispiel, ob der Standort erfasst wurde oder der Nutzer ihn abgelehnt hat - sodass du in deiner UI reagieren kannst:
const { payload, diagnostics } = await collect({ collectLocation: true });
if (diagnostics.location === 'denied') {
// prompt the user to enable location, then retry
}
Siehe Ergebnis-Diagnose für die vollständige Feldreferenz.
Backend: an Surt weiterleiten
Dein Backend empfängt das Payload vom Browser und sendet es an Surts Evaluate-Endpoint:
POST https://api.surt.com/geolocation/transactions/evaluate
Content-Type: application/json
Authorization: Bearer YOUR_SURT_API_KEY
{
"customer_id": "user_123",
"transaction_type": "login",
"transaction_name": "Sign in",
"payload": {
"type": "encrypted",
"data": "<payload from collect()>"
},
"config": {
"response": {
"address": { "type": "include" }
}
}
}
Die Form der Anfrage, die unterstützten transaction_type-Werte, die config-Optionen und das Antwortschema sind in allen Guardian-SDKs identisch. Siehe Collect (Server-zu-Server) für die vollständige Referenz, einschließlich Beispielen in Node / Java / Python.
Fehler
collect() wirft einen einzelnen GuardianError in drei Fällen:
import { collect, GuardianError } from '@surtai/guardian-web';
try {
const { payload } = await collect();
} catch (err) {
if (err instanceof GuardianError) {
switch (err.code) {
case 'CRYPTO_UNAVAILABLE': /* not in a secure context */ break;
case 'ENCRYPTION_FAILED': /* encryption failed (rare) */ break;
case 'INVALID_OPTIONS': /* bad arguments */ break;
}
}
}
| Code | Bedeutung |
|---|---|
CRYPTO_UNAVAILABLE | window.crypto.subtle fehlt. Das SDK benötigt einen sicheren Kontext (HTTPS oder localhost). |
ENCRYPTION_FAILED | Der Verschlüsselungsschritt ist fehlgeschlagen. Wie einen Bug behandeln: erfassen und melden. |
INVALID_OPTIONS | Das Argument von collect() ist kein Objekt. |
Einzelne Collectors (Fingerabdruck, Akku, Geolocation, Netzwerk, etc.) werfen niemals: sie scheitern lautlos. Eine entzogene Berechtigung oder eine nicht unterstützte API bedeutet nur, dass das entsprechende Feld aus dem Payload weggelassen wird. Auch die Abfrage der öffentlichen IP ist Best-Effort: Wenn der Aufruf GET /geolocation/client-ip fehlschlägt oder das JWT abgelehnt wird, wird das IP-Feld einfach weggelassen und collect() gibt trotzdem ein Payload zurück. Das Backend toleriert sparsame Payloads.
Browser-Unterstützung
- Jeder moderne Browser mit
window.crypto.subtle(Chrome, Edge, Firefox, Safari, Opera). - Sicherer Kontext erforderlich: HTTPS in der Produktion,
localhostfür die Entwicklung. Das SDK wirftCRYPTO_UNAVAILABLEaußerhalb eines sicheren Kontexts. - Geolocation, Battery Status, NetworkInformation und andere optionale APIs degradieren elegant, wenn sie nicht verfügbar sind.
Framework-Beispiele
- React
- Next.js
- Vue
- Svelte
- Vanilla JS
import { useState } from 'react';
import { collect } from '@surtai/guardian-web';
export function VerifyButton({ userId }: { userId: string }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
try {
const { payload } = await collect({ collectLocation: false });
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, payload }),
});
} finally {
setLoading(false);
}
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Wird geprüft...' : 'Weiter'}
</button>
);
}
'use client';
import { collect } from '@surtai/guardian-web';
export function SignInForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const { payload } = await collect();
await fetch('/api/sign-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: new FormData(e.currentTarget).get('email'),
payload,
}),
});
// Your /api/sign-in route forwards `payload` to Surt's /evaluate.
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Anmelden</button>
</form>
);
}
<script setup lang="ts">
import { ref } from 'vue';
import { collect } from '@surtai/guardian-web';
const loading = ref(false);
const props = defineProps<{ userId: string }>();
async function verify() {
loading.value = true;
try {
const { payload } = await collect();
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: props.userId, payload }),
});
} finally {
loading.value = false;
}
}
</script>
<template>
<button :disabled="loading" @click="verify">
{{ loading ? 'Wird geprüft...' : 'Weiter' }}
</button>
</template>
<script>
import { collect } from '@surtai/guardian-web';
export let userId;
let loading = false;
async function verify() {
loading = true;
try {
const { payload } = await collect();
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, payload }),
});
} finally {
loading = false;
}
}
</script>
<button on:click={verify} disabled={loading}>
{loading ? 'Wird geprüft...' : 'Weiter'}
</button>
<button id="verify">Weiter</button>
<script type="module">
import { collect } from 'https://esm.sh/@surtai/guardian-web';
document.getElementById('verify').addEventListener('click', async () => {
const { payload } = await collect({ collectLocation: false });
await fetch('/api/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'user_123', payload }),
});
});
</script>
Unterschiede zu den nativen SDKs
@surtai/guardian-web | @surtai/guardian-rn / iOS / Android | |
|---|---|---|
verify()-Methode | Nein | Ja |
collect()-Methode | Ja (einzige API) | Ja |
| App-Level-Initialisierung | Keine | initialize(options) |
| Kunden- / Transaktionskontext | Vom Backend gesetzt | Wird in den Claims des vom Backend erzeugten JWT mitgeführt |
| API-Schlüssel im Client | Nein | Nein (verwendet ein vom Backend erzeugtes JWT, keinen API-Schlüssel) |
| Netzwerkaufrufe vom SDK | Keine (außer wenn geolocationJwt übergeben wird) | Ja |
| Geräteintegritätsprüfung | Nein (kein Browser-Äquivalent) | Ja |
| Persistenter Kundenzustand | Nein (zustandslos pro Aufruf) | Nein |
Web ist bewusst schlank: eine einzige statische Funktion, die ein verschlüsseltes Payload liefert. Kundenbindung, Transaktionsmetadaten und Entscheidungen passieren alle in deinem Backend.