{"_id":"@bsatproto/oauth-client-browser","name":"@bsatproto/oauth-client-browser","dist-tags":{"latest":"0.3.27"},"versions":{"0.3.27":{"name":"@bsatproto/oauth-client-browser","version":"0.3.27","license":"MIT","description":"ATPROTO OAuth client for the browser (relies on WebCrypto & Indexed DB)","keywords":["atproto","oauth","client","browser","webcrypto","indexed","db"],"homepage":"https://atproto.com","repository":{"type":"git","url":"git+https://github.com/bluesky-social/atproto.git","directory":"packages/oauth/oauth-client-browser"},"type":"commonjs","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"dependencies":{"@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-labs/did-resolver":"npm:@bsatproto-labs/did-resolver@0.2.0","@atproto/did":"npm:@bsatproto/did@0.1.5","@atproto/oauth-client":"npm:@bsatproto/oauth-client@0.5.1","@atproto/jwk":"npm:@bsatproto/jwk@0.4.0","@atproto/jwk-webcrypto":"npm:@bsatproto/jwk-webcrypto@0.1.9","@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-browser@0.3.27","bugs":{"url":"https://github.com/bluesky-social/atproto/issues"},"_integrity":"sha512-aWfgS7kDdXc8uSkC/IgxRlC7fy/wvcpSfRYfvvhYyhrsq9sV6SDRTpUVpLvY6jyyGDTklmQB9dcOd8xCEmLO1Q==","_resolved":"/private/var/folders/cb/7sst87dj7jd40nc7b7q7h76r0000gn/T/efc6bab6e3c84c4845c914a2ac06f8f1/bsatproto-oauth-client-browser-0.3.27.tgz","_from":"file:bsatproto-oauth-client-browser-0.3.27.tgz","_nodeVersion":"22.11.0","_npmVersion":"10.9.0","dist":{"integrity":"sha512-aWfgS7kDdXc8uSkC/IgxRlC7fy/wvcpSfRYfvvhYyhrsq9sV6SDRTpUVpLvY6jyyGDTklmQB9dcOd8xCEmLO1Q==","shasum":"830befc6691c315513333761294dcfa8a120d76f","tarball":"https://registry.npmjs.org/@bsatproto/oauth-client-browser/-/oauth-client-browser-0.3.27.tgz","fileCount":63,"unpackedSize":110093,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCdv28aoGJvnqX+uudKdosfqzyTcbysfxvK6MRJXwDblQIgHmNsqSj0Osj0iOR+Jk3UONZ7ZoJTCxzfRSlSWNKDckE="}]},"_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-browser_0.3.27_1753457348718_0.9541660425717613"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-25T15:29:08.656Z","0.3.27":"2025-07-25T15:29:08.925Z","modified":"2025-07-25T15:29:09.410Z"},"maintainers":[{"name":"web3km","email":"web3km@proton.me"}],"description":"ATPROTO OAuth client for the browser (relies on WebCrypto & Indexed DB)","homepage":"https://atproto.com","keywords":["atproto","oauth","client","browser","webcrypto","indexed","db"],"repository":{"type":"git","url":"git+https://github.com/bluesky-social/atproto.git","directory":"packages/oauth/oauth-client-browser"},"bugs":{"url":"https://github.com/bluesky-social/atproto/issues"},"license":"MIT","readme":"# atproto OAuth Client for the Browser\n\nThis package provides a browser specific OAuth client implementation for\natproto. It implements all the OAuth features required by [ATPROTO] (PKCE, DPoP,\netc.).\n\n`@atproto/oauth-client-browser` is designed for front-end applications that do\nnot have a backend server to manage OAuth sessions, a.k.a \"Single Page\nApplications\" (SPA).\n\n> [!IMPORTANT]\n>\n> When a backend server is available, it is recommended to use\n> [`@atproto/oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node)\n> to manage OAuth sessions from the server side and use a session cookie to map\n> the OAuth session to the front-end. Because this mechanism allows the backend\n> to invalidate OAuth credentials at scale, this method is more secure than\n> managing OAuth sessions from the front-end directly. Thanks to the added\n> security, the OAuth server will provide longer lived tokens when issued to a\n> BFF (Backend-for-frontend).\n\n## Setup\n\n### Client ID\n\nThe `client_id` is what identifies your application to the OAuth server. It is\nused to fetch the client metadata and to initiate the OAuth flow. The\n`client_id` must be a URL that points to the [client\nmetadata](#client-metadata).\n\n### Client Metadata\n\nYour OAuth client metadata should be hosted at a URL that corresponds to the\n`client_id` of your application. This URL should return a JSON object with the\nclient metadata. The client metadata should be configured according to the\nneeds of your application and must respect the [ATPROTO] spec.\n\n```json\n{\n  // Must be the same URL as the one used to obtain this JSON object\n  \"client_id\": \"https://my-app.com/client-metadata.json\",\n  \"client_name\": \"My App\",\n  \"client_uri\": \"https://my-app.com\",\n  \"logo_uri\": \"https://my-app.com/logo.png\",\n  \"tos_uri\": \"https://my-app.com/tos\",\n  \"policy_uri\": \"https://my-app.com/policy\",\n  \"redirect_uris\": [\"https://my-app.com/callback\"],\n  \"scope\": \"atproto\",\n  \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n  \"response_types\": [\"code\"],\n  \"token_endpoint_auth_method\": \"none\",\n  \"application_type\": \"web\",\n  \"dpop_bound_access_tokens\": true\n}\n```\n\nThe client metadata is used to instantiate an OAuth client. There are two ways\nof doing this:\n\n1. Either you \"burn\" the metadata into your application:\n\n   ```typescript\n   import { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\n   const client = new BrowserOAuthClient({\n     clientMetadata: {\n       // Exact same JSON object as the one returned by the client_id URL\n     },\n     // ...\n   })\n   ```\n\n2. Or you load it asynchronously from the URL:\n\n   ```typescript\n   import { OAuthClient } from '@atproto/oauth-client-browser'\n\n   const client = await BrowserOAuthClient.load({\n     clientId: 'https://my-app.com/client-metadata.json',\n     // ...\n   })\n   ```\n\nIf performances are important to you, it is recommended to burn the metadata\ninto the script. Server side rendering techniques can also be used to inject the\nmetadata into the script at runtime.\n\n### Handle Resolver\n\nWhenever your application initiates an OAuth flow, it will start to resolve\nthe (user provider) APTROTO handle of the user. This is typically done though a\nDNS request. However, because DNS resolution is not available in the browser, a\nbackend service must be provided.\n\n> [!CAUTION]\n>\n> Using Bluesky-hosted services for handle resolution (eg, the `bsky.social`\n> endpoint) will leak both user IP addresses and handle identifiers to Bluesky,\n> a third party. While Bluesky has a declared privacy policy, both developers\n> and users of applications need to be informed and aware of the privacy\n> implications of this arrangement. Application developers are encouraged to\n> improve user privacy by operating their own handle resolution service when\n> possible. If you are a PDS self-hoster, you can use your PDS's URL for\n> `handleResolver`.\n\nIf a `string` or `URL` object is used as `handleResolver`, the library will\nexpect this value to be the URL of a service running the\n`com.atproto.identity.resolveHandle` XRPC Lexicon method.\n\n> [!TIP]\n>\n> If you host your own PDS, you can use its URL as a handle resolver.\n\n```typescript\nimport { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: 'https://my-pds.example.com',\n  // ...\n})\n```\n\nAlternatively, if a \"DNS over HTTPS\" (DoH) service is available, it can be used\nto resolve the handle. In this case, the `handleResolver` should be initialized\nwith a `AtprotoDohHandleResolver` instance:\n\n```typescript\nimport {\n  BrowserOAuthClient,\n  AtprotoDohHandleResolver,\n} from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: new AtprotoDohHandleResolver('https://my-doh.example.com'),\n  // ...\n})\n```\n\n### Other configuration options\n\nIn addition to [Client Metadata](#client-metadata) and [Handle\nResolver](#handle-resolver), the `BrowserOAuthClient` constructor accepts the\nfollowing optional configuration options:\n\n- `fetch`: A custom wrapper around the `fetch` function. This can be useful to\n  add custom headers, logging, or to use a different fetch implementation.\n  Defaults to `window.fetch`.\n\n- `responseMode`: `query` or `fragment`. Determines how the authorization\n  response is returned to the client. Defaults to `fragment`.\n\n- `plcDirectoryUrl`: The URL of the PLC directory. This will typically not be\n  needed unless you run an entire atproto stack locally. Defaults to\n  `https://plc.directory`.\n\n## Usage\n\nOnce the `client` is set up, it can be used to initiate & manage OAuth sessions.\n\n### Initializing the client\n\nThe client will manage the sessions for you. In order to do so, it must first\ninitialize itself. Note that this operation must be performed once (and **only\nonce**) whenever the web app is loaded.\n\n```typescript\nconst result: undefined | { session: OAuthSession; state?: string } =\n  await client.init()\n\nif (result) {\n  const { session, state } = result\n  if (state != null) {\n    console.log(\n      `${session.sub} was successfully authenticated (state: ${state})`,\n    )\n  } else {\n    console.log(`${session.sub} was restored (last active session)`)\n  }\n}\n```\n\nThe return value can be used to determine if the client was able to restore the\nlast used session (`session` is defined) or if the current navigation is the\nresult of an authorization redirect (both `session` and `state` are defined).\n\n### Initiating an OAuth flow\n\nIn order to initiate an OAuth flow, we must first determine which PDS the\nauthentication flow will be initiated from. This means that the user must\nprovide one of the following information:\n\n- The user's handle\n- The user's DID\n- A PDS/Entryway URL\n\nUsing that information, the OAuthClient will resolve all the needed information\nto initiate the OAuth flow, and redirect the user to the OAuth server.\n\n```typescript\ntry {\n  await client.signIn('my.handle.com', {\n    state: 'some value needed later',\n    prompt: 'none', // Attempt to sign in without user interaction (SSO)\n    ui_locales: 'fr-CA fr en', // Only supported by some OAuth servers (requires OpenID Connect support + i18n support)\n    signal: new AbortController().signal, // Optional, allows to cancel the sign in (and destroy the pending authorization, for better security)\n  })\n\n  console.log('Never executed')\n} catch (err) {\n  console.log('The user aborted the authorization process by navigating \"back\"')\n}\n```\n\nThe returned promise will never resolve (because the user will be redirected to\nthe OAuth server). The promise will reject if the user cancels the sign in\n(using an `AbortSignal`), or if the user navigates back from the OAuth server\n(because of browser's back-forward cache).\n\n### Handling the OAuth response\n\nWhen the user is redirected back to the application, the OAuth response will be\navailable in the URL. The `BrowserOAuthClient` will automatically detect the\nresponse and handle it when `client.init()` is called. Alternatively, the\napplication can manually handle the response using the\n`client.callback(urlQueryParams)` method.\n\n### Restoring a session\n\nThe client keeps track of all the sessions that it manages through an internal\nstore. Regardless of the session that was returned from the `client.init()`\ncall, any other session can be loaded using the `client.restore()` method. This\nmethod will throw an error if the session is no longer available or if it has\nbecome expired.\n\n```ts\nconst aliceSession = await client.restore('did:plc:alice')\nconst bobSession = await client.restore('did:plc:bob')\n```\n\nIn its current form, the client does not expose methods to list all sessions\nin its store. The app will have to keep track of those itself.\n\n### Watching for session invalidation\n\nThe client will emit events whenever a session becomes unavailable, allowing to\ntrigger global behaviors (e.g. show the login page).\n\n```ts\nclient.addEventListener(\n  'deleted',\n  (\n    event: CustomEvent<{\n      sub: string\n      cause: TokenRefreshError | TokenRevokedError | TokenInvalidError\n    }>,\n  ) => {\n    const { sub, cause } = event.detail\n    console.error(`Session for ${sub} is no longer available (cause: ${cause})`)\n  },\n)\n```\n\n## Usage with `@atproto/api`\n\nThe `@atproto/api` package provides a way to interact with multiple Bluesky\nspecific XRPC lexicons (`com.atproto`, `app.bsky`, `chat.bsky`, `tools.ozone`)\nthrough the `Agent` interface. The `oauthSession` returned by the\n`BrowserOAuthClient` can be used to instantiate an `Agent` instance.\n\n```typescript\nimport { Agent } from '@atproto/api'\n\nconst session = await client.restore('did:plc:alice')\n\nconst agent = new Agent(session)\n\nawait agent.getProfile({ actor: agent.accountDid })\n```\n\nAny refresh of the credentials will happen under the hood, and the new tokens\nwill be saved in the session store (in the browser's indexed DB).\n\n## Advances use-cases\n\n### Using in development (localhost)\n\nThe OAuth server must be able to fetch the `client_metadata` object. The best\nway to do this if you didn't already deployed your app is to use a tunneling\nservice like [ngrok](https://ngrok.com/).\n\nThe `client_id` will then be something like\n`https://<your-ngrok-id>.ngrok.io/<path_to_your_client_metadata>`.\n\nThere is however a special case for loopback clients. A loopback client is a\nclient that runs on `localhost`. In this case, the OAuth server will not be able\nto fetch the `client_metadata` object because `localhost` is not accessible from\nthe outside. To work around this, atproto OAuth servers are required to support\nthis case by providing an hard coded `client_metadata` object for the client.\n\nThis has several restrictions:\n\n1. There is no way of configuring the client metadata (name, logo, etc.)\n2. The validity of the refresh tokens (if any) will be very limited (typically 1\n   day)\n3. Silent-sign-in will not be allowed\n4. Only `http://127.0.0.1:<any_port>` and `http://[::1]:<any_port>` can be used\n   as origin for your app, and **not** `http://localhost:<any_port>`. This\n   library will automatically redirect the user to an IP based origin\n   (`http://127.0.0.1:<port>`) when visiting an origin with `localhost`.\n\nUsing a loopback client is only recommended for development purposes. A loopback\nclient can be instantiated like this:\n\n```typescript\nimport { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: 'https://bsky.social',\n  // Only works if the current origin is a loopback address:\n  clientMetadata: undefined,\n})\n```\n\nIf you need to use a special `redirect_uris`, you can configure them like this:\n\n```typescript\nimport { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: 'https://bsky.social',\n  // Note that the origin of the \"client_id\" URL must be \"http://localhost\" when\n  // using this configuration, regardless of the actual hostname (\"127.0.0.1\" or\n  // \"[::1]\"), port or pathname. Only the `redirect_uris` must contain the\n  // actual url that will be used to redirect the user back to the application.\n  clientMetadata: `http://localhost?redirect_uri=${encodeURIComponent('http://127.0.0.1:8080/callback')}`,\n})\n```\n\n[ATPROTO]: https://atproto.com/ 'AT Protocol'\n","readmeFilename":"README.md","_rev":"1-7be0a687d733042bde978d4557321acb"}