{"_id":"@bsatproto/oauth-client","name":"@bsatproto/oauth-client","dist-tags":{"latest":"0.5.1"},"versions":{"0.5.1":{"name":"@bsatproto/oauth-client","version":"0.5.1","license":"MIT","description":"OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).","keywords":["atproto","oauth","client","isomorphic"],"homepage":"https://atproto.com","repository":{"type":"git","url":"git+https://github.com/bluesky-social/atproto.git","directory":"packages/oauth/oauth-client"},"type":"commonjs","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"dependencies":{"multiformats":"^9.9.0","zod":"^3.23.8","@atproto-labs/did-resolver":"npm:@bsatproto-labs/did-resolver@0.2.0","@atproto-labs/fetch":"npm:@bsatproto-labs/fetch@0.2.3","@atproto-labs/identity-resolver":"npm:@bsatproto-labs/identity-resolver@0.3.0","@atproto/did":"npm:@bsatproto/did@0.1.5","@atproto-labs/simple-store-memory":"npm:@bsatproto-labs/simple-store-memory@0.1.3","@atproto-labs/simple-store":"npm:@bsatproto-labs/simple-store@0.2.0","@atproto-labs/handle-resolver":"npm:@bsatproto-labs/handle-resolver@0.3.0","@atproto/jwk":"npm:@bsatproto/jwk@0.4.0","@atproto/xrpc":"npm:@bsatproto/xrpc@0.7.1","@atproto/oauth-types":"npm:@bsatproto/oauth-types@0.4.0"},"devDependencies":{"typescript":"^5.6.3"},"scripts":{"build":"tsc --build tsconfig.build.json"},"_id":"@bsatproto/oauth-client@0.5.1","bugs":{"url":"https://github.com/bluesky-social/atproto/issues"},"_integrity":"sha512-xz9QG1JIHVkHLIfPMi37Eh7zD5UDjNKxIzBQrKDDH8EuJIqwf+jBgHAvQ4m45l0Jw6XEpRork3RV68wrxcOjfg==","_resolved":"/private/var/folders/cb/7sst87dj7jd40nc7b7q7h76r0000gn/T/de2cf858c755e43ac6118a8ed663fea2/bsatproto-oauth-client-0.5.1.tgz","_from":"file:bsatproto-oauth-client-0.5.1.tgz","_nodeVersion":"22.11.0","_npmVersion":"10.9.0","dist":{"integrity":"sha512-xz9QG1JIHVkHLIfPMi37Eh7zD5UDjNKxIzBQrKDDH8EuJIqwf+jBgHAvQ4m45l0Jw6XEpRork3RV68wrxcOjfg==","shasum":"0a336ce27a853face143dabd81a89ebd48b1c14e","tarball":"https://registry.npmjs.org/@bsatproto/oauth-client/-/oauth-client-0.5.1.tgz","fileCount":147,"unpackedSize":478957,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHxZi//6/zjJYLcr1D5JuYYa4ubswhjoGqqvOLOtzoIJAiBGT9PvM83jcMGaJD1iNtFA2cA7olKDW8WVUIvlvmZF7Q=="}]},"_npmUser":{"name":"web3km","email":"web3km@proton.me"},"directories":{},"maintainers":[{"name":"web3km","email":"web3km@proton.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/oauth-client_0.5.1_1753457349245_0.8503235011134331"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-25T15:29:09.135Z","0.5.1":"2025-07-25T15:29:09.419Z","modified":"2025-07-25T15:29:10.052Z"},"maintainers":[{"name":"web3km","email":"web3km@proton.me"}],"description":"OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).","homepage":"https://atproto.com","keywords":["atproto","oauth","client","isomorphic"],"repository":{"type":"git","url":"git+https://github.com/bluesky-social/atproto.git","directory":"packages/oauth/oauth-client"},"bugs":{"url":"https://github.com/bluesky-social/atproto/issues"},"license":"MIT","readme":"# @atproto/oauth-client: atproto flavoured OAuth client\n\nCore library for implementing [atproto][ATPROTO] OAuth clients.\n\nFor a browser specific implementation, see [@atproto/oauth-client-browser](https://www.npmjs.com/package/@atproto/oauth-client-browser).\nFor a node specific implementation, see\n[@atproto/oauth-client-node](https://www.npmjs.com/package/@atproto/oauth-client-node).\n\n## Usage\n\n### Configuration\n\n```ts\nimport { OAuthClient, Key, Session } from '@atproto/oauth-client'\nimport { JoseKey } from '@atproto/jwk-jose' // NodeJS/Browser only\n\nconst client = new OAuthClient({\n  handleResolver: 'https://my-backend.example', // backend instances should use a DNS based resolver\n  responseMode: 'query', // or \"fragment\" (frontend only) or \"form_post\" (backend only)\n\n  // These must be the same metadata as the one exposed on the\n  // \"client_id\" endpoint (except when using a loopback client)\n  clientMetadata: {\n    client_id: 'https://my-app.example/atproto-oauth-client.json',\n    jwks_uri: 'https://my-app.example/jwks.json',\n  },\n\n  runtimeImplementation: {\n    // A runtime specific implementation of the crypto operations needed by the\n    // OAuth client. See \"@atproto/oauth-client-browser\" for a browser specific\n    // implementation. The following example is suitable for use in NodeJS.\n\n    createKey(algs: string[]): Promise<Key> {\n      // algs is an ordered array of preferred algorithms (e.g. ['RS256', 'ES256'])\n\n      // Note, in browser environments, it is better to use non extractable keys\n      // to prevent the private key from being stolen. This can be done using\n      // the WebcryptoKey class from the \"@atproto/jwk-webcrypto\" package. The\n      // inconvenient of these keys (which is also what makes them stronger) is\n      // that the only way to persist them across browser reloads is to save\n      // them in the indexed DB.\n      return JoseKey.generate(algs)\n    },\n\n    getRandomValues(length: number): Uint8Array | PromiseLike<Uint8Array> {\n      return crypto.getRandomValues(new Uint8Array(length))\n    },\n\n    digest(\n      bytes: Uint8Array,\n      algorithm: { name: string },\n    ): Uint8Array | PromiseLike<Uint8Array> {\n      // sha256 is required. Unsupported algorithms should throw an error.\n\n      if (algorithm.name.startsWith('sha')) {\n        const subtleAlgo = `SHA-${algorithm.name.slice(3)}`\n        const buffer = await crypto.subtle.digest(subtleAlgo, bytes)\n        return new Uint8Array(buffer)\n      }\n\n      throw new TypeError(`Unsupported algorithm: ${algorithm.name}`)\n    },\n\n    requestLock: <T>(\n      name: string,\n      fn: () => T | PromiseLike<T>,\n    ): Promise<T> => {\n      // This function is used to prevent concurrent refreshes of the same\n      // credentials. It is important to ensure that only one refresh is done at\n      // a time to prevent the sessions from being revoked.\n\n      // The following example shows a simple in-memory lock. In a real\n      // application, you should use a more robust solution (e.g. a system wide\n      // lock manager). Note that not providing a lock will result in an\n      // in-memory lock to be used (DO NOT copy-paste the following code).\n\n      declare const locks: Map<string, Promise<void>>\n\n      const current = locks.get(name) || Promise.resolve()\n      const next = current\n        .then(fn)\n        .catch(() => {})\n        .finally(() => {\n          if (locks.get(name) === next) locks.delete(name)\n        })\n\n      locks.set(name, next)\n      return next\n    },\n  },\n\n  stateStore: {\n    // A store for saving state data while the user is being redirected to the\n    // authorization server.\n\n    set(key: string, internalState: InternalStateData): Promise<void> {\n      throw new Error('Not implemented')\n    },\n    get(key: string): Promise<InternalStateData | undefined> {\n      throw new Error('Not implemented')\n    },\n    del(key: string): Promise<void> {\n      throw new Error('Not implemented')\n    },\n  },\n\n  sessionStore: {\n    // A store for saving session data.\n\n    set(sub: string, session: Session): Promise<void> {\n      throw new Error('Not implemented')\n    },\n    get(sub: string): Promise<Session | undefined> {\n      throw new Error('Not implemented')\n    },\n    del(sub: string): Promise<void> {\n      throw new Error('Not implemented')\n    },\n  },\n\n  keyset: [\n    // For backend clients only, a list of private keys to use for signing\n    // credentials. These keys MUST correspond to the public keys exposed on the\n    // \"jwks_uri\" of the client metadata. Note that the jwks JSON corresponding\n    // to the following keys can be obtained using the `client.jwks` getter.\n    await JoseKey.fromImportable(process.env.PRIVATE_KEY_1),\n    await JoseKey.fromImportable(process.env.PRIVATE_KEY_2),\n    await JoseKey.fromImportable(process.env.PRIVATE_KEY_3),\n  ],\n})\n```\n\n### Authentication\n\n```ts\nconst url = await client.authorize('foo.bsky.team', {\n  state: '434321',\n  prompt: 'consent',\n  scope: 'email',\n  ui_locales: 'fr',\n})\n```\n\nMake user visit `url`. Then, once it was redirected to the callback URI, perform the following:\n\n```ts\n// Parse the query params from the callback URI\nconst params = new URLSearchParams('code=...&state=...')\n\n// Process the callback using the OAuth client\nconst result = await client.callback(params)\n\n// Verify the state (e.g. to link to an internal user)\nresult.state === '434321' // true\n\nconst oauthSession = result.session\n```\n\nThe sign-in process results in an `OAuthSession` instance that can be used to make\nauthenticated requests to the resource server. This instance will automatically\nrefresh the credentials when needed.\n\n### Making authenticated requests\n\nThe `OAuthSession` instance obtained after signing in can be used to make\nauthenticated requests to the user's PDS. There are two main use-cases:\n\n1. Making authenticated request to Bluesky's AppView in order to fetch and\n   manipulate data from the `app.bsky` lexicon.\n\n2. Making authenticated request to your own AppView, in order to fetch and\n   manipulate data from your own lexicon.\n\n#### Making authenticated requests to Bluesky's AppView\n\nThe `@atproto/oauth-client` package provides a `OAuthSession` class that can be\nused to make authenticated requests to Bluesky's AppView. This can be achieved\nby constructing an `Agent` (from `@atproto/api`) instance using the\n`OAuthSession` instance.\n\n```ts\nimport { Agent } from '@atproto/api'\n\nconst agent = new Agent(oauthSession)\n\n// Make an authenticated request to the server. New credentials will be\n// automatically fetched if needed (causing sessionStore.set() to be called).\nawait agent.post({\n  text: 'Hello, world!',\n})\n\n// revoke credentials on the server (causing sessionStore.del() to be called)\nawait agent.signOut()\n```\n\n#### Making authenticated requests to your own AppView\n\nThe `OAuthSession` instance obtained after signing in can be used to instantiate\nthe `XrpcClient` class from the `@atproto/xrpc` package.\n\n```ts\nimport { Lexicons } from '@atproto/lexicon'\nimport { OAuthClient } from '@atproto/oauth-client' // or \"@atproto/oauth-client-browser\" or \"@atproto/oauth-client-node\"\nimport { XrpcClient } from '@atproto/xrpc'\n\n// Define your lexicons\nconst myLexicon = new Lexicons([\n  {\n    lexicon: 1,\n    id: 'com.example.query',\n    defs: {\n      main: {\n        // ...\n      },\n    },\n  },\n])\n\n// Describe your app's oauth client\nconst oauthClient = new OAuthClient({\n  // ...\n})\n\n// Authenticate the user\nconst oauthSession = await oauthClient.restore('did:plc:123')\n\n// Instantiate a client using the `oauthSession` as fetch handler object\nconst client = new XrpcClient(oauthSession, myLexicon)\n\n// Make authenticated calls\nconst response = await client.call('com.example.query')\n```\n\nNote that the user's PDS might not know about your lexicon, or what to do with\nthose calls (PDS' are only mandated to implement the `com.atproto` lexicon). In\norder to process your calls, you need to have a backend that will process those\ncalls. You can then instruct your PDS to forward those calls to your backend.\n\n```ts\nconst response = await client.call(\n  'com.example.query',\n  {\n    // Params\n  },\n  {\n    headers: {\n      // The PDS will proxy calls to the specified service in did:plc:xyz's did document.\n      // These calls will be authenticated using \"service auth\", a single use JWT Bearer token, signed with the logged-in user's private key.\n      'atproto-proxy': 'did:plc:xyz#serviceId',\n    },\n  },\n)\n```\n\nYou can also instantiate the `XrpcClient` class with a custom `fetch` function\nthat will provide the `atproto-proxy` header on all calls:\n\n```ts\nconst boundClient = new XrpcClient((url, init) => {\n  const headers = new Headers(init?.headers)\n\n  // Add the atproto-proxy header if it is not already present\n  if (!headers.has('atproto-proxy')) {\n    headers.set('atproto-proxy', 'did:plc:xyz#serviceId')\n  }\n\n  return oauthSession.fetchHandler(url, { ...init, headers })\n}, myLexicon)\n\n// No need to specify the atproto-proxy header anymore\nconst response = await boundClient.call('com.example.query')\n```\n\n> [!NOTE]\n>\n> Proxying every call through the PDS is not recommended for performance\n> reasons, as it will increase the latency of readonly calls to your lexicon.\n> Doing so will also prevent your backend from being able to anticipate writes\n> on the network. Indeed, write calls will be sent to the PDS, which will then\n> propagate them on the network through a relay (a.k.a. \"firehose\"). This will\n> introduce a delay between the time the write is made and the time it is\n> processed by your backend.\n>\n> In order to avoid those issues, it is recommended that you implement your\n> backend using a backend-for-frontend pattern. This backend will be responsible\n> for processing the calls made by the client, and will be able to anticipate\n> writes on the network.\n>\n> Read more about the backend-for-frontend pattern in the [atproto][ATPROTO]\n> documentation website.\n\n## Advances use-cases\n\n### Listening for session updates and deletion\n\nThe `OAuthClient` will emit events whenever a session is updated or deleted.\n\n```ts\nimport {\n  Session,\n  TokenRefreshError,\n  TokenRevokedError,\n} from '@atproto/oauth-client'\n\nclient.addEventListener('updated', (event: CustomEvent<Session>) => {\n  console.log('Refreshed tokens were saved in the store:', event.detail)\n})\n\nclient.addEventListener(\n  'deleted',\n  (\n    event: CustomEvent<{\n      sub: string\n      cause: TokenRefreshError | TokenRevokedError | unknown\n    }>,\n  ) => {\n    console.log('Session was deleted from the session store:', event.detail)\n\n    const { cause } = event.detail\n\n    if (cause instanceof TokenRefreshError) {\n      // - refresh_token unavailable or expired\n      // - oauth response error (`cause.cause instanceof OAuthResponseError`)\n      // - session data does not match expected values returned by the OAuth server\n    } else if (cause instanceof TokenRevokedError) {\n      // Session was revoked through:\n      // - agent.signOut()\n      // - client.revoke(sub)\n    } else {\n      // An unexpected error occurred, causing the session to be deleted\n    }\n  },\n)\n```\n\n### Force user to re-authenticate\n\n```ts\nconst url = await client.authorize(handle, {\n  prompt: 'login',\n  state,\n})\n```\n\nor\n\n```ts\nconst url = await client.authorize(handle, {\n  state,\n})\n```\n\n### Silent Sign-In\n\nUsing silent sign-in requires to handle retries on the callback endpoint.\n\n```ts\nasync function createLoginUrl(handle: string, state?: string): string {\n  return client.authorize(handle, {\n    state,\n    // Use \"prompt=none\" to attempt silent sign-in\n    prompt: 'none',\n  })\n}\n\nasync function handleCallback(params: URLSearchParams) {\n  try {\n    return await client.callback(params)\n  } catch (err) {\n    // Silent sign-in failed, retry without prompt=none\n    if (\n      err instanceof OAuthCallbackError &&\n      ['login_required', 'consent_required'].includes(err.params.get('error'))\n    ) {\n      // Do *not* use prompt=none when retrying (to avoid infinite redirects)\n      const url = await client.authorize(handle, { state: err.state })\n\n      // Allow calling code to catch the error and redirect the user to the new URL\n      return new MyLoginRequiredError(url)\n    }\n\n    throw err\n  }\n}\n```\n\n[ATPROTO]: https://atproto.com/ 'AT Protocol'\n","readmeFilename":"README.md","_rev":"1-b3e1be51f2031de76f8fbe52d0e458d5"}