> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shodai.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect an installed TypeScript client with delegated OAuth

> Sign a Shodai user into an installed Node.js client, restore their session, authenticate API calls, and disconnect safely.

For the complete documentation index, see [llms.txt](https://docs.shodai.network/llms.txt).

Use this workflow when you are building a Node.js CLI or desktop-style installed client in which a Shodai user signs in. The authenticated `ApiClient` calls the Agreements API directly with that user's delegated access token.

## Prerequisites

* Node.js `>=18`.
* A public OAuth client ID that is already registered in the Developer Portal.
* The exact loopback redirect `http://127.0.0.1/callback` registered for that client.
* The authorization-server issuer and Agreements API base URL for the same Shodai environment.

## Install the SDK

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @shodai-network/agreements-api-client
```

Import `ApiClient` from the package root and the delegated session types from the Node-only `/oauth` export.

## Create and restore the session

The following example stores the complete rotated token set in an application-owned file. Replace these file operations with your platform's secure storage when appropriate.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { ApiClient } from '@shodai-network/agreements-api-client';
import {
  OauthDelegatedSession,
  type OauthDelegatedTokenSet,
} from '@shodai-network/agreements-api-client/oauth';

const clientId = requiredEnv('OAUTH_CLIENT_ID');
const issuer = requiredEnv('OAUTH_ISSUER_URL');
const apiBaseUrl = requiredEnv('EXTERNAL_API_BASE_URL');
const tokenPath = resolve(process.env.SHODAI_TOKEN_PATH ?? '.shodai-oauth-session.json');

async function loadTokens(): Promise<OauthDelegatedTokenSet | undefined> {
  try {
    return JSON.parse(await readFile(tokenPath, 'utf8')) as OauthDelegatedTokenSet;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
    throw error;
  }
}

async function saveTokens(tokens: OauthDelegatedTokenSet): Promise<void> {
  await mkdir(dirname(tokenPath), { recursive: true });
  await writeFile(tokenPath, `${JSON.stringify(tokens, null, 2)}\n`, { mode: 0o600 });
}

async function clearTokens(): Promise<void> {
  await rm(tokenPath, { force: true });
}

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Set ${name} before running this client.`);
  return value;
}

const session = new OauthDelegatedSession({
  clientId,
  issuer,
  scope: 'agreements.read agreements.write',
  onTokensUpdated: saveTokens,
  onTokensCleared: clearTokens,
});

const savedTokens = await loadTokens();
if (savedTokens) {
  session.restoreTokens(savedTokens);
}

if (process.argv.includes('--disconnect')) {
  if (!savedTokens) {
    console.log('No saved session to disconnect.');
  } else {
    try {
      await session.revoke();
      console.log('Disconnected and cleared the saved session.');
    } catch (error) {
      console.error('Local token cleanup or remote revocation was not confirmed.', error);
      process.exitCode = 1;
    }
  }
} else {
  if (!savedTokens) {
    await session.loginWithLoopback();
  }

  const client = new ApiClient({
    baseUrl: apiBaseUrl,
    tokenProvider: session.tokenProvider(),
  });

  const agreements = await client.listAgreements({ limit: 5 });
  console.log(`Signed in; ${agreements.data.length} agreement(s) returned.`);
}
```

Set the environment-matched values, then run the application. On the first run, `loginWithLoopback()` opens the consent page and waits for the registered loopback redirect. Later runs restore the saved token set, and `tokenProvider()` refreshes an expired access token when a refresh token is available.

Run the same application with `--disconnect` when the user disconnects.

## Confirm the result

A successful first run opens the consent page, saves the delegated token set after authorization, and prints an authenticated Agreements API result. A later run reuses the saved session without opening the consent page.

The lifecycle callbacks are both required for durable sessions:

* `onTokensUpdated` saves the complete token set after login and refresh-token rotation.
* `restoreTokens()` restores that saved set before the first API call.
* `onTokensCleared` removes the saved set when `revoke()` disconnects the user.
* `revoke()` clears in-memory state and attempts persisted-state deletion even when server-side revocation cannot be confirmed.

Treat a rejected `revoke()` as an incomplete disconnect: local storage deletion may have failed, remote revocation may be unconfirmed, or both failures may be present in an `AggregateError`. Keep the error visible to the user or operator instead of reporting an unconditional successful disconnect.

For constructor fields and exported symbols, see the [TypeScript client reference](/sdks/typescript-client).
