diff --git a/.changeset/nylas-connect-readme-rewrite.md b/.changeset/nylas-connect-readme-rewrite.md
new file mode 100644
index 0000000..c008e9b
--- /dev/null
+++ b/.changeset/nylas-connect-readme-rewrite.md
@@ -0,0 +1,5 @@
+---
+"@nylas/connect": patch
+---
+
+Rewrite the README. The previous version's first example read the mailbox address from `result.email`, which does not exist on `ConnectResult`, and its config table documented 6 of the 11 available options.
diff --git a/packages/nylas-connect/README.md b/packages/nylas-connect/README.md
index 9bf410c..c2aa725 100644
--- a/packages/nylas-connect/README.md
+++ b/packages/nylas-connect/README.md
@@ -1,341 +1,311 @@
-# @nylas/connect
+
+
+
+
-[](https://www.npmjs.com/package/@nylas/connect)
-[](https://www.typescriptlang.org/)
-[](https://opensource.org/licenses/MIT)
+
Nylas Connect for JavaScript
-> ๐ Modern, secure, developer-friendly OAuth connection for Nylas APIs
+
+ Connect a user's mailbox to your app without building the OAuth flow
+
-## Highlights
+
+
+
+
+
+
-- **๐ Secure by default** - PKCE flow, automatic token management, no secrets in browser
-- **โก Zero dependencies** - Lightweight, fast, and reliable
-- **๐ฏ TypeScript-first** - Full type safety and IntelliSense support
-- **๐ Universal** - Works in modern browsers and Node.js 18+
-- **๐ฑ Flexible flows** - Popup (recommended) or redirect authentication
-- **๐พ Smart persistence** - Automatic session and token storage
+
+ ๐ Library guide ยท
+ ๐ API Reference ยท
+ ๐ Sign up ยท
+ ๐ก Samples ยท
+ ๐ฌ Forum
+
+
-## Install
+
-```bash
-npm install @nylas/connect
-```
+`@nylas/connect` runs the OAuth flow that connects a user's mailbox to your app, so you don't write a redirect route, a callback handler, a code-for-token exchange, or refresh logic. Paired with your identity provider, it also lets you keep addressing that user by the ID your app already has, instead of storing a Nylas grant ID alongside it. Works across Gmail, Microsoft, IMAP, iCloud, and the other providers behind [Nylas](https://developer.nylas.com/docs/v3/).
-**Prerequisites:** Node.js 18+ and a modern browser
+This repository is for contributors and anyone installing from source. If you just want to use the library in your app, head straight to the [**library guide**](https://developer.nylas.com/docs/v3/auth/nylas-connect/) on developer.nylas.com.
-## Usage
+## Get started
-```typescript
-import { NylasConnect } from '@nylas/connect';
+1. [Sign up for a free Nylas account](https://dashboard-v3.nylas.com/register) and grab your client ID from the [Nylas Dashboard](https://dashboard-v3.nylas.com/).
+2. Register your app's origin and callback URI under **Hosted Authentication**, so the browser flow is allowed to run.
+3. Install the package and connect your first mailbox โ see below.
-const nylasConnect = new NylasConnect({
- clientId: 'your-nylas-client-id',
- redirectUri: 'http://localhost:3000/auth/callback'
-});
+The [quickstart](https://developer.nylas.com/docs/v3/getting-started/nylas-connect/) walks through a working setup end to end, including which of the 3 configurations fits your app.
-// Connect with popup (recommended)
-const result = await nylasConnect.connect({ method: 'popup' });
-console.log('Connected as:', result.email);
-```
+## โ๏ธ Install
-Environment variables (recommended):
+> **Requirements:** a modern browser, or Node.js 22+. Zero runtime dependencies.
-```typescript
-// Use environment variables
-const nylasConnect = new NylasConnect();
-// Reads from NYLAS_CLIENT_ID and NYLAS_REDIRECT_URI
+```bash
+npm install @nylas/connect
+# or
+pnpm add @nylas/connect
```
-## Connection Methods
+The package ships its own TypeScript types. It's ESM-only.
-### Popup Flow (Recommended)
+React apps should install [`@nylas/react`](https://www.npmjs.com/package/@nylas/react) instead, which wraps this client in a `useNylasConnect` hook and a `NylasConnectButton` component.
-```typescript
-const result = await nylasConnect.connect({ method: 'popup' });
+To install from source:
+
+```bash
+git clone https://github.com/nylas/javascript.git
+cd javascript
+pnpm install
```
-- User stays in your app
-- Seamless experience
-- Best for SPAs
+### Runtime support
-### Redirect Flow
+Runs in modern browsers and in Node.js 22+. The popup flow needs a browser, since it drives a popup window and reads `localStorage`; the redirect flow and `callback(url)` both work server-side.
-```typescript
-const url = await nylasConnect.connect({ method: 'inline' });
-window.location.href = url;
-```
+The library calls Web Crypto (`crypto.subtle`), `fetch`, and `btoa`/`atob` as globals, with no polyfill and no `node:` imports โ which is why Node 22 is the floor rather than 18.
-- Full page redirect
-- Works when popups blocked
-- Better for mobile
+## โก๏ธ Usage
-### Callback Handler
+You drive everything through an instance of `NylasConnect`. Initialize it with your client ID and the URI Nylas redirects back to โ both fall back to `NYLAS_CLIENT_ID` and `NYLAS_REDIRECT_URI`, so `new NylasConnect()` with no arguments works once those are set.
```typescript
-// At your redirect URI (e.g., /auth/callback)
-await nylasConnect.callback();
-```
+import { NylasConnect } from "@nylas/connect";
-## Environment Setup
-
-```env
-NYLAS_CLIENT_ID=your-nylas-client-id
-NYLAS_REDIRECT_URI=http://localhost:3000/auth/callback
+const nylasConnect = new NylasConnect({
+ clientId: process.env.NYLAS_CLIENT_ID,
+ redirectUri: "http://localhost:3000/auth/callback",
+ apiUrl: "https://api.us.nylas.com", // or https://api.eu.nylas.com
+});
```
-**Note:** With modern bundlers, prefix environment variables:
-- Vite: `VITE_NYLAS_CLIENT_ID`
-- Next.js: `NEXT_PUBLIC_NYLAS_CLIENT_ID`
+Bundlers need their own prefix: `VITE_NYLAS_CLIENT_ID` for Vite, `NEXT_PUBLIC_NYLAS_CLIENT_ID` for Next.js.
-## Session Management
+Once initialized, connect a mailbox and read from it:
```typescript
-// Check current session
-const session = await nylasConnect.getSession();
-if (session) {
- console.log('User:', session.grantInfo?.email);
-}
+// Opens a popup, completes the PKCE exchange, stores the tokens.
+const result = await nylasConnect.connect({ method: "popup" });
+console.log("Connected:", result.grantInfo?.email);
-// Logout
-await nylasConnect.logout();
+// Authorize with the returned access token and address the mailbox as `me`.
+const res = await fetch("https://api.us.nylas.com/v3/grants/me/messages?limit=5", {
+ headers: { Authorization: `Bearer ${result.accessToken}` },
+});
```
-## Error Handling
+The mailbox address is on `result.grantInfo`, not on `result` itself.
+
+This runs entirely in the browser, so it authorizes with the `accessToken` that `connect()` returns โ never an API key, which would be inlined into your bundle. An access token covers grant-level data like the request above; application-level requests still need an API key, and those belong on your server.
+
+### Connection methods
```typescript
-try {
- await nylasConnect.connect({ method: 'popup' });
-} catch (error) {
- console.error('Connection failed:', error.message);
-}
-```
+// Popup: user stays on your page. Best for SPAs.
+const result = await nylasConnect.connect({ method: "popup" });
-## Configuration
+// Inline: full-page redirect. Better for mobile, works when popups are blocked.
+const url = await nylasConnect.connect({ method: "inline" });
+window.location.href = url;
-| Option | Type | Default | Description |
-|--------|------|---------|-------------|
-| `clientId` | `string` | - | Nylas Client ID |
-| `redirectUri` | `string` | - | OAuth redirect URI |
-| `apiUrl` | `string` | `https://api.us.nylas.com` | API base URL |
-| `persistTokens` | `boolean` | `true` | Store tokens in localStorage |
-| `debug` | `boolean` | `true` on localhost | Enable debug logging |
-| `codeExchange` | (param: CodeExchangeParams) => Promise` | - | Custom code exchange method |
+// At your redirectUri, complete the exchange.
+await nylasConnect.callback();
+```
-## Custom Code Exchange
+`provider` accepts `google`, `microsoft`, `imap`, and `icloud`. Omit it and the user picks their own from the Nylas login screen.
-For enhanced security, you can handle the OAuth code exchange on your backend instead of in the browser. This approach keeps your API keys secure and gives you full control over the token exchange process.
+### Keep your own user IDs
-### Backend Code Exchange
+Pass `identityProviderToken` and the grant is linked to the `sub` claim in your identity provider's JWT. You then address the mailbox as `/v3/grants/me` with your own user ID in a header, so there's no `grant_id` column and no join table.
```typescript
const nylasConnect = new NylasConnect({
- clientId: 'your-nylas-client-id',
- redirectUri: 'http://localhost:3000/auth/callback',
- codeExchange: async (params) => {
- // Send the authorization code to your backend
- const response = await fetch('/api/auth/exchange', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- code: params.code,
- state: params.state,
- clientId: params.clientId,
- redirectUri: params.redirectUri,
- scopes: params.scopes,
- provider: params.provider,
- }),
- });
+ clientId: process.env.NYLAS_CLIENT_ID,
+ redirectUri: "http://localhost:3000/auth/callback",
+ identityProviderToken: async () => auth0.getTokenSilently(),
+});
- if (!response.ok) {
- throw new Error(`Token exchange failed: ${response.statusText}`);
- }
+await nylasConnect.connect({ method: "popup" });
- const tokenData = await response.json();
-
- // Return the expected ConnectResult format
- return {
- accessToken: tokenData.access_token,
- idToken: tokenData.id_token,
- grantId: tokenData.grant_id,
- expiresAt: Date.now() + tokenData.expires_in * 1000,
- scope: tokenData.scope,
- grantInfo: tokenData.grant_info,
- };
- }
+// The same user ID your app already uses, everywhere.
+const res = await fetch("https://api.us.nylas.com/v3/grants/me/messages", {
+ headers: {
+ Authorization: `Bearer ${await auth0.getTokenSilently()}`,
+ "X-Nylas-External-User-Id": user.sub,
+ },
});
-
-// Use normally - the custom exchange will be called automatically
-const result = await nylasConnect.connect({ method: 'popup' });
```
-### Backend Implementation Example
-
-```typescript
-// Example backend endpoint (/api/auth/exchange)
-export async function POST(request: Request) {
- const { code, clientId, redirectUri } = await request.json();
-
- // Exchange code for tokens using your API key
- const response = await fetch('https://api.us.nylas.com/connect/token', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Authorization': `Bearer ${process.env.NYLAS_API_KEY}`,
- },
- body: new URLSearchParams({
- client_id: clientId,
- redirect_uri: redirectUri,
- code,
- grant_type: 'authorization_code',
- }),
- });
-
- const tokenData = await response.json();
- return Response.json(tokenData);
-}
-```
+The callback runs during the token exchange and the JWT is sent to Nylas as `idp_claims`. Return a fresh token each time; most identity provider SDKs refresh for you. Returning `null` continues without claims, and throwing fails the exchange with a `NETWORK_ERROR` event.
-## API
+Works with Auth0, Clerk, Google Identity, WorkOS, and any provider exposing a JSON Web Key Set endpoint. Setup guides for each: [external identity providers](https://developer.nylas.com/docs/v3/auth/nylas-connect/use-external-idp/).
-### `connect(options?)`
+Without an identity provider you still skip the OAuth plumbing, but you store the grant ID yourself. See [backend callback handling](https://developer.nylas.com/docs/v3/auth/nylas-connect/backend-oauth/).
-Start OAuth flow. Returns `ConnectResult` for popup or URL string for redirect.
+### Sessions
```typescript
-// Popup
-await nylasConnect.connect({ method: 'popup' });
+const session = await nylasConnect.getSession();
+if (session?.grantInfo) console.log(session.grantInfo.email);
+
+// "connected" | "expired" | "invalid" | "not_connected"
+const status = await nylasConnect.getConnectionStatus();
-// Redirect
-const url = await nylasConnect.connect({ method: 'inline' });
+await nylasConnect.logout(); // or logout(grantId) for one of several
```
-### `callback(url?)`
+Call `getSession()` on load to restore a session. It returns `null` when nobody is connected, which is your cue to show a connect button.
-Handle OAuth callback. Auto-detects current URL if none provided.
+### Configuration
-### `getSession(grantId?)`
+| Option | Type | Default | Description |
+| ----------------------- | ---------------------------------- | -------------------------- | --------------------------------------------------------------- |
+| `clientId` | `string` | `NYLAS_CLIENT_ID` | Your Nylas application's client ID |
+| `redirectUri` | `string` | `NYLAS_REDIRECT_URI` | Where Nylas returns the user after they authorize |
+| `apiUrl` | `string` | `https://api.us.nylas.com` | Use `https://api.eu.nylas.com` for EU accounts |
+| `environment` | `Environment` | detected automatically | `development`, `staging`, or `production` |
+| `defaultScopes` | `NylasScope[]` \| `ProviderScopes` | connector scopes | Scopes to request, optionally keyed per provider |
+| `persistTokens` | `boolean` | `true` | Store tokens in `localStorage`; `false` keeps them in memory |
+| `autoHandleCallback` | `boolean` | `true` | Let the browser exchange the code; `false` for backend handling |
+| `debug` | `boolean` | on in development | Enable debug logging |
+| `logLevel` | `LogLevel` \| `"off"` | follows `debug` | `error`, `warn`, `info`, `debug`, or `off` |
+| `codeExchange` | `CodeExchangeMethod` | built-in PKCE exchange | Replace the token exchange with your own |
+| `identityProviderToken` | `IdentityProviderTokenCallback` | none | Returns your IdP's JWT, sent to Nylas as `idp_claims` |
-Get current session. Returns `null` if no active session.
+### Custom code exchange
-### `logout(grantId?)`
+Pass `codeExchange` to run the token exchange on your own backend, keeping your API key out of the browser. Your function receives the code and returns a `ConnectResult`.
-Clear stored tokens and logout.
+```typescript
+const nylasConnect = new NylasConnect({
+ clientId: process.env.NYLAS_CLIENT_ID,
+ redirectUri: "http://localhost:3000/auth/callback",
+ codeExchange: async (params) => {
+ const r = await fetch("/api/auth/exchange", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(params),
+ });
+ const t = await r.json();
+ return {
+ accessToken: t.access_token,
+ idToken: t.id_token,
+ grantId: t.grant_id,
+ expiresAt: Date.now() + t.expires_in * 1000,
+ scope: t.scope,
+ grantInfo: t.grant_info,
+ };
+ },
+});
+```
-## Advanced Usage
+The matching backend route, plus the `state` handling that links a grant to your user, is in [backend callback handling](https://developer.nylas.com/docs/v3/auth/nylas-connect/backend-oauth/).
-### Backend-Only Flow
+### Backend-only flow
-For server-side token exchange:
+To do the whole exchange server-side, `getAuthUrl()` builds the authorization URL **without** PKCE and without storing any state, leaving your backend to exchange the code as a confidential client using your API key.
```typescript
-// Client: build auth URL without PKCE
-const { url, state } = await nylasConnect.getAuthUrl();
+// Client: build the URL and send the user to it.
+const { url, state, scopes } = await nylasConnect.getAuthUrl();
window.location.href = url;
-// Server: exchange code using Nylas Node SDK
+// Server: exchange the code with the Nylas Node SDK.
const { grantId } = await nylas.auth.exchangeCodeForToken({
clientId: process.env.NYLAS_CLIENT_ID,
clientSecret: process.env.NYLAS_CLIENT_SECRET,
code: req.query.code,
- redirectUri: process.env.NYLAS_REDIRECT_URI
+ redirectUri: process.env.NYLAS_REDIRECT_URI,
});
```
-### Custom Scopes
+Persist the returned `state` and check it when the user comes back. Because nothing is stored client-side, `callback()` and `getSession()` play no part in this flow.
-```typescript
-await nylasConnect.connect({
- method: 'popup',
- scopes: ['https://www.googleapis.com/auth/gmail.readonly']
-});
-```
+### Error handling
-### Event Handling
+Every error extends `NylasConnectError` and sets a distinct `name`, so checking `error.name` always works. The 3 you'll handle most often:
```typescript
-const unsubscribe = nylasConnect.onConnectStateChange((event, session) => {
- if (event === 'CONNECT_SUCCESS') {
- console.log('Connected:', session?.grantInfo?.email);
+try {
+ await nylasConnect.connect({ method: "popup" });
+} catch (error) {
+ if (error.name === "PopupError") {
+ // Blocked or closed. Fall back to method: "inline".
+ } else if (error.name === "ConfigError") {
+ // Missing clientId or redirectUri.
+ } else if (error.name === "OAuthError") {
+ // Provider rejected the request.
}
-});
-
-// Clean up
-unsubscribe();
+}
```
-## FAQ
+`OAuthError` is thrown as one of 9 subclasses mapping to the OAuth 2.0 error codes โ `OAuthAccessDeniedError`, `OAuthInvalidGrantError`, `OAuthInvalidScopeError`, and so on. Those subclasses aren't exported, so narrow them with `error.name`; `instanceof OAuthError` catches all 9. The importable classes are `NylasConnectError`, `ConfigError`, `NetworkError`, `OAuthError`, `TokenError`, and `PopupError`.
-### Popup vs Redirect?
+You can also subscribe rather than catch:
-**Popup:** Better UX, works in SPAs, requires popup permission
-**Redirect:** Works everywhere, better for mobile, full page navigation
+```typescript
+const unsubscribe = nylasConnect.onConnectStateChange((event, session, data) => {
+ if (event === "CONNECT_SUCCESS") console.log(session?.grantInfo?.email);
+ if (event === "CONNECT_ERROR") console.error(data?.error);
+});
+```
-### Do I need custom scopes?
+## ๐ก Examples
-Usually no. Nylas handles default scopes automatically. Override only for specific provider permissions.
+A runnable demo lives at the package root โ `index.html`, `callback.html`, and `auth-instance.js`. Start it with `pnpm dev`.
-### Which region?
+For full sample apps and product quickstarts, browse [**nylas-samples** on GitHub](https://github.com/orgs/nylas-samples/repositories).
-Match your Nylas account region:
-- US: `https://api.us.nylas.com`
-- EU: `https://api.eu.nylas.com`
+## ๐ค AI agents
-### Token refresh?
+[nylas/skills](https://github.com/nylas/skills) drops Nylas into Claude Code, Cursor, Codex, and other agents that support the skills format:
-Automatic. @nylas/connect handles token refresh in the background.
+```bash
+npx skills add nylas/skills
+/plugin marketplace add nylas/skills # Claude Code
+```
+The CLI also installs an MCP server for Claude Desktop, Claude Code, Cursor, Windsurf, or VS Code:
-# External Identity Provider Integration Example
+```bash
+brew install nylas/nylas-cli/nylas
+nylas mcp install
+```
-This example demonstrates how to use the new `identityProviderToken` callback feature to integrate external identity providers (via JWKS) with Nylas Connect.
+Walkthrough: [give AI agents email access via MCP](https://cli.nylas.com/guides/give-ai-agents-email-access-via-mcp).
-## Basic Usage
+## ๐ Reference
-```typescript
-import { NylasConnect } from '@nylas/connect';
-
-// Example: Using a function that returns a JWT token
-const connect = new NylasConnect({
- clientId: 'your-client-id',
- redirectUri: 'http://localhost:3000/auth/callback',
-
- // New feature: Identity provider token callback
- identityProviderToken: async () => {
- // Your logic to get the JWT token from your external identity provider
- // This could be from your own auth system, a third-party service, etc.
- const token = await getJWTFromYourIdentityProvider();
- return token; // Return the JWT string, or null if not available
- }
-});
+- **Library guide:** [developer.nylas.com/docs/v3/auth/nylas-connect](https://developer.nylas.com/docs/v3/auth/nylas-connect/)
+- **Quickstart:** [developer.nylas.com/docs/v3/getting-started/nylas-connect](https://developer.nylas.com/docs/v3/getting-started/nylas-connect/)
+- **`NylasConnect` class reference:** [every method signature and return type](https://developer.nylas.com/docs/v3/auth/nylas-connect/nylasconnect-class/)
+- **Identity provider guides:** [Auth0, Clerk, Google, WorkOS, custom JWKS](https://developer.nylas.com/docs/v3/auth/nylas-connect/use-external-idp/)
+- **API reference:** [developer.nylas.com/docs/api/v3](https://developer.nylas.com/docs/api/v3/)
+- **Auth flows:** [developer.nylas.com/docs/v3/auth](https://developer.nylas.com/docs/v3/auth/)
+- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
-// The rest works the same as before
-const result = await connect.connect({ method: 'popup' });
-```
+## โจ Upgrading
+See [`CHANGELOG.md`](CHANGELOG.md) for per-release notes.
-## How It Works
+## ๐ Contributing
-1. When you call `connect.connect()`, the authentication flow proceeds normally
-2. During the token exchange step (when exchanging the authorization code for access tokens), the `identityProviderToken` callback is called
-3. If the callback returns a JWT token, it's sent to Nylas as the `idp_claims` parameter
-4. If the callback returns `null` or throws an error:
- - Returning `null`: The auth flow continues without IDP claims
- - Throwing an error: The entire token exchange fails with a `NETWORK_ERROR` event
+Issues, ideas, and pull requests welcome โ see [CONTRIBUTING.md](../../CONTRIBUTING.md). Before opening a large change, please open an issue or post in the [forum](https://forums.nylas.com) so we can sanity-check the direction.
-## Error Handling
+## ๐ Security
-If the `identityProviderToken` callback throws an error, the entire authentication flow will fail with a `NETWORK_ERROR` event. You can listen for this event to handle IDP-related errors:
+Found a vulnerability? Please **don't** open a public issue. Report it through our [Vulnerability Disclosure Policy](https://www.nylas.com/security/vulnerability-disclosure-policy/).
-```typescript
-connect.onConnectStateChange((event, session, data) => {
- if (event === 'NETWORK_ERROR' && data?.operation === 'identity_provider_token_callback') {
- // Handle IDP token callback error
- console.error('IDP token error:', data.error);
- }
-});
-```
+## ๐ Other Nylas SDKs
+- [@nylas/react](https://github.com/nylas/javascript/tree/main/packages/react) ยท `npm install @nylas/react`
+- [nylas-nodejs](https://github.com/nylas/nylas-nodejs) ยท `npm install nylas`
+- [nylas-python](https://github.com/nylas/nylas-python) ยท `pip install nylas`
+- [nylas-ruby](https://github.com/nylas/nylas-ruby) ยท `gem install nylas`
+- [nylas-java](https://github.com/nylas/nylas-java) ยท Maven / Gradle (Kotlin too)
-## License
+## ๐ License
-MIT ยฉ [Nylas](https://nylas.com)
\ No newline at end of file
+MIT โ see [LICENSE.md](LICENSE.md).