{"_id":"@bluesky-social/oauth-client","_rev":"2-7e63f3396b0dffdb20dad4f669011c6c","name":"@bluesky-social/oauth-client","dist-tags":{"latest":"0.5.7"},"versions":{"0.5.1":{"name":"@bluesky-social/oauth-client","version":"0.5.1","keywords":["atproto","oauth","client","isomorphic"],"license":"MIT","_id":"@bluesky-social/oauth-client@0.5.1","maintainers":[{"name":"openweb3.io","email":"mtsocialdao@gmail.com"},{"name":"web3km","email":"web3km@proton.me"}],"homepage":"https://atproto.com","bugs":{"url":"https://github.com/bluesky-social/atproto/issues"},"dist":{"shasum":"281d00ab329c2502db562630f17a5af397d876f1","tarball":"https://registry.npmjs.org/@bluesky-social/oauth-client/-/oauth-client-0.5.1.tgz","fileCount":147,"integrity":"sha512-pSvL/q5g4NSUbs4DBbSiD/DfXBMp5FpvQela8RXwBKerm9/Nof4fUEszfAnXGtzGFqg1eb1gloqfoLOOmJz3pg==","signatures":[{"sig":"MEYCIQDgmyY4vJoeMsy6dje6KvPfeLevtZkyVfgDvRNHQKpr3QIhAPhn10ABLn+urR2yA5Iar9DHb7guF1c/ED0qIXkMibtt","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":479750},"main":"dist/index.js","type":"commonjs","_from":"file:bluesky-social-oauth-client-0.5.1.tgz","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc --build tsconfig.build.json"},"_npmUser":{"name":"web3km","email":"web3km@proton.me"},"_resolved":"/private/var/folders/cb/7sst87dj7jd40nc7b7q7h76r0000gn/T/c3fb2f884018a4dc4ee5b652f850cd47/bluesky-social-oauth-client-0.5.1.tgz","_integrity":"sha512-pSvL/q5g4NSUbs4DBbSiD/DfXBMp5FpvQela8RXwBKerm9/Nof4fUEszfAnXGtzGFqg1eb1gloqfoLOOmJz3pg==","repository":{"url":"git+https://github.com/bluesky-social/atproto.git","type":"git","directory":"packages/oauth/oauth-client"},"_npmVersion":"10.8.2","description":"OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).","directories":{},"_nodeVersion":"18.20.8","dependencies":{"zod":"^3.23.8","multiformats":"^9.9.0","@atproto-labs/fetch":"0.2.3","@bluesky-social/did":"0.1.5","@bluesky-social/jwk":"0.4.0","@bluesky-social/xrpc":"0.7.1","@atproto-labs/did-resolver":"0.2.0","@atproto-labs/simple-store":"0.2.0","@bluesky-social/oauth-types":"0.4.0","@atproto-labs/handle-resolver":"0.3.0","@atproto-labs/identity-resolver":"0.3.0","@atproto-labs/simple-store-memory":"0.1.3"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.6.3"},"_npmOperationalInternal":{"tmp":"tmp/oauth-client_0.5.1_1753545368645_0.502897302965311","host":"s3://npm-registry-packages-npm-production"}},"0.5.7":{"name":"@bluesky-social/oauth-client","version":"0.5.7","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":{"core-js":"^3.46.0","multiformats":"^9.9.0","zod":"^3.25.76","@atproto-labs/did-resolver":"0.2.2","@atproto-labs/fetch":"0.2.3","@atproto-labs/identity-resolver":"0.3.2","@bluesky-social/did":"0.2.1","@bluesky-social/jwk":"0.6.0","@atproto-labs/simple-store":"0.3.0","@bluesky-social/xrpc":"0.7.5","@atproto-labs/handle-resolver":"0.3.2","@atproto-labs/simple-store-memory":"0.1.4","@bluesky-social/oauth-types":"0.4.2"},"devDependencies":{"typescript":"^5.9.3"},"scripts":{"build":"tsc --build tsconfig.build.json"},"_id":"@bluesky-social/oauth-client@0.5.7","bugs":{"url":"https://github.com/bluesky-social/atproto/issues"},"_integrity":"sha512-WxgABvkTtcdnKWQTT7vTchxYTqwxUUuzpt7VihcV5xCxHl4DLc+UJXoG2TNMuH5MNOmE0hHWlOoHEwkV3lHDRA==","_resolved":"/private/var/folders/cb/7sst87dj7jd40nc7b7q7h76r0000gn/T/7ade84f95c4a9d7b5c3fc12866ef6682/bluesky-social-oauth-client-0.5.7.tgz","_from":"file:bluesky-social-oauth-client-0.5.7.tgz","_nodeVersion":"20.15.0","_npmVersion":"10.7.0","dist":{"integrity":"sha512-WxgABvkTtcdnKWQTT7vTchxYTqwxUUuzpt7VihcV5xCxHl4DLc+UJXoG2TNMuH5MNOmE0hHWlOoHEwkV3lHDRA==","shasum":"88b3b19beefcf6349d41c156737bc5e444877eb5","tarball":"https://registry.npmjs.org/@bluesky-social/oauth-client/-/oauth-client-0.5.7.tgz","fileCount":147,"unpackedSize":508216,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDnF0ICQ+2Oa277iLii7cZsf43ngRUCjH0LY2wMlps9PAIhAMZy7Ug/sTR6cvJwjYgxAHNMvtD5R1OdJ+DGYBzbM4S2"}]},"_npmUser":{"name":"web3km","email":"web3km@proton.me"},"directories":{},"maintainers":[{"name":"openweb3.io","email":"mtsocialdao@gmail.com"},{"name":"web3km","email":"web3km@proton.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/oauth-client_0.5.7_1761039227071_0.15719892642378785"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-26T15:56:08.539Z","modified":"2025-10-21T09:33:47.500Z","0.5.1":"2025-07-26T15:56:08.837Z","0.5.7":"2025-10-21T09:33:47.279Z"},"bugs":{"url":"https://github.com/bluesky-social/atproto/issues"},"license":"MIT","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"},"description":"OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).","maintainers":[{"name":"openweb3.io","email":"mtsocialdao@gmail.com"},{"name":"web3km","email":"web3km@proton.me"}],"readme":"# @bluesky-social/oauth-client: atproto flavoured OAuth client\n\nCore library for implementing [atproto][ATPROTO] OAuth clients.\n\nFor a browser specific implementation, see [@bluesky-social/oauth-client-browser](https://www.npmjs.com/package/@bluesky-social/oauth-client-browser).\nFor a node specific implementation, see\n[@bluesky-social/oauth-client-node](https://www.npmjs.com/package/@bluesky-social/oauth-client-node).\n\n## Usage\n\n### Configuration\n\n```ts\nimport { OAuthClient, Key, Session } from '@bluesky-social/oauth-client'\nimport { JoseKey } from '@bluesky-social/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 \"@bluesky-social/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 \"@bluesky-social/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 `@bluesky-social/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 `@bluesky-social/api`) instance using the\n`OAuthSession` instance.\n\n```ts\nimport { Agent } from '@bluesky-social/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 `@bluesky-social/xrpc` package.\n\n```ts\nimport { Lexicons } from '@bluesky-social/lexicon'\nimport { OAuthClient } from '@bluesky-social/oauth-client' // or \"@bluesky-social/oauth-client-browser\" or \"@bluesky-social/oauth-client-node\"\nimport { XrpcClient } from '@bluesky-social/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 '@bluesky-social/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"}