Ir al contenido

Integración con DUST Go

DUST Go es un navegador móvil integrado: carga tu aplicación web en una WebView y expone a la página el hardware de escaneo DUST del dispositivo mediante un pequeño puente JavaScript, @dustid/dust-go-connect. Tú desarrollas y alojas una aplicación web convencional; DUST Go proporciona la cámara, los accesorios ópticos y el proceso de captura, sin necesidad de una cadena de herramientas nativa.

Esta página te guía hasta un escaneo resuelto. Todo lo demás —inicio de sesión, geolocalización, compatibilidad y hardware de escritorio— se explica en Más allá del primer escaneo, más abajo.

Antes de empezar

Rol
Una credencial de cuenta de servicio en poder de tu backend
Dispositivo
Un iPhone con DUST Go y un accesorio Loupe compatible para la captura DUST
  1. Un usuario abre tu aplicación web dentro de DUST Go (mediante un enlace de aplicación).
  2. Tu página importa @dustid/dust-go-connect; la biblioteca detecta el puente de DUST Go y expone un connector.
  3. Tu página llama a scanAsync(). DUST Go abre el escáner nativo sobre tu página.
  4. Tras la captura, DUST Go devuelve un evento de escaneo a tu página: la carga útil contiene los datos y metadatos de la captura.
  5. Tu página envía la captura a tu backend, que llama a APID (/api/v1/tags/identify, /bind o /verify) para resolverla.

Un escaneo DUST entrega a tu página una captura sin procesar (un JPEG codificado en base64) junto con los metadatos de captura, no un Identificador resuelto. La identificación, la verificación y la vinculación se realizan en el servidor.

Ventana de terminal
npm install @dustid/dust-go-connect

El paquete no tiene dependencias, utiliza la licencia MIT e incluye tipos de TypeScript.

La exportación connector es undefined cuando tu página no se ejecuta dentro de DUST Go, por lo que la misma compilación de tu aplicación puede funcionar tanto en navegadores convencionales como en DUST Go:

dust-go.ts — runs in the BROWSER
import { connector } from "@dustid/dust-go-connect";
export const insideDustGo = Boolean(connector);

Debes tener en cuenta dos cosas:

  • Momento de la importación. La detección se produce en el navegador al importar el módulo. Si renderizas en el servidor, condiciona cualquier uso del conector a la hidratación.
  • Detección en el servidor. Las versiones recientes de DUST Go también etiquetan el User-Agent de la WebView con DustGo/<version> (<app id>), por lo que tu servidor puede detectar la aplicación antes de que se ejecute JavaScript. Trátalo como una indicación, no como un límite de seguridad: cualquiera puede falsificar un User-Agent.

La vía más sencilla es la API basada en promesas: presenta el escáner y espera una captura:

scan.ts — runs in the BROWSER
import { scanAsync } from "@dustid/dust-go-connect";
const payload = await scanAsync();
// payload: { type: 'DUST' | 'QR' | 'BARCODE' | 'DATA_MATRIX' | 'NFC',
// data: string, metadata?: ScanMetadata }

scanAsync() devuelve una Promise<ScanPayload>. Se rechaza si el usuario cierra el escáner sin realizar una captura y se rechaza inmediatamente cuando se llama fuera de DUST Go (no hay ningún conector), por lo que debes comprobar primero connector si la misma ruta de código se ejecuta en navegadores convencionales.

payload.typepayload.dataNotas
DUSTJPEG de la captura DUST codificado en base64De gran tamaño; resuélvelo en el servidor mediante APID
QR, BARCODE, DATA_MATRIXEl contenido decodificado del símbolo
NFCEl identificador hexadecimal leído del chip NFC

