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 y no maneja ninguna clave de API, con una excepción: cuando pasas un geolocationJwt opcional, collect() realiza una única llamada GET /geolocation/client-ip en modo best-effort 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, así 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.
Uso
import { collect } from '@surtai/guardian-web';
// Variante 1: sin búsqueda de IP, cero llamadas de red.
const { payload } = await collect({ collectLocation: false });
// Variante 2: resuelve la IP pública del navegador dentro del payload.
// Tu backend genera el JWT de corta duración mediante preflight.
const { payload: payloadWithIp } = await collect({
collectLocation: false,
geolocationJwt: jwt,
});
// Reenvía el payload a tu 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 resolver la IP pública; de lo contrario, omítelo.
Opciones
| Opción | Tipo | Requerida | Predeterminado | Descripción |
|---|---|---|---|---|
collectLocation | boolean | No | false | Cuando es true, solicita la API de Geolocalización del navegador. Es lo único en collect() que puede activar un aviso 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 saltarte la búsqueda de IP (y todas las llamadas de red). |
CollectResult
interface CollectResult {
/** Payload en base64. Pásalo como `payload.data` en la solicitud evaluate mostrada a continuación. */
payload: string;
}
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 de collect()>"
},
"config": {
"response": {
"address": { "type": "include" }
}
}
}
La forma de la solicitud, los valores admitidos de transaction_type, 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 de Node / Java / Python.
Errores
collect() lanza un único GuardianError en 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': /* no está en un contexto seguro */ break;
case 'ENCRYPTION_FAILED': /* el cifrado falló (raro) */ break;
case 'INVALID_OPTIONS': /* argumentos inválidos */ 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 bug: captura y reporta. |
INVALID_OPTIONS | El argumento de collect() no es un objeto. |
Los recolectores individuales (huella digital, batería, geolocalización, red, etc.) nunca lanzan: fallan silenciosamente. Un permiso revocado o una API no compatible solo significa que el campo correspondiente se omite del payload. La búsqueda de IP pública también es best-effort: si la llamada GET /geolocation/client-ip falla o el JWT es rechazado, el campo de IP simplemente se omite y collect() igual devuelve un payload. El backend tolera payloads incompletos.
Compatibilidad de navegadores
- Cualquier navegador moderno con
window.crypto.subtle(Chrome, Edge, Firefox, Safari, Opera). - Contexto seguro requerido: HTTPS en producción,
localhostpara desarrollo. El SDK lanzaCRYPTO_UNAVAILABLEfuera de un contexto seguro. - Geolocalización, Battery Status, NetworkInformation y otras APIs opcionales degradan con elegancia cuando no están disponibles.
Ejemplos de frameworks
- 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 ? 'Verificando...' : 'Continuar'}
</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,
}),
});
// Tu ruta /api/sign-in reenvía `payload` al endpoint /evaluate de Surt.
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Iniciar sesión</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 ? 'Verificando...' : 'Continuar' }}
</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 ? 'Verificando...' : 'Continuar'}
</button>
<button id="verify">Continuar</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>
Diferencias con 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 | Incluido 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í |
| Estado por cliente persistente | No (sin estado entre llamadas) | No |
Web es deliberadamente minimalista: una única función estática que produce un payload cifrado. El vínculo con el cliente, los metadatos de la transacción y las decisiones ocurren todos en tu backend.
Diagnósticos del resultado
El resultado de collect() ahora incluye un objeto opcional diagnostics con lo que observó el SDK durante la transacción: location (collected · denied · unavailable · timeout · not_requested), networkIntel (collected · unavailable) y warnings (lista de { code, signal }).
const { diagnostics } = await collect();
if (diagnostics?.location === 'denied') {
// pide al usuario que active la ubicación
}
Consulta Códigos de error para la referencia completa.