Bind a browser device through AuthenSee, vouch for another persona, and verify the resulting path on staging. The steps below show excerpts from the full module; use its complete imports, checks, and application adapters when integrating.
1. Arrange access and configuration
Request early access for the private SDK and register your exact application origin, including its port. Public package availability is coming soon. Follow the installation instructions supplied during onboarding and arrange browser CORS access.
The optional /zk entry requires the exact peer @aztec/bb.js@5.2.0. Configure your bundler to serve its module and WASM assets locally. Supply staging attestation JWKS through trusted application configuration, independently of query responses.
const serverOrigin = 'https://trust-staging.rebellion.systems';
const graph = createTrustGraphSdk({ serverUrl: serverOrigin });Exchange a second consenting test participant's graphNodeId directly. There is no public email-address or phone-number lookup.
2. Implement the application adapters
Your app supplies durable storage and verified device state. These methods are application code, not SDK APIs:
export interface BrowserApp {
loadKey(): Promise<BrowserKey | null>;
saveKey(key: BrowserKey): Promise<void>;
loadPending(): Promise<PendingBind | null>;
savePending(pending: PendingBind): Promise<void>;
loadCompletion(): Promise<string | null>;
saveCompletion(completionProof: string): Promise<void>;
loadBound(): Promise<FinalizeBindResult | null>;
saveBound(bound: FinalizeBindResult): Promise<void>;
clearCeremony(): Promise<void>;
refreshDeviceState: FinalizeBindInput['refreshDeviceState'];
}Persist the non-extractable browser CryptoKey with IndexedDB structured cloning. Keep bind capabilities private. savePending must durably replace the pending record without clearing its matching key or completion capability. Save the bound result before clearCeremony removes those capabilities.
refreshDeviceState must fetch and verify the own-node directory against retained chain pins, finish pagination to the advertised head, persist verified state atomically, and call deviceState with current lifecycle metadata. Pass key.recovery?.rootRecordHash as recoveryRootRecordHash; missing and null differ. See identity and the devices API.
3. Save the key and start binding
Call the module's start() from your sign-in action. It saves the key and pending ceremony before navigating. For another device on an existing persona, call start(recoverNodeId) with that persona's reference.
let key = await app.loadKey();
if (!key) {
key = await generateBrowserGraphKey();
await app.saveKey(key); // Durably persist before any navigation.
}
const pending = await beginBind({
serverOrigin,
appOrigin: window.location.origin,
publicKeyJwk: key.publicKeyJwk,
...(recoverNodeId ? { recoverNodeId } : {}),
});
await app.savePending(pending);
window.location.assign(pending.hostedUrl);The full module also refuses to overwrite a pending ceremony. Resume it or explicitly clear a definitely failed or expired ceremony before starting again.
4. Complete the callback
Serve /trust-graph/bind/callback on your allowed origin. Initialize the same adapters there and call complete(). The SDK checks the callback against the saved origin, path and bind identifier. Retain the completion capability before removing the fragment:
let completionProof = await app.loadCompletion();
if (window.location.hash) {
const completion = parseBindCallback(pending, window.location.href);
completionProof = completion.completionProof;
await app.saveCompletion(completionProof);
history.replaceState(null, '', window.location.pathname);
}
if (!completionProof) throw new Error('Waiting for the AuthenSee callback');Finalize with the saved key and both capabilities. The full module creates the signer from the local private key:
const bound = await finalizeBind(pending, {
completionProof,
publicKeyJwk: key.publicKeyJwk,
persistPending: value => app.savePending(value),
signRoot: record => signer.signFeedRecord(record),
refreshDeviceState: value => app.refreshDeviceState(value),
});
await app.saveBound(bound);
await app.clearCeremony();The signRoot adapter signs the initial device-authority record. The SDK currently calls this helper signFeedRecord; it is required for first-device binding when the server requires a root. Keep this callback even when your application only uses core graph operations. See device authority.
Exclude third-party analytics from the callback page. Show the returned approved, legacy, or quarantined state; continue to approval or eligible recovery before offering ordinary actions to a quarantined device.
5. Declare a vouch and sign the query
Have the user review the target and score before calling vouchAndVerify(targetGraphNodeId). The full module checks current device authority, then makes this real directed declaration on staging. The recipient does not need to accept or reciprocate it.
const certificate = {
signerGraphKeyId: graphNodeId,
subjectGraphKeyId: targetGraphNodeId,
trust: 80,
scope: 'identity' as const,
createdAt: new Date().toISOString(),
nonce: crypto.randomUUID(),
};
await graph.requestEdge({
...certificate,
signingGraphKeyId: graphKeyId,
signature: await signBrowserEdgeCertificate(local.privateKey, certificate),
});Query from your own persona's node. The authorization identifies your signing device separately:
const query = {
sourceGraphKeyId: graphNodeId,
targetGraphKeyId: targetGraphNodeId,
policy: { scope: 'identity' as const, minTrust: 50, maxHops: 3 },
};
const authorization = await signBrowserGraphRead(local.privateKey, {
operation: 'query-reachable', graphKeyId, graphNodeId, query,
});
const response = await graph.queryReachability({ ...query, authorization });A direct active vouch with score 80 satisfies the example's minimum of 50. Sign the exact query, and generate fresh read authorization for each retry.
6. Verify locally and use the checked result
Verify the attestation with the independently pinned JWKS and bundled circuit key. Use only the verified verdict; release the WASM verifier when finished.
const verifier = createZkVerifier();
try {
const result = await verifier.verifyAttestation({
verificationKey: wotPathVerificationKey(),
attestation: response.attestation,
jwks: pinnedAttestationJwks,
question: {
sourceGraphKeyId: query.sourceGraphKeyId,
targetGraphKeyId: query.targetGraphKeyId,
...query.policy,
},
});
if (!result.verified) throw new Error(`Answer rejected: ${result.code}`);
return result.verdict.reachable
? { label: 'Connected under your policy · proof verified', ...result.verdict }
: { label: 'No path reported under your policy · signed report', ...result.verdict };
} finally {
await verifier.destroy();
}Does not establish
The proof does not establish that the service published the complete graph. The service sees the graph and query. A proof does not establish graph completeness or latest state. A negative is a signed report, not a proof that no path exists.Full module
Implement BrowserApp, then call createBrowserQuickstart(app, pinnedAttestationJwks) to obtain the three UI actions. Each excerpt above is checked against this module and typechecked in its complete context.
Full module · browser integration
import {
beginBind,
parseBindCallback,
finalizeBind,
createGraphSigner,
createTrustGraphSdk,
generateBrowserGraphKey,
signBrowserEdgeCertificate,
signBrowserGraphRead,
type PendingBind,
type FinalizeBindInput,
type FinalizeBindResult,
} from '@rebellion-systems/trust-graph-sdk/browser';
import {
createZkVerifier,
wotPathVerificationKey,
} from '@rebellion-systems/trust-graph-sdk/zk';
type BrowserKey = Awaited<ReturnType<typeof generateBrowserGraphKey>>;
// Application adapters: IndexedDB storage and verified device authority.
export interface BrowserApp {
loadKey(): Promise<BrowserKey | null>;
saveKey(key: BrowserKey): Promise<void>;
loadPending(): Promise<PendingBind | null>;
savePending(pending: PendingBind): Promise<void>;
loadCompletion(): Promise<string | null>;
saveCompletion(completionProof: string): Promise<void>;
loadBound(): Promise<FinalizeBindResult | null>;
saveBound(bound: FinalizeBindResult): Promise<void>;
clearCeremony(): Promise<void>;
refreshDeviceState: FinalizeBindInput['refreshDeviceState'];
}
export function createBrowserQuickstart(
app: BrowserApp,
pinnedAttestationJwks: unknown, // Trusted app configuration, not response data.
) {
const serverOrigin = 'https://trust-staging.rebellion.systems';
const graph = createTrustGraphSdk({ serverUrl: serverOrigin });
async function start(recoverNodeId?: string): Promise<void> {
if (await app.loadPending()) {
throw new Error('Resume or explicitly clear the existing ceremony first');
}
let key = await app.loadKey();
if (!key) {
key = await generateBrowserGraphKey();
await app.saveKey(key); // Durably persist before any navigation.
}
const pending = await beginBind({
serverOrigin,
appOrigin: window.location.origin,
publicKeyJwk: key.publicKeyJwk,
...(recoverNodeId ? { recoverNodeId } : {}),
});
await app.savePending(pending);
window.location.assign(pending.hostedUrl);
}
async function complete(): Promise<FinalizeBindResult> {
const pending = await app.loadPending();
const key = await app.loadKey();
if (!pending || !key) throw new Error('Missing saved ceremony or device key');
let completionProof = await app.loadCompletion();
if (window.location.hash) {
const completion = parseBindCallback(pending, window.location.href);
completionProof = completion.completionProof;
await app.saveCompletion(completionProof);
history.replaceState(null, '', window.location.pathname);
}
if (!completionProof) throw new Error('Waiting for the AuthenSee callback');
const signer = createGraphSigner({
signatureFormat: 'ieee-p1363',
randomUUID: () => crypto.randomUUID(),
sign: async bytes => new Uint8Array(await crypto.subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' },
key.privateKey,
bytes as Uint8Array<ArrayBuffer>,
)),
});
const bound = await finalizeBind(pending, {
completionProof,
publicKeyJwk: key.publicKeyJwk,
persistPending: value => app.savePending(value),
signRoot: record => signer.signFeedRecord(record),
refreshDeviceState: value => app.refreshDeviceState(value),
});
await app.saveBound(bound);
await app.clearCeremony();
return bound;
}
async function vouchAndVerify(targetGraphNodeId: string) {
const local = await app.loadKey();
const bound = await app.loadBound();
if (!local || !bound) throw new Error('Bind this device first');
const state = await app.refreshDeviceState(bound.key);
if (state.state !== 'approved' && state.state !== 'legacy') {
throw new Error('This device is not currently authorized');
}
const { graphKeyId, graphNodeId } = bound.key;
const certificate = {
signerGraphKeyId: graphNodeId,
subjectGraphKeyId: targetGraphNodeId,
trust: 80,
scope: 'identity' as const,
createdAt: new Date().toISOString(),
nonce: crypto.randomUUID(),
};
await graph.requestEdge({
...certificate,
signingGraphKeyId: graphKeyId,
signature: await signBrowserEdgeCertificate(local.privateKey, certificate),
});
const query = {
sourceGraphKeyId: graphNodeId,
targetGraphKeyId: targetGraphNodeId,
policy: { scope: 'identity' as const, minTrust: 50, maxHops: 3 },
};
const authorization = await signBrowserGraphRead(local.privateKey, {
operation: 'query-reachable', graphKeyId, graphNodeId, query,
});
const response = await graph.queryReachability({ ...query, authorization });
const verifier = createZkVerifier();
try {
const result = await verifier.verifyAttestation({
verificationKey: wotPathVerificationKey(),
attestation: response.attestation,
jwks: pinnedAttestationJwks,
question: {
sourceGraphKeyId: query.sourceGraphKeyId,
targetGraphKeyId: query.targetGraphKeyId,
...query.policy,
},
});
if (!result.verified) throw new Error(`Answer rejected: ${result.code}`);
return result.verdict.reachable
? { label: 'Connected under your policy · proof verified', ...result.verdict }
: { label: 'No path reported under your policy · signed report', ...result.verdict };
} finally {
await verifier.destroy();
}
}
return { start, complete, vouchAndVerify };
}Resume and handle failures
After a transient finalize failure, retain the pending record and completion capability and call complete() again. The SDK validates the returned device against the saved key. Clear capabilities for a definitely expired or failed ceremony; choose whether to reuse or replace the key through your recovery flow.
Honor retry-after on rate limits. Treat a proving failure as unavailable, never as a positive without a proof. Do not turn verification failure into “not connected”. Because vouchAndVerify also writes a vouch, provide a separate query-only retry action in a production UI.
Continue with signed messages, trust-gated features, or attestation verification.
Positive answers use the UltraHonk ultrahonk-wot-path-v2 proof scheme; negative answers remain signed reports.