@dustid/dust-go-connect
v0.1.15
Published
A library for connecting a frontend application to the DUST GO react native application
Readme
@dustid/dust-go-connect
DUST Go is a prototyping platform that allows developers to create mobile webapps that leverage DUST Identity's platform-dependent functionality without requiring an upfront investment into mobile toolchains.
Install
npm install @dustid/dust-go-connectUsage
// NOTE: connector will be undefined if running outside DUST Go
import { connector } from '@dustid/dust-go-connect';
const scans: {data: string, metadata: Record<string, unknown>}[] = [];
connector?.add("unique-id-for-this-listener", (event: ScanEvent) => {
if (event.type === 'scan') scans.push(event.payload)
if (event.type === 'hide') // do something
if (event.type === 'show') // do something
});
connector?.showScanner();API
High Level API
scanAsync() Presents the scanner, and awaits a scan.
Rejects if scanner is hidden.
// scanAsync(): Promise<ScanPayload>
const scan = await scanAsync();DUST Go Connector
This is more low level, but more flexible.
connector.showScanner(): voidShows the scanner modal over the main webview.
connector.hideScanner(): voidHides the scanner modal, returning to the main webview. Note, this can be called even while the webview is in the "background", the webview isn't paused while the scanner is presented.
Events
type ScanPayload =
| {
type: 'DUST'
data: string
metadata?: ScanSettings
exif?: ScanExif
}
| {
type: 'QR'
data: string
metadata?: Record<string, any>
}
| { type: 'BARCODE'; data: string; metadata?: Record<string, any> }
| { type: 'DATA_MATRIX'; data: string; metadata?: Record<string, any> }
| { type: 'NFC'; data: string; metadata?: Record<string, any> }ScanSettings can include optional Android Dragon capture fields:
captureSource, usbVendorId, usbProductId, usbDeviceName,
androidCameraId, dragonBackend, and dragonErrorCode.
EXIF (protocol v5)
A DUST scan carries the capture's photographic EXIF, keyed by canonical tag
name — the same names exifr produces:
type ScanExif = {
source: 'camera' | 'synthesized'
tags: Record<string, string | number | number[]>
}
// e.g.
{
source: 'camera',
tags: {
Make: 'Apple',
Model: 'iPhone 17 Pro',
Software: '26.2.1',
ExposureTime: 0.01,
FNumber: 1.78,
ISO: 1600,
FocalLength: 6.765,
LensModel: 'iPhone 17 Pro back camera 6.765mm f/1.78',
ExifImageWidth: 640,
ExifImageHeight: 480,
Orientation: 1,
ProcessingSoftware: 'dust-go 1.1.0 (25)',
UserComment: 'scan-7-h29fk1a2', // the scan id, as DICE wrote its session uuid
}
}Three things to know about it:
sourceis not decoration.camerameans the tags were read out of the EXIF the capture's own encoder wrote.synthesizedmeans the capture carried none (Dragon UVC frames have no EXIF at all) and the app assembled identity and commanded values instead — never measurements. This is the same Attested/Commanded/Applied distinction asappliedAxes(ADR-0004); do not present a synthesized value as what the instrument did.- The tags describe the submitted image, not the sensor frame. The phone
rotates and resizes a loupe capture before encoding, so
Orientationis normalized and the geometry tags report the submitted size (typically 640x480). - The same tags are in three places, so no consumer has to change to see
them: stamped into the image bytes (for backends that parse the stored file,
like the legacy scan-api admin page), on
payload.exif, and flattened as canonical keys inmetadataalongsideexifSource(for tooling that reads only the metadata bag, like the apid console).
Only a curated tag set is carried — no MakerNote-sized blobs, and nothing the
app cannot name. Absent on hosts older than v5.
| Scan Events |
| --------------------------------------- |
| { type: 'scan', payload: ScanPayload} |
| { type: 'show' } |
| { type: 'hide' } |
connector.dispatch(event: ScanEvent): voidAdd a listener with a unique identifier.
connector.add(id: string, cb: (event: ScanEvent) => void): voidRemove the listener with the provided identifier.
connector.remove(id: string): voidCheck if a listener has been added for the given id
connector.has(id: string): booleanRemove all event listeners
connector.clear();Restricting a capture to specific Dust Go builds (protocol v4)
A prescribed capture can name which builds of the capture app may take it, so a
measurement series is not silently mixed across app versions that behave
differently. The policy is carried on CaptureRequirements.app and answered
rejected before the shutter is armed — by the host, and by this library
before it even sends the command.
await runCaptureAsync({
requestId,
condition,
requirements: {
// Every constraint declared here must hold.
app: { minBuild: 128, minVersion: '1.5.0' },
device: 'iPhone 15 Pro',
},
})
// Rejects with reason 'app-version-unsupported' (reported build is outside the
// policy) or 'app-version-unknown' (the app is too old to report its build).build is Dust Go's monotonic build number — iOS buildNumber / Android
versionCode, which never reset — so minBuild is the reliable way to say
"this fix or later".
The two version forms compare differently, on purpose:
minVersion/maxVersionorder dotted numeric segments (1.10.0is newer than1.9.3) and ignore any-suffix, so they cannot separate a prerelease from its release.versions: [...]is an exact string match:['1.5.0']admits1.5.0and rejects1.5.0-beta.2, and naming the prerelease explicitly is the only way to admit it.
The build constraints come in the same two shapes — inclusive bounds and an exact allowlist:
minBuild/maxBuildare inclusive bounds —minBuild: 128rejects every build below 128, which is what you want for "this fix or later".builds: [...]is an exact allowlist. Reach for it when the set is not a range: pinning a run to the builds it started on, or excluding one bad build while keeping the others around it.
A host that cannot report a constrained field is refused, not passed, unless the
policy sets allowUnknown: true — which waives only the constraints whose value
is unknown, leaving the rest enforced.
The same comparison is available directly, for gating UI before a capture is attempted:
import {
evaluateAppRequirement,
getCaptureCapabilities,
hostEnforcesRequirement,
} from '@dustid/dust-go-connect'
const capabilities = getCaptureCapabilities() // null on hosts that announced nothing
const verdict = evaluateAppRequirement({ minBuild: 128 }, capabilities?.app)
if (!verdict.ok) {
// verdict.message is operator-readable; verdict.reason is machine-readable.
}
// Which requirements this host checks itself. A requirement missing from the
// list is one the host would ignore, so gate on it yourself — absence of a
// declaration is never evidence of capability.
hostEnforcesRequirement(capabilities, 'app')Auth
On some platforms, it's necessary to rewrite the redirects from the app to the OAuth2 server to work around platform limitations. It's recommended to always wrap those redirects with this call, and let the connector no-op when no workaround is required.
connector.rewriteRedirect(url: URL): URL