# Your first verified path

Bind a browser device, declare a vouch, query from your persona, and verify the answer locally.


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](#full-module); use its complete imports, checks, and application adapters when integrating.

## 1. Arrange access and configuration

[Request early access](/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.

```ts excerpt="browser-module"
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:

```ts excerpt="browser-module"
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](/concepts/identity) and the [devices API](/api/devices).


**Never**
Never infer approval from a completed bind or a parsed directory. A recovered device may be quarantined; the adapter must return its verified state.

## 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.

```ts excerpt="browser-module"
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:

```ts excerpt="browser-module"
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:

```ts excerpt="browser-module"
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](/api/devices).

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.

```ts excerpt="browser-module"
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:

```ts excerpt="browser-module"
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.

```ts excerpt="browser-module"
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();
}
```


**Establishes**
A verified positive proves a policy-satisfying path in the cited graph commitment, without revealing intermediate nodes or edges to the verifier.


**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

```ts sample="browser-module"
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](/build/signed-messages), [trust-gated features](/build/trust-gated-features), or [attestation verification](/api/attestations).

Positive answers use the UltraHonk `ultrahonk-wot-path-v2` proof scheme; negative answers remain signed reports.
