npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@atproto/lex-password-session

v0.0.10

Published

Password based client authentication for AT Lexicons

Downloads

25,528

Readme

@atproto/lex-password-session

Password-based session authentication for AT Protocol Lexicons. See the Changelog for version history.

npm install @atproto/lex-password-session
  • Session management with automatic token refresh
  • Hooks for persisting and monitoring session state
  • PDS endpoint discovery from DID documents
  • Two-factor authentication support

[!IMPORTANT]

This package is currently in preview. The API and features are subject to change before the stable release.

What is this?

@atproto/lex-password-session provides a PasswordSession class that implements the Agent interface from @atproto/lex-client. It handles password-based authentication with AT Protocol services, including:

  1. Creating sessions with username/password credentials
  2. Automatic token refresh when access tokens expire
  3. Session persistence through lifecycle hooks
  4. Graceful logout with server-side session cleanup
import { Client } from '@atproto/lex-client'
import { PasswordSession } from '@atproto/lex-password-session'
import * as app from './lexicons/app.js'

// Login with credentials
const session = await PasswordSession.login({
  service: 'https://bsky.social',
  identifier: 'alice.bsky.social',
  password: 'app-password',
  onUpdated: (data) => saveToStorage(data),
  onDeleted: (data) => clearStorage(data.did),
})

const client = new Client(session)

// Make authenticated requests
const profile = await client.call(app.bsky.actor.getProfile, {
  actor: session.did,
})

Quick Start

1. Install the package

npm install @atproto/lex-password-session @atproto/lex-client

2. Login and make requests

import { Client } from '@atproto/lex-client'
import { PasswordSession } from '@atproto/lex-password-session'

const session = await PasswordSession.login({
  service: 'https://bsky.social',
  identifier: 'your-handle.bsky.social',
  password: 'your-app-password',
})

const client = new Client(session)

// Make authenticated API calls
console.log('Logged in as:', session.did)

PasswordSession

The PasswordSession class manages password-based authentication sessions.

Login

Create a new session with username and password:

import { PasswordSession } from '@atproto/lex-password-session'

const session = await PasswordSession.login({
  service: 'https://bsky.social',
  identifier: 'alice.bsky.social', // handle or email
  password: 'app-password',
  onUpdated: (data) => {
    // Persist session for later restoration
    localStorage.setItem('session', JSON.stringify(data))
  },
  onDeleted: () => {
    localStorage.removeItem('session')
  },
})

console.log('Logged in as:', session.did)

The login() method throws on failure. For expected errors like invalid credentials, an XrpcResponseError is thrown. For 2FA requirements, a LexAuthFactorError is thrown.

Two-Factor Authentication

[!CAUTION]

Two-factor authentication only applies when using main account credentials, which is strongly discouraged. Password authentication should be used with app passwords only because they are designed for programmatic access (bots, scripts, CLI tools). For user-facing applications, use OAuth via @atproto/oauth-client which provides better security and user control.

If the account has 2FA enabled, login will throw a LexAuthFactorError:

import {
  PasswordSession,
  LexAuthFactorError,
} from '@atproto/lex-password-session'

async function loginWith2FA(
  identifier: string,
  password: string,
  authFactorToken?: string,
): Promise<PasswordSession> {
  try {
    return await PasswordSession.login({
      service: 'https://bsky.social',
      identifier,
      password,
      authFactorToken,
      onUpdated: (data) => saveToStorage(data),
      onDeleted: (data) => removeFromStorage(data.did),
    })
  } catch (err) {
    if (err instanceof LexAuthFactorError && !authFactorToken) {
      // 2FA required - prompt user for code
      const token = await promptUserFor2FACode(err.message)
      return loginWith2FA(identifier, password, token)
    }
    throw err
  }
}

Resume Session

Restore a previously saved session:

import { PasswordSession, SessionData } from '@atproto/lex-password-session'

// Load session from storage
const savedSession: SessionData = JSON.parse(localStorage.getItem('session')!)

// Resume the session (automatically refreshes tokens)
const session = await PasswordSession.resume(savedSession, {
  onUpdated: (data) => {
    localStorage.setItem('session', JSON.stringify(data))
  },
  onDeleted: () => {
    localStorage.removeItem('session')
  },
})

console.log('Session resumed for:', session.did)

// Access session properties
console.log(session.did) // User's DID
console.log(session.handle) // User's handle
console.log(session.destroyed) // false (session is active)

[!NOTE]

resume() automatically calls refresh() to ensure the session is valid and tokens are current.

Logout

End the session and notify the server:

await session.logout()

After logout:

  • The onDeleted hook is called
  • The session is marked as destroyed (session.destroyed === true)
  • Further requests will throw 'Logged out'

Static Delete

