Paquete NPM
El SDK @surtai/guardian-web ejecuta el flujo collect() de Guardian en cualquier navegador moderno. Recopila señales del dispositivo, las cifra localmente y devuelve un payload opaco que tu backend envía al endpoint de evaluación de Surt. El SDK no realiza llamadas de red ni contiene ninguna clave de API - con una excepción: cuando pasas un geolocationJwt opcional, collect() realiza una única llamada de mejor esfuerzo GET /geolocation/client-ip para resolver la IP pública del navegador e incrustarla en el payload. Sin geolocationJwt, collect() realiza cero llamadas de red. El JWT es un token de corta duración generado por tu backend, no una clave de API, por lo que sigue sin haber ninguna clave de API en el navegador.
npm install @surtai/guardian-web
A diferencia de los SDKs nativos (iOS / Android / React Native), el SDK web no expone un método verify(). Todas las decisiones de riesgo ocurren del lado del servidor una vez que tu backend reenvía el payload. Consulta Collect (Servidor a servidor) para el flujo completo del backend.
Reconocimiento del dispositivo (v0.5.0)
A partir de v0.5.0 el SDK añade dos señales al payload cifrado automáticamente - sin ningún cambio de código de tu parte:
- Reconocimiento estable del dispositivo - el SDK genera y persiste un install id por
navegador y lo incluye en
collect(). Surt lo usa para reconocer un dispositivo recurrente entre visitas, de modo que el mismo navegador se resuelve al mismo dispositivo incluso cuando su huella digital cruda colisiona con otras máquinas del mismo modelo. - Reporte de la versión del SDK - el SDK reporta su propia versión de compilación, para que Surt pueda atribuir cada transacción a la versión exacta del SDK.
Ambas viajan dentro del payload cifrado - nunca las lees ni las estableces. Actualizar el paquete es todo lo que se requiere para beneficiarse; consulta Migrar de v0.4 → v0.5.
Navegadores como Safari eliminan el almacenamiento escribible por scripts tras ~7 días, lo que rota el install id. Para mantener el reconocimiento estable ahí, tu backend puede persistir el id en una cookie de origen propio (first-party) retransmitida a través de preflight - un cambio opcional de ~5 líneas cubierto en la guía de migración.
Uso
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 }),
});
Tu backend genera el geolocationJwt llamando a preflight con tu clave sp_live_* - consulta Autenticación. Pasa el JWT a collect() solo cuando quieras que se resuelva la IP pública; de lo contrario, omítelo.
Opciones
| Opción | Tipo | Requerido | Predeterminado | Descripción |
|---|---|---|---|---|
collectLocation | boolean | No | false | Cuando es true, solicita la API de Geolocalización del navegador. Lo único en collect() que puede activar una solicitud de permiso. |
geolocationJwt | string | No | (ninguno) | Cuando se proporciona, collect() resuelve la IP pública del navegador mediante GET /geolocation/client-ip y la incrusta en el payload. Omítelo para saltar la búsqueda de IP (y todas las llamadas de red). |
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 }[];
};
}
El objeto diagnostics te indica qué ocurrió en el dispositivo - por ejemplo, si se recopiló la ubicación o si el usuario la denegó - para que puedas reaccionar en tu interfaz:
const { payload, diagnostics } = await collect({ collectLocation: true });
if (diagnostics.location === 'denied') {
// prompt the user to enable location, then retry
}
Consulta Diagnósticos del resultado para la referencia completa de campos.
Backend: reenviar a Surt
Tu backend recibe el payload del navegador y lo envía al endpoint de evaluación de Surt:
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" }
}
}
}
La forma de la solicitud, los valores de transaction_type admitidos, las opciones de config y el esquema de respuesta son idénticos en todos los SDKs de Guardian. Consulta Collect (Servidor a servidor) para la referencia completa, incluyendo ejemplos en Node / Java / Python.
Errores
collect() lanza un único GuardianError para tres condiciones:
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;
}
}
}
| Código | Significado |
|---|---|
CRYPTO_UNAVAILABLE | Falta window.crypto.subtle. El SDK requiere un contexto seguro (HTTPS o localhost). |
ENCRYPTION_FAILED | El paso de cifrado falló. Trátalo como un error: captúralo y repórtalo. |
INVALID_OPTIONS | El argumento de collect() no es un objeto. |
Los recopiladores individuales (huella digital, batería, geolocalización, red, etc.) nunca lanzan errores: fallan de forma silenciosa. Un permiso revocado o una API no compatible simplemente significa que el campo correspondiente se omite del payload. La búsqueda de IP pública también es de mejor esfuerzo: si la llamada GET /geolocation/client-ip falla o el JWT es rechazado, el campo de IP simplemente se omite y collect() aun así devuelve un payload. El backend tolera payloads incompletos.
Compatibilidad de navegadores
- Cualquier navegador evergreen con
window.crypto.subtle(Chrome, Edge, Firefox, Safari, Opera). - Se requiere contexto seguro: HTTPS en producción,
localhostpara desarrollo. El SDK lanzaCRYPTO_UNAVAILABLEfuera de un contexto seguro. - Geolocalización, Battery Status, NetworkInformation y otras APIs opcionales se degradan de forma elegante cuando no están disponibles.
Ejemplos por framework
- 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 ? 'Verifying...' : 'Continue'}
</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">Sign in</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 ? 'Verifying...' : 'Continue' }}
</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 ? 'Verifying...' : 'Continue'}
</button>
<button id="verify">Continue</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>
Qué es diferente respecto a los SDKs nativos
@surtai/guardian-web | @surtai/guardian-rn / iOS / Android | |
|---|---|---|
Método verify() | No | Sí |
Método collect() | Sí (única API) | Sí |
| Inicialización a nivel de aplicación | Ninguna | initialize(options) |
| Contexto de cliente / transacción | Establecido por tu backend | Transportado en los claims del JWT generado por el backend |
| Clave de API en el cliente | No | No (usa un JWT generado por el backend, no una clave de API) |
| Llamadas de red desde el SDK | Ninguna (a menos que se pase geolocationJwt) | Sí |
| Verificación de integridad del dispositivo | No (sin equivalente en el navegador) | Sí |
| Persiste estado por cliente | No (sin estado en cada llamada) | No |
La versión web es intencionadamente ligera: una única función estática que produce un payload cifrado. La vinculación del cliente, los metadatos de la transacción y las decisiones ocurren en tu backend.