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.
Verwendung
import { collect } from '@surtai/guardian-web';
// Variante 1: keine IP-Abfrage, null Netzwerkaufrufe.
const { payload } = await collect({ collectLocation: false });
// Variante 2: öffentliche IP des Browsers in das Payload auflösen.
// Dein Backend erzeugt das kurzlebige JWT via Preflight.
const { payload: payloadWithIp } = await collect({
collectLocation: false,
geolocationJwt: jwt,
});
// Leite das Payload an dein Backend weiter.
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. Übergib es als `payload.data` in der unten gezeigten Evaluate-Anfrage. */
payload: string;
/** Was das SDK während der Erfassung beobachtet hat — additiv, kann ignoriert werden. */
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') {
// den Nutzer auffordern, den Standort zu aktivieren, dann erneut versuchen
}
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 von 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': /* nicht in einem sicheren Kontext */ break;
case 'ENCRYPTION_FAILED': /* Verschlüsselung fehlgeschlagen (selten) */ break;
case 'INVALID_OPTIONS': /* ungültige Argumente */ 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,
}),
});
// Deine /api/sign-in-Route leitet `payload` an Surts /evaluate weiter.
}
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.