payload.metadata (con tipo ScanMetadata) describe la captura: identificadores del dispositivo (deviceId, modelName, osName, osVersion, appVersion), selección de lente/cámara y, opcionalmente, campos ópticos (zoom, enfoque, exposición, ISO), geolocalización (latitude/longitude/accuracy) y detalles del origen de captura para accesorios de escaneo externos (captureSource, usbVendorId, usbProductId, dragonBackend). Reenvíalo sin modificar al vincular: APID lo almacena con el Identificador.

Lee esto antes del paso de resolución: determina la estructura de toda tu integración.

Por tanto, el paso de resolución utiliza dos archivos en dos lugares:

ArchivoDónde se ejecuta¿Contiene la credencial de DUST?
Tu página de escaneoNavegador dentro de DUST GoNo
Tu controlador /api/dust-scanTu servidorSí

La captura DUST solo resulta útil cuando APID encuentra una coincidencia. La página decodifica la captura base64 en datos binarios y la envía a tu propio endpoint:

identify.ts — runs in the BROWSER
// No DUST credential in this file.
async function identifyDustScan(base64Jpeg: string) {
// The scan arrives base64-encoded; APID expects binary multipart data.
const bytes = Uint8Array.from(atob(base64Jpeg), (c) => c.charCodeAt(0));
const form = new FormData();
form.set("operation", "identify");
form.set("tagType", "DUST");
form.set("data", new Blob([bytes], { type: "image/jpeg" }));
const response = await fetch("/api/dust-scan", {
method: "POST",
body: form,
credentials: "same-origin",
});
return { status: response.status, body: await response.json() };
}

Tu backend añade la credencial y el contexto, y especifica por sí mismo el ámbito de búsqueda:

server/dust-scan.ts — runs on YOUR SERVER
export async function handleIdentify(form: FormData, session: Session) {
// `searchTeamIds` is a JSON array of Team UUIDs. Set it on the server — a
// browser-supplied scope is a request, never an authorization.
form.set("searchTeamIds", JSON.stringify(session.allowedTeamIds));
const response = await fetch("https://apid.dustid.io/api/v1/tags/identify", {
method: "POST",
headers: {
Authorization: `Bearer ${await getDustToken()}`,
"Dust-Ctx-Org-Id": session.organizationId,
},
body: form,
});
// Pass the status and body through unchanged: the page needs to tell
// "nothing matched" (404 IDENTIFIER_NOT_FOUND) from "try again"
// (503 SCAN_SEARCH_INCOMPLETE).
return new Response(await response.text(), {
status: response.status,
headers: { "Content-Type": "application/json" },
});
}

El campo es searchTeamIds. Las cargas útiles de identificación rechazan las propiedades que no declaran, por lo que la grafía antigua searchGroupIds hace que toda la solicitud falle con 400 INVALID_REQUEST en lugar de ignorarse. El único nombre antiguo group que se conserva es el encabezado Dust-Ctx-Grp-Id, que aún se acepta como alias de Dust-Ctx-Team-Id.

Las otras dos operaciones utilizan la misma estructura multipart:

  • /api/v1/tags/bind: añade threadId. options.enrollmentSessionId es opcional: proporciona un UUID generado por el cliente y reutilizado durante una ejecución cuando varias capturas pertenecen al mismo grupo (por ejemplo, una estación de inscripción que procesa un lote); omítelo para una vinculación puntual.
  • /api/v1/tags/verify: añade threadId y tags, una matriz de objetos ([{ "tagId": "…", "tagType": "DUST" }], codificada como JSON en el contenido multipart), no cadenas de identificadores. tags es obligatorio.

Consulta Identificadores para ver los contratos completos de solicitud y respuesta, y React Scanner para obtener un componente listo para copiar que implementa las tres operaciones mediante un controlador de backend como el anterior.

  1. Instala DUST Go desde la App Store o Google Play (consulta Dispositivos compatibles para conocer los requisitos de hardware; la captura DUST necesita un accesorio óptico compatible).
  2. Sirve tu aplicación mediante HTTPS en una URL a la que pueda acceder el dispositivo (una dirección LAN sirve para el desarrollo).
  3. En DUST Go, añade tu URL como enlace de aplicación personalizado y ábrela.
  4. Comprueba que se detecta el conector y, después, realiza un escaneo.
  5. Confirma que la captura llega a tu backend y que la llamada de DUST realizada por este devuelve un resultado que puedas renderizar, incluido el caso «sin coincidencia».

En el repositorio del paquete hay disponible una página mínima de diagnóstico que ejercita todo el puente (detección, scanAsync y registro de eventos).


Todo lo que aparece a continuación es opcional. Vuelve a esta sección cuando un único escaneo se resuelva de principio a fin.

Para los flujos que realizan varios escaneos antes de enviarlos, utiliza la API de escucha: el escáner permanece abierto entre capturas:

scan-many.ts — runs in the BROWSER
import { connector } from "@dustid/dust-go-connect";
connector?.add("my-listener", (event) => {
switch (event.type) {
case "scan":
handleScan(event.payload);
break;
case "hide": // scanner closed
case "show": // scanner opened
break;
default:
// Ignore unknown event types — the protocol may grow.
break;
}
});
connector?.showScanner();
// later: connector?.hideScanner(); connector?.remove("my-listener");

addScanListener hace lo mismo sin necesidad de gestionar el registro del listener: recibe todos los escaneos y devuelve su propia función de cancelación de la suscripción:

scan-many-simple.ts — runs in the BROWSER
import { addScanListener } from "@dustid/dust-go-connect";
const stop = addScanListener((payload) => handleScan(payload));
// later: stop();

Una misma sesión del escáner puede producir una serie de capturas, así que envíalas de una en una. Una carga útil DUST es un JPEG base64 de gran tamaño; cargar varias a la vez satura la conexión y no ofrece al operador ninguna indicación útil del progreso. Pon las cargas útiles en una cola y espera a que termine cada envío antes de comenzar el siguiente.

Los hosts varían: un teléfono puede ajustar el zoom y la exposición, un accesorio de microscopio USB expone un conjunto diferente y una versión anterior podría no exponer nada. Consulta las funciones disponibles en lugar de deducirlas a partir del User-Agent:

capabilities.ts — runs in the BROWSER
const capabilities = connector?.getCapabilities?.();

Considera que un anuncio ausente o vacío significa solo capturas individuales, sin parámetros ajustables. Nunca interpretes la ausencia de información como una función disponible.

Las llamadas estándar a navigator.geolocation funcionan dentro de DUST Go: la aplicación las canaliza de forma transparente a través de la solicitud de permisos nativa del sistema operativo. No se necesita código del conector.

Flujos de inicio de sesión dentro de DUST Go

Sección titulada «Flujos de inicio de sesión dentro de DUST Go»

Si tu aplicación utiliza OAuth/OIDC, ten en cuenta que DUST Go transfiere al navegador del sistema las navegaciones hacia proveedores de identidad externos y que la devolución de llamada regresa a tu página mediante un esquema de URL personalizado.

Cuando construyas un redirect_uri de OAuth en la página, pásalo siempre por el método correspondiente:

redirect.ts — runs in the BROWSER
import { connector } from "@dustid/dust-go-connect";
const origin = window.location.origin;
const redirectUri = (
connector?.rewriteRedirect(new URL(`${origin}/auth/callback`)) ??
new URL(`${origin}/auth/callback`)
).href;

Fuera de DUST Go (y en hosts donde no sea necesario reescribirlo), esto no produce ningún cambio. Dentro de DUST Go, sustituye el protocolo de la URL por el esquema personalizado de la aplicación y produce un URI como com.dustidentity.dustgo://your-host/auth/callback (el esquema exacto procede de la versión de DUST Go; dustgo es el valor alternativo), que tu proveedor de identidad debe tener registrado como URI de redirección permitido.