Delete a session without creating a session instance:

import { PasswordSession, SessionData } from '@atproto/lex-password-session'

const data: SessionData = JSON.parse(localStorage.getItem('session')!)

// Delete the session on the server
await PasswordSession.delete(data)

This is useful for cleanup scenarios where you don't need to make additional requests.

Create Account

Create a new account and get an authenticated session:

import { PasswordSession } from '@atproto/lex-password-session'

const session = await PasswordSession.createAccount(
  {
    handle: 'alice.bsky.social',
    email: '[email protected]',
    password: 'secure-password',
  },
  {
    service: 'https://bsky.social',
    onUpdated: (data) => saveToStorage(data),
    onDeleted: (data) => removeFromStorage(data.did),
  },
)

console.log('Account created:', session.did)

Session Hooks

Hooks provide callbacks for session lifecycle events. All hooks receive the session instance as this context.

onUpdated

Called when the session is successfully created or refreshed:

const session = await PasswordSession.login({
  service: 'https://bsky.social',
  identifier: 'alice.bsky.social',
  password: 'app-password',
  onUpdated(data) {
    // `this` is the PasswordSession instance
    console.log('Session updated for:', this.did)

    // Persist the updated session
    saveSession(data)
  },
})

[!IMPORTANT]

Requests are blocked while onUpdated is running. Keep this callback fast to avoid delays.

onUpdateFailure

Called when token refresh fails due to transient errors (network issues, server unavailability):

{
  onUpdateFailure(data, error) {
    console.warn('Token refresh failed:', error.message)
    // Session may still be valid - consider retry logic
  }
}

onDeleted

Called when the session is terminated (logout or server-side invalidation):

{
  onDeleted(data) {
    console.log('Session ended for:', data.did)
    clearPersistedSession(data.did)
    redirectToLogin()
  }
}

onDeleteFailure

Called when logout fails due to transient errors:

{
  onDeleteFailure(data, error) {
    console.error('Logout failed:', error.message)
    // Consider queuing for retry to avoid orphaned sessions
    queueLogoutRetry(data)
  }
}

[!WARNING]

Ignoring delete failures can leave sessions active on the server. Implement retry logic for security-sensitive applications.

Session Data

The SessionData type contains all data needed to authenticate and restore sessions:

type SessionData = {
  // Session credentials and user info from createSession response
  accessJwt: string
  refreshJwt: string
  did: string
  handle: string
  email?: string
  emailConfirmed?: boolean
  didDoc?: object
  // ... other fields from createSession

  // Original service URL used for login
  service: string
}

Error Handling

The PasswordSession class uses exception-based error handling:

import {
  PasswordSession,
  LexAuthFactorError,
} from '@atproto/lex-password-session'
import { XrpcResponseError } from '@atproto/lex-client'

try {
  const session = await PasswordSession.login({
    service: 'https://bsky.social',
    identifier: 'alice.bsky.social',
    password: 'wrong-password',
  })
} catch (err) {
  if (err instanceof LexAuthFactorError) {
    console.error('2FA required')
  } else if (err instanceof XrpcResponseError) {
    switch (err.error) {
      case 'AuthenticationRequired':
        console.error('Invalid credentials')
        break
      case 'AccountTakedown':
        console.error('Account has been suspended')
        break
      default:
        console.error('Login failed:', err.message)
    }
  } else {
    throw err
  }
}

Common error codes:

| Error Code | Description | | ------------------------- | ------------------------------ | | AuthenticationRequired | Invalid username or password | | AuthFactorTokenRequired | 2FA code needed | | AccountTakedown | Account suspended | | ExpiredToken | Token has expired (on refresh) | | InvalidToken | Token is invalid |

Using with Client

The PasswordSession implements the Agent interface and can be used directly with Client:

import { Client } from '@atproto/lex-client'
import { PasswordSession } from '@atproto/lex-password-session'
import * as app from './lexicons/app.js'

const session = await PasswordSession.login({
  service: 'https://bsky.social',
  identifier: 'alice.bsky.social',
  password: 'app-password',
})

const client = new Client(session)

// The client automatically uses the session for authentication
const profile = await client.call(app.bsky.actor.getProfile, {
  actor: client.assertDid,
})

// Tokens are automatically refreshed when expired
const timeline = await client.call(app.bsky.feed.getTimeline, {
  limit: 50,
})

// Create records
await client.create(app.bsky.feed.post, {
  text: 'Hello from lex-password-session!',
  createdAt: new Date().toISOString(),
})

The session handles:

  • Adding Authorization headers to requests
  • Detecting expired tokens (401 responses or ExpiredToken errors)
  • Automatically refreshing tokens and retrying failed requests
  • Routing requests to the correct PDS based on DID document

License

MIT or Apache2