Highly ambitious ATProtocol AppView service and sdks
README.md

@slices/oauth#

AIP OAuth 2.1 client with PKCE support and Device Authorization Grant (RFC 8628) for TypeScript.

Features#

  • OAuth 2.1 with PKCE flow
  • Device Authorization Grant (RFC 8628) support
  • Token refresh handling
  • Pluggable storage backends (SQLite, Deno KV, others can be added)
  • Automatic token management and refresh

Installation#

Deno#

deno add jsr:@slices/oauth
import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth";

For node based package managers:

# pnpm (10.9+)
pnpm install jsr:@slices/oauth

# Yarn (4.9+)
yarn add jsr:@slices/oauth

Usage#

Standard OAuth Flow (Web Applications)#

OAuth clients are session-scoped. Each client instance is tied to a specific session ID, enabling proper multi-device support where each session has independent OAuth tokens.

import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth";

// Set up storage
const storage = new SQLiteOAuthStorage("oauth.db");

// OAuth configuration
const config = {
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authBaseUrl: "https://auth.example.com",
  redirectUri: "http://localhost:8000/oauth/callback",
  scopes: ["atproto"],
};

// Start authorization flow (before we have a session)
const tempClient = new OAuthClient(config, storage, "temp");
const result = await tempClient.authorize({ loginHint: "user.bsky.social" });
// Redirect user to result.authorizationUrl

// Handle callback
const tokens = await tempClient.handleCallback({ code, state });

// Create session and store tokens by sessionId
// (typically done via @slices/session - see Session Integration below)
const sessionId = "user-session-id"; // from session store
const sessionClient = new OAuthClient(config, storage, sessionId);
await storage.setTokens(tokens, sessionId);

// Use the session-scoped client for API calls
const userInfo = await sessionClient.getUserInfo();

Session-Scoped Pattern#

The key concept: one OAuth client per session. This enables:

  • Multiple active sessions per user (different devices)
  • Independent token management per session
  • Proper logout isolation (logging out one device doesn't affect others)
// Each session gets its own OAuth client
function createSessionClient(sessionId: string) {
  return new OAuthClient(config, storage, sessionId);
}

// Session 1 (laptop)
const laptopClient = createSessionClient("session-laptop-123");

// Session 2 (phone) - completely independent tokens
const phoneClient = createSessionClient("session-phone-456");

Device Authorization Grant (CLI Applications)#

The Device Authorization Grant flow is perfect for CLI applications, TVs, or other devices without a browser or with limited input capabilities.

import { DeviceCodeClient } from "@slices/oauth";

// Create device code client
const client = new DeviceCodeClient({
  clientId: "your-cli-client-id",
  authBaseUrl: "https://auth.example.com",
  scopes: ["atproto", "transition:generic", "repo:*"],
});

// Start device code flow
const deviceAuth = await client.startDeviceAuth();

// Display the user code to the user
console.log(`Please visit: ${deviceAuth.verification_uri_complete}`);
console.log(
  `Or go to ${deviceAuth.verification_uri} and enter code: ${deviceAuth.user_code}`,
);

// Poll for tokens (handles the polling interval automatically)
const tokens = await client.pollForTokens(deviceAuth);

// Store the tokens securely
console.log("Authentication successful!");
console.log("Access token:", tokens.access_token);
console.log("DID:", tokens.sub);

Device Code Flow Features#

  • Automatic polling: The client handles the polling interval specified by the server
  • User-friendly codes: Short, easy-to-type codes for manual entry
  • Browser integration: Can automatically open the browser to the verification URL
  • Timeout handling: Automatically handles device code expiration
  • Error recovery: Handles slow_down responses and other polling errors

Typical Flow#

  1. Device requests authorization: The device (CLI app) requests a device code and user code
  2. User authorizes: The user visits the verification URL and enters the code
  3. Device polls for tokens: The device polls the authorization server until the user completes authorization
  4. Tokens received: Once authorized, the device receives access and refresh tokens

Example: CLI Application with Device Flow#

import { DeviceCodeClient } from "@slices/oauth";

export class CLIAuthManager {
  private client: DeviceCodeClient;

  constructor(clientId: string, authBaseUrl: string) {
    this.client = new DeviceCodeClient({
      clientId,
      authBaseUrl,
      scopes: [
        "atproto",
        "transition:generic",
        "repo:network.slices.slice",
        "repo:network.slices.lexicon",
      ],
    });
  }

  async login(): Promise<AuthTokens> {
    // Start device authorization
    const deviceAuth = await this.client.startDeviceAuth();

    // Show user instructions
    console.log("🔐 Authenticate with Slices");
    console.log("");
    console.log(`Please visit: ${deviceAuth.verification_uri_complete}`);
    console.log("");
    console.log("Or manually:");
    console.log(`1. Go to: ${deviceAuth.verification_uri}`);
    console.log(`2. Enter code: ${deviceAuth.user_code}`);
    console.log("");
    console.log("Waiting for authorization...");

    // Optionally open browser automatically
    if (Deno.build.os !== "linux") {
      await this.openBrowser(deviceAuth.verification_uri_complete);
    }

    // Poll for tokens
    try {
      const tokens = await this.client.pollForTokens(deviceAuth);
      console.log("✅ Authentication successful!");

      // Get user info to find out who authenticated
      const userInfo = await this.getUserInfo(tokens);
      console.log("Authenticated as:", userInfo?.sub);

      return tokens;
    } catch (error) {
      if (error.message.includes("expired")) {
        throw new Error("Device code expired. Please try again.");
      }
      throw error;
    }
  }

  private async openBrowser(url: string): Promise<void> {
    const cmd = Deno.build.os === "darwin"
      ? "open"
      : Deno.build.os === "windows"
      ? "start"
      : "xdg-open";

    try {
      await new Deno.Command(cmd, { args: [url] }).output();
    } catch {
      // Ignore errors - user can manually open the URL
    }
  }
}

Token Management#

Both OAuth flows support automatic token refresh. The client handles token refresh automatically:

// The client ensures tokens are valid before making requests
const sessionClient = new OAuthClient(config, storage, sessionId);

// Automatically refreshes if needed
const tokens = await sessionClient.ensureValidToken();

// Get user info (automatically uses valid tokens)
const userInfo = await sessionClient.getUserInfo();
console.log("Authenticated as:", userInfo.sub);

// Manual refresh if needed
const refreshedTokens = await sessionClient.refreshAccessToken();

Storage Backends#

The library provides pluggable storage backends. All storage backends store tokens by sessionId:

// SQLite storage (persistent)
import { SQLiteOAuthStorage } from "@slices/oauth";
const sqliteStorage = new SQLiteOAuthStorage("oauth.db");

// Deno KV storage (persistent)
import { DenoKVOAuthStorage } from "@slices/oauth";
const kvStorage = new DenoKVOAuthStorage(await Deno.openKv());

// Use with OAuthClient (requires sessionId)
const client = new OAuthClient(config, storage, sessionId);

Note: Tokens are stored with sessionId as the key, enabling multiple independent sessions per user.