El puente utiliza un protocolo numerado. La página anuncia los eventos que admite mediante un intercambio automático hello al importar el paquete, y el host solo envía los eventos que la página ha anunciado. Por tanto, un host antiguo y una página más reciente pueden interoperar mediante una reducción de funciones en lugar de fallar.

Lee el número del paquete que hayas instalado, no de esta página: el SDK lo exporta.

import { CONNECT_PROTOCOL_VERSION } from "@dustid/dust-go-connect";
ProtocoloFunciones añadidas
1El contrato antiguo implícito: scan / hide / show, sin intercambio inicial.
2El intercambio hello y el evento calibrationResult.
3El anuncio capabilities del host a la página, el comando runCapture de la página al host y el evento captureStatus que lo responde.
4CaptureRequirements.app (una política de versión de la aplicación aplicada por el host) y CaptureCapabilities.enforcedRequirements, para que una página pueda distinguir entre un host que comprueba un requisito y otro que lo ignoraría.
5ScanPayload.exif: los datos EXIF fotográficos de la captura junto con su procedencia. Es una incorporación totalmente aditiva.
6El comando capturePhoto de la página al host y el evento photo que lo responde: una fotografía convencional (por ejemplo, un archivo adjunto a una Ficha), que deliberadamente no es un escaneo.

El SDK del árbol de código fuente de esta documentación es @dustid/dust-go-connect 0.2.0, que utiliza el protocolo 6. Aquí no se afirma qué ofrece actualmente el registro público de npm: consulta CONNECT_PROTOCOL_VERSION y el campo version del paquete que hayas instalado realmente, y pregunta a DUST Identity si necesitas una versión concreta.

Reglas que seguirán siendo válidas independientemente del número de versión:

  • La detección de funciones disponibles en tiempo de ejecución es la fuente de autoridad. Un número de protocolo indica lo que puede expresar el SDK de la página, pero no dice nada sobre el host del otro extremo ni sobre el hardware conectado. Consulta getCapabilities() y considera que la ausencia de un anuncio significa «solo capturas individuales, sin parámetros ajustables».
  • Evalúa event.type e ignora aquello que no reconozcas. Se añaden nuevos tipos de eventos; una página que genera una excepción ante un tipo desconocido deja de funcionar tras una actualización del host que no debería afectarle.
  • Un control manual en la interfaz de un host no constituye una función del protocolo, y que un comando de hardware se ejecute correctamente no demuestra que el valor se haya aplicado en el momento de la captura.
  • Los eventos calibrationResult y ackCalibrationResults() son mecanismos internos de los flujos de trabajo de calibración propios de DUST; las integraciones de terceros pueden ignorarlos.

Esta sección trata sobre un Dragon conectado a un ordenador de escritorio, controlado desde un navegador convencional mediante la aplicación complementaria. No es el flujo de Android: dentro de DUST Go en Android, la propia aplicación controla el Dragon conectado y sus capturas llegan mediante el flujo runCapture convencional descrito anteriormente; createDragonClient() no interviene y no se instala ninguna aplicación complementaria en el teléfono. Consulta Dispositivos compatibles.

Tu sitio web puede controlar la vista previa de Dragon y sus controles de enfoque. La aplicación complementaria instalada gestiona los comandos USB en segundo plano. La primera vez, los usuarios aprueban tu sitio web en DUST Camera Controls y conceden acceso del navegador a la cámara y al dispositivo local cuando se les solicita. La aprobación se recuerda para ese sitio web y ese navegador. El sitio web debe usar HTTPS. Camera Controls puede permanecer en la barra de menús con su ventana cerrada; no se necesita ninguna ventana emergente de conexión. No se requiere ningún sitio web alojado por DUST.

Instala DUST Camera Controls en Aplicaciones y ábrelo una vez. La aplicación muestra si Dragon está conectado y permanece disponible en la barra de menús. El inicio al iniciar sesión está habilitado de forma predeterminada; puedes desactivarlo en la aplicación.

