--- name: dust-go-connect-integration description: Make a customer web app scan DUST identifiers when it runs inside the DUST Go mobile app, using the @dustid/dust-go-connect npm package — detect the app, capture scans (promise and listener APIs), push scan context and result feedback into the native scanner, handle DUST/QR/barcode/NFC payloads, resolve captures server-side against the DUST API, and handle OAuth redirect rewriting inside the WebView. Use this skill whenever you are adding DUST Go support to a web app, debugging the connector bridge, or wiring scan payloads to the /api/v1/tags endpoints. version: 2026.8.0 --- # DUST Go integration (@dustid/dust-go-connect) Generated from DUST API OpenAPI spec 2026.8.0 (190 paths), docs version 2026.8.0. This skill is self-contained. Reference material: the tutorial at https://docs.dustid.io/integrate/dust-go-connect/, the npm package https://www.npmjs.com/package/@dustid/dust-go-connect, and the DUST API docs at https://docs.dustid.io (agent skill: `dice-api-integration`). ## What DUST Go is DUST Go is DUST Identity's mobile app (App Store id `6636551540`). It is an embedded mobile browser: it loads the customer's ordinary web app in a WebView and exposes the device's DUST scanning hardware (camera + optical accessories) to the page through a small JavaScript bridge — the `@dustid/dust-go-connect` package. The web app never talks to scanning hardware directly and needs no native toolchain. Constraints that shape integrations: - **App-link allowlist.** DUST Go only opens URLs registered with DUST Identity (plus custom links added manually on-device for development). The customer's app URL must be registered before production use. - **A DUST scan is a raw capture, not an identity.** The page receives a base64-encoded JPEG plus metadata. Identification / verification / binding happen server-side via the DUST API `/api/v1/tags/*` endpoints, which require server-held credentials. - **Version pinning.** Use the latest published scanner-only version available from DUST, but avoid registry version `0.1.9` exactly — it contains a since-removed native-auth surface. If your registry only offers `0.1.9`, pin the latest earlier version or request the replacement package from DUST Identity. ## 1. Install and detect ```bash npm install @dustid/dust-go-connect ``` The package is dependency-free and ships TypeScript types. The `connector` export is `undefined` outside DUST Go, so one build serves both regular browsers and DUST Go: ```ts import { connector } from "@dustid/dust-go-connect"; export const insideDustGo = Boolean(connector); ``` - **Import timing:** detection happens at module-import time, in the browser. In SSR apps, gate all connector usage on hydration (the import is safe server-side; `connector` is just `undefined` there). - **Server-side detection:** DUST Go tags the WebView User-Agent with `DustGo/ ()` — usable as a rendering hint before any JS runs, but spoofable; never treat it as a security boundary. ## 2. Capture a scan ### Promise API (single capture) ```ts import { scanAsync } from "@dustid/dust-go-connect"; const payload = await scanAsync(); // payload: { type: 'DUST' | 'QR' | 'BARCODE' | 'DATA_MATRIX' | 'NFC', // data: string, metadata?: ScanMetadata } ``` `scanAsync()` opens the native scanner over the page and resolves with one `ScanPayload`. It **rejects** when the user closes the scanner without capturing, and rejects immediately when no connector is present — guard on `connector` first in shared code paths. ### Listener API (multi-scan; scanner stays open) ```ts import { connector } from "@dustid/dust-go-connect"; connector?.add("my-listener", (event) => { switch (event.type) { case "scan": handleScan(event.payload); break; case "show": break; // scanner opened case "hide": break; // scanner closed default: break; // ignore unknown types — the protocol may grow } }); connector?.showScanner(); // later: connector?.hideScanner(); connector?.remove("my-listener"); ``` For the common case of "receive every scan without managing listener ids", use `addScanListener`, which returns its own unsubscribe: ```ts import { addScanListener } from "@dustid/dust-go-connect"; const stop = addScanListener((payload) => handleScan(payload)); // later: stop(); ``` Unlike `scanAsync()` (one scan per call), this keeps listening — which is what a multi-capture flow needs, because a single scanner session can deliver a series of scans. **Submit them one at a time.** A DUST payload is a large base64 JPEG, so uploading several concurrently starves the connection; queue them and await each. ### Scan feedback (spinner → result in the native scanner) By default the native scanner closes immediately after each scan. A page that processes scans server-side can instead keep the scanner open and show the outcome right over the viewfinder: ```ts import { addScanListener, sendScanResult, setScanFeedbackEnabled, } from "@dustid/dust-go-connect"; setScanFeedbackEnabled(true); // once, when the scanning flow mounts const stop = addScanListener(async (payload) => { const scanId = payload.metadata?.scanId; sendScanResult({ op: "verify", scanId, status: "pending", title: "Verifying…" }); try { const outcome = await verifyOnServer(payload); sendScanResult({ op: "verify", scanId, status: "success", title: outcome.title }); } catch { sendScanResult({ op: "verify", scanId, status: "failure", title: "Verification failed", retry: true, }); } }); ``` - `setScanFeedbackEnabled(true)` is the page's declaration that it will answer scans with `sendScanResult`; only then does the scanner hold open after a scan showing a spinner and awaiting the result. `sendScanResult` is only meaningful after that call, and pages that never call it keep the close-after-scan behavior. - **Echo the scan's `metadata.scanId` on every result you send for it.** The scanner drops a result naming a scan it has moved past, so a slow answer can never land on the next item's overlay. - `status: 'pending'` updates the in-progress label; `success` / `failure` are terminal for that scan — in a batch, scanning resumes the moment a terminal result arrives. `message` carries operator guidance, `retry: true` asks the operator to rescan the same item. - Older DUST Go builds ignore both calls, so the code above degrades cleanly. ### Scan context (show the operator what the next scan is for) `setScanContext` pushes context — the operation, the Thread being worked on, a suggested Identifier name, an optional batch queue — into the native scanner so the operator can see what the next scan applies to without flipping back to the page. Pass `null` to clear. It is purely informational: the app echoes it back on scan metadata (`contextOp`, `contextTitle`, `operatorTagName`, …) but never acts on it. ```ts import { onScanContextSelect, setScanContext } from "@dustid/dust-go-connect"; setScanContext({ op: "bind", title: "Widget A-42", subtitle: "Batch 7", queue: items.map((i) => ({ id: i.id, label: i.name, status: i.status })), queueIndex: 2, }); const stop = onScanContextSelect((selection) => { // The operator picked a different queue item inside the scanner (tapped an // item, skipped forward, stepped back). The web app stays the owner of the // queue: treat the selection as a request to move the pointer, then answer // it by pushing a fresh setScanContext — exactly as a pick made in the web // app's own UI would. moveTo(selection.id ?? queue[selection.index]?.id); }); ``` Correlate picks on `selection.id` when your queue items carry ids — `index` is only a position in the queue as it was on screen. `selection.source` (`'forward'` / `'back'` / `'reselect'`) describes the move so you can decide what happens to the item left behind (e.g. mark a skipped item so it stays visibly outstanding). Older hosts that never send the event simply never fire the callback; the scanner still tracks the pick locally and echoes the chosen item on the resulting scan, so an unanswered pick is degraded, not broken. ### Host capabilities A host announces what it can actually do, so a page can adapt instead of guessing from the User-Agent. Read announcements through the typed helpers: ```ts import { getCaptureCapabilities, onCaptureCapabilities, } from "@dustid/dust-go-connect"; const capabilities = getCaptureCapabilities(); // CaptureCapabilities | null — { host, app, device, scanner, captureModes, axes, … } const stop = onCaptureCapabilities((caps) => { // fires immediately with the cached announcement when one exists, and again // whenever the host re-announces (e.g. an external accessory is attached) }); ``` `getCaptureCapabilities()` returns `null` when the host has not announced — an older DUST Go build, or an announcement that has not arrived yet. Read `null` as "single captures only, nothing adjustable" — never as silently capable. The package exports `UNANNOUNCED_HOST_CAPABILITIES` as exactly that conservative assumption (`captureModes: ['single']`, no adjustable axes) for code that wants a value to fall back to. `calibrationResult` / `ackCalibrationResults()`, and the capture-prescription surface a host may accept — `runCaptureAsync`, `captureStatus` events, `CaptureRequirements` (including its `app` version/build policy), `CaptureCapabilities.enforcedRequirements`, and the requirement-evaluation helpers (`evaluateAppRequirement`, `hostEnforcesRequirement`, `compareAppVersions`, `parseAppBuild`) — are internal to DUST first-party workflows — ignore them. ### Payload contents | `payload.type` | `payload.data` | | --- | --- | | `DUST` | Base64-encoded JPEG of the capture (large) — resolve server-side | | `QR`, `BARCODE`, `DATA_MATRIX` | The decoded symbol contents | | `NFC` | The hex id read from the NFC chip | `payload.metadata` (`ScanMetadata`) carries device identifiers (`deviceId`, `modelName`, `osName`, `osVersion`, `appVersion`), lens/camera selection, and optionally optics (zoom/focus/exposure/ISO), geolocation (`latitude`/`longitude`/`accuracy`), and external-accessory capture-source fields (`captureSource`, `usbVendorId`, `usbProductId`, `dragonBackend`). Forward it opaquely when binding — the API stores it with the Identifier. ## 3. Resolve the capture against the DUST API DUST captures must be sent as **binary multipart data** (decode the base64 first) to the identifier endpoints. The three operations: - `POST /api/v1/tags/identify` — which Thread does this capture belong to? (no `threadId`; optional `searchGroupIds` JSON array of Team ids narrows the search) - `POST /api/v1/tags/bind` — attach the capture to a Thread (`threadId` required; DUST binds may pass `options.enrollmentSessionId`, a client-generated UUID grouping multi-scan enrollment) - `POST /api/v1/tags/verify` — does this capture match a specific Thread's identifiers? (`threadId` required) ```ts async function identifyDustScan(base64Jpeg: string) { const bytes = Uint8Array.from(atob(base64Jpeg), (c) => c.charCodeAt(0)); const form = new FormData(); form.set("tagType", "DUST"); form.set("data", new Blob([bytes], { type: "image/jpeg" })); // API field names keep legacy "group" naming — these are Team ids. form.set("searchGroupIds", JSON.stringify([TEAM_ID])); const response = await fetch(`${APID_URL}/api/v1/tags/identify`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Dust-Ctx-Org-Id": ORGANIZATION_ID, }, body: form, }); if (!response.ok) throw new Error(`identify failed: ${response.status}`); return await response.json(); // → { type: "identified", identified: { tag, thread, ... } } on a confident match // or { type: "matches", matches: [...] } for candidates } ``` Text payloads (`QR`/`BARCODE`/`DATA_MATRIX`/`NFC`) send `payload.data` directly as the `data` form field — no decoding. **Credentials are server-side only.** Never embed a long-lived DUST credential or token in the page. Have the customer's backend either proxy the `/api/v1/tags/*` calls or mint short-lived scoped tokens per browser session. Auth details (Service Account API key → `GET /api/auth/token` with `x-api-key` header → `Authorization: Bearer`, plus the `Dust-Ctx-Org-Id`/`Dust-Ctx-Team-Id` context headers) are covered by the `dice-api-integration` skill and https://docs.dustid.io/api/authentication/. ## 4. Geolocation Standard `navigator.geolocation` works inside DUST Go — proxied through the native OS permission prompt. No connector code needed. ## 5. OAuth / OIDC inside DUST Go DUST Go hands external identity-provider navigations to the system browser; the callback returns to the page via a custom URL scheme. Always wrap OAuth `redirect_uri` construction: ```ts 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; ``` Outside DUST Go this is a no-op. Inside, it swaps the protocol for the app's custom scheme (e.g. `com.dustidentity.dustgo://your-host/auth/callback`; the exact scheme comes from the DUST Go build, `dustgo` is the fallback), which the identity provider must have registered as an allowed redirect URI. ### Logout and account switching DUST Go's embedded WebView and the device's system browser do not necessarily share a cookie jar. Clearing the web app's session during an explicit logout therefore may leave the identity-provider session in the system browser intact, causing the next authorization request to sign the same account back in silently. To preserve normal SSO while still allowing account switching, set a short-lived, one-use first-party marker when the app logs out; consume it on the next login and add the standard OIDC `prompt=login` parameter to that authorization request. Do not add the prompt to every login, and do not rely on clearing an identity-provider cookie from the WebView. **Known limitation:** current DUST Go builds rewrite the `redirect_uri` query parameter on *any* cross-origin navigation carrying one. Third-party providers that reject unregistered custom-scheme redirect URIs (most public IdPs) will fail to complete sign-in inside DUST Go. If the app needs third-party OAuth inside DUST Go, register the DUST Go schemes with the provider if possible, and contact DUST Identity. ## 6. Protocol versioning The connector speaks protocol version 4: at import time it sends an automatic `hello` handshake advertising the events and commands the page supports. Keep the package current; switch on `event.type` and ignore unknown event types. ## 7. Testing checklist 1. Install DUST Go from the App Store (`id6636551540`); DUST capture requires a supported optical accessory (see https://docs.dustid.io/integrate/supported-devices/). 2. Serve the app over HTTPS on a URL the device can reach (LAN is fine for development). 3. Add the URL as a custom app link in DUST Go and open it. 4. Confirm `connector` is defined, run `scanAsync()`, and verify the payload reaches the backend and resolves via `/api/v1/tags/identify`. 5. A diagnostic page exercising the whole bridge (detection, `scanAsync`, event log) is available in the package repository. ## Verification checklist for generated integrations - All connector usage is guarded (`connector?.` or an `insideDustGo` check) so the code also runs in regular browsers. - DUST payloads are base64-decoded to binary before multipart upload; text payloads are sent as-is. - No DUST API key or long-lived token appears in client-side code. - Unknown connector event types are ignored, not errors. - Pages that call `sendScanResult` enable feedback first (`setScanFeedbackEnabled(true)`) and echo `metadata.scanId` on every result. - The package version is the latest scanner-only release available from DUST, never exactly `0.1.9`.