Los instaladores para cada plataforma se enumeran en la página Descargas. Las distribuciones con actualizaciones habilitadas buscan actualizaciones automáticamente y ofrecen Buscar actualizaciones… en la barra de menús. Tú eliges cuándo instalar una actualización; guarda tu trabajo y termina el escaneo antes de reiniciar la aplicación. Las versiones de evaluación sin una fuente de actualizaciones requieren que DUST proporcione un instalador de reemplazo.

En DICE, abre Escanear, selecciona DUST, activa Controles de enfoque de Dragon debajo del escáner y elige Conectar la primera vez. Tras la aprobación, el área de visualización ofrece enfoque automático, ajustes de enfoque y Escanear mediante la operación seleccionada. Cuando vuelves a Escanear con el modo activado, DICE se conecta de nuevo automáticamente si Camera Controls está en ejecución y el navegador aún tiene acceso a la cámara. Si los permisos del navegador requieren atención, elige Conectar o Iniciar cámara para terminar la conexión.

dragon.ts — runs in the BROWSER
import { createDragonClient } from "@dustid/dust-go-connect";
const dragon = createDragonClient();
const video = document.querySelector("video")!;
// First use: request native approval and browser permissions from a user action.
connectButton.onclick = async () => {
try {
await dragon.connect();
await dragon.openVideo(video);
const controls = await dragon.getControls();
const focus = controls.find((control) => control.name === "focus");
// Use focus.min / focus.max for your manual slider when focus is available.
} catch (error) {
showConnectionError(error);
}
};
// On a later visit, reuse approval without creating a native approval prompt.
// If this rejects, present Connect Dragon; do not repeatedly request approval.
// await dragon.connect({ interactive: false });
// Reopen video only after browser camera permission has already been granted.
manualSlider.onchange = async () => {
await dragon.setControl("focus", Number(manualSlider.value));
};
autofocusButton.onclick = async () => {
try {
const result = await dragon.autofocus(video, {
radius: Number(autofocusRangeSlider.value),
onProgress: ({ position, sampled }) => showProgress(position, sampled),
});
manualSlider.value = String(result.position);
// "low-contrast" means autofocus retained the starting focus.
showFocusResult(result.outcome);
} catch (error) {
showFocusError(error);
}
};
cancelButton.onclick = () => dragon.cancelAutofocus();
captureButton.onclick = async () => {
const payload = await dragon.capture(video);
// Send the raw DUST JPEG to your server, which calls the Identifier API.
await sendToYourServer(payload);
};
const unsubscribe = dragon.onState(({ connected, controls }) => {
updateDeviceUI(connected, controls);
});
// On page/component teardown: unsubscribe(); dragon.dispose();

Solo una sesión de un sitio web puede controlar Dragon a la vez. El usuario puede revocar el acceso mediante Gestionar el acceso de sitios web… en Camera Controls. Cerrar la ventana nativa mantiene disponible la conexión; salir de la aplicación la detiene. Una sesión aprobada recibe actualizaciones del estado del dispositivo cuando Dragon se desconecta o vuelve a conectarse. Vuelve a abrir el vídeo después de la reconexión. Conectar el hardware no concede acceso a los sitios web de forma silenciosa.

El enfoque automático busca alrededor del último comando de enfoque, dentro del intervalo calibrado del dispositivo. Su control deslizante de intervalo representa una distancia a cada lado de esa posición inicial. Mantén la página visible y la muestra física inmóvil, con el detalle deseado en el centro de la imagen. Una muestra física sin rasgos distintivos o con poca iluminación podría no proporcionar contraste suficiente para seleccionar el enfoque. Comprueba la imagen resultante antes de realizar la captura.

Los valores de enfoque son comandos, no mediciones de la posición de la lente. Estos métodos de control manual no garantizan los ajustes en el momento de accionar el obturador y no habilitan capturas prescritas ni combinadas. El escáner móvil y su contrato scanAsync() existente siguen disponibles de forma independiente.