A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add SDKs to the docs and update authentication docs

Trezy 3d667250 5366177b

+499 -9
+45 -9
packages/docs/docs/getting-started/authentication.md
··· 9 9 10 10 | Endpoint type | Client identification | User authentication | 11 11 | ----------------------------------- | ------------------------ | ------------------------------------------------------------------------------------ | 12 - | Queries (`GET /xrpc/{method}`) | `X-Client-Key` required | Optional — provide a session if the query needs to know who the user is | 13 - | Procedures (`POST /xrpc/{method}`) | `X-Client-Key` required | Required — a live OAuth session so HappyView can proxy writes to the user's PDS | 14 - | Admin API (`/admin/*`) | — | Required — must be a HappyView user with the right [permissions](../guides/permissions.md) | 12 + | Queries (`GET /xrpc/{method}`) | `X-Client-Key` required | Optional — DPoP auth if the query needs to know who the user is | 13 + | Procedures (`POST /xrpc/{method}`) | `X-Client-Key` required | Required — DPoP auth so HappyView can proxy writes to the user's PDS | 14 + | Admin API (`/admin/*`) | — | Required — session cookie, admin API key, or service auth JWT with the right [permissions](../guides/permissions.md) | 15 15 | Health check (`GET /health`) | — | — | 16 16 17 17 ## XRPC: API client identification ··· 48 48 -H 'X-Client-Secret: hvs_d4e5f6...' 49 49 ``` 50 50 51 - ### Logging a user in so you can call procedures 51 + ### Authenticating users for procedures 52 + 53 + Queries that don't care who is calling need nothing more than the client key. Procedures — and queries whose Lua scripts read the caller's DID — need a real AT Protocol OAuth session. 54 + 55 + XRPC routes only accept **DPoP auth** (`Authorization: DPoP <token>` + `DPoP` proof header + `X-Client-Key`). Bearer tokens, service auth JWTs, and session cookies are not accepted on XRPC endpoints. 56 + 57 + Third-party apps authenticate users through the [DPoP key provisioning](#dpop-key-provisioning-for-third-party-apps) flow: your app gets a DPoP keypair from HappyView, runs a standard OAuth flow with the user's PDS using that keypair, then registers the resulting tokens back with HappyView. 52 58 53 - Queries that don't care who is calling need nothing more than the client key. Procedures — and queries whose Lua scripts read the caller's DID — need a real AT Protocol OAuth session. The shape of the flow: 59 + The [JavaScript SDK](../sdk/overview.md) handles this entire flow for you: 54 60 55 - 1. Publish a client metadata document at your API client's `client_id_url`. 56 - 2. Redirect the user to HappyView's OAuth authorize endpoint with your `hvc_…` key as `client_id`. 57 - 3. Exchange the authorization code at the token endpoint using your client key + `hvs_…` secret. 58 - 4. HappyView sets a signed session cookie containing the user's DID and your client key. Subsequent XRPC requests made with that cookie are automatically attributed to your client — you don't need to also send `X-Client-Key`. 61 + ```typescript 62 + import { Client } from "@atproto/lex"; 63 + import { HappyViewBrowserClient } from "@happyview/oauth-client-browser"; 64 + import { createAgent } from "@happyview/lex-agent"; 65 + 66 + const oauthClient = new HappyViewBrowserClient({ 67 + instanceUrl: "https://happyview.example.com", 68 + clientKey: "hvc_your_client_key", 69 + }); 70 + 71 + // Login — redirects to the user's PDS for authorization 72 + await oauthClient.login("alice.bsky.social"); 73 + 74 + // On /oauth/callback — complete the token exchange 75 + const session = await oauthClient.callback(); 76 + 77 + // Create a type-safe Lex client 78 + const agent = createAgent(session); 79 + const lex = new Client(agent); 80 + 81 + // Make authenticated XRPC calls 82 + await lex.xrpc(myLexicons.com.example.createPost, { 83 + input: { text: "Hello from HappyView!" }, 84 + }); 85 + ``` 59 86 60 87 For procedures, HappyView proxies the write to the user's PDS using the stored OAuth session (see [Proxying procedures](#proxying-procedures-to-the-users-pds) below). 88 + 89 + :::note 90 + The HappyView dashboard uses a separate cookie-based OAuth flow where HappyView itself acts as the OAuth server. This is only for the dashboard — third-party apps always use DPoP key provisioning. 91 + ::: 61 92 62 93 ## Admin API: user authentication 63 94 ··· 112 143 Third-party apps that want HappyView to make PDS writes on behalf of their users use the **DPoP key provisioning** flow instead of cookie auth. This avoids browser-based redirects through HappyView's domain, which can be blocked by Firefox's Bounce Tracker Protection. 113 144 114 145 The idea: the app gets a DPoP keypair from HappyView, uses that keypair during its own OAuth flow with the user's PDS, then registers the resulting tokens back with HappyView. From that point on, XRPC requests authenticated with `Authorization: DPoP <access_token>` plus a `DPoP` proof header and `X-Client-Key` will have HappyView proxy writes using the stored session. 146 + 147 + :::tip 148 + The [JavaScript SDK](../sdk/overview.md) handles this entire flow for you. The raw HTTP flow below is useful for understanding the protocol or building a non-JavaScript client. 149 + ::: 115 150 116 151 ### API clients: confidential vs public 117 152 ··· 245 280 246 281 ## Next steps 247 282 283 + - [JavaScript SDK](../sdk/overview.md) — authenticate and make XRPC calls from JavaScript 248 284 - [Permissions](../guides/permissions.md) — full list of permissions and what each one grants 249 285 - [API Keys](../guides/api-keys.md) — create scoped admin API keys for automation 250 286 - [Admin API — API Clients](../reference/admin-api.md#api-clients) — register API clients and configure rate limits
+60
packages/docs/docs/sdk/lex-agent.md
··· 1 + # Lex Agent 2 + 3 + The Lex agent adapter is the recommended way to interact with HappyView from JavaScript. It creates an [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex) `Agent` from a `HappyViewSession`, so you can use `@atproto/lex`'s type-safe `Client` to make XRPC calls with HappyView's DPoP authentication. All requests are routed to your HappyView instance, which handles its own lexicons locally and proxies standard AT Protocol methods (e.g., `com.atproto.repo.createRecord`) to the user's PDS. 4 + 5 + The adapter gives you lexicon-level type checking on parameters, input bodies, and responses, and works with any library or tool that accepts an `@atproto/lex` `Agent`. 6 + 7 + ## Installation 8 + 9 + ```bash 10 + npm install @happyview/lex-agent @atproto/lex 11 + ``` 12 + 13 + `@atproto/lex` is a peer dependency (`>=0.0.20`). 14 + 15 + ## Usage 16 + 17 + ```typescript 18 + import { Client } from "@atproto/lex"; 19 + import { HappyViewBrowserClient } from "@happyview/oauth-client-browser"; 20 + import { createAgent } from "@happyview/lex-agent"; 21 + 22 + const client = new HappyViewBrowserClient({ 23 + instanceUrl: "https://happyview.example.com", 24 + clientKey: "hvc_your_client_key", 25 + }); 26 + 27 + // Authenticate (or restore a session) 28 + const session = await client.restore(); 29 + 30 + // Create a Lex agent from the session 31 + const agent = createAgent(session); 32 + const lex = new Client(agent); 33 + ``` 34 + 35 + ## Type-safe XRPC calls 36 + 37 + With a `Client` instance, you can make type-safe XRPC calls using lexicon definitions: 38 + 39 + ```typescript 40 + // Query 41 + const result = await lex.xrpc(myLexicons.com.example.getGame, { 42 + params: { slug: "celeste" }, 43 + }); 44 + 45 + // Procedure 46 + await lex.xrpc(myLexicons.com.example.createPost, { 47 + input: { text: "Hello from HappyView!" }, 48 + }); 49 + ``` 50 + 51 + The `Client` validates parameters and return types against the lexicon schema at the type level, so your IDE catches mismatches before runtime. 52 + 53 + ## API 54 + 55 + ### `createAgent(session: HappyViewSession): Agent` 56 + 57 + Creates an `@atproto/lex` `Agent` from a `HappyViewSession`. 58 + 59 + - `agent.did` — the session user's DID 60 + - `agent.fetchHandler(path, init)` — delegates to `session.fetchHandler`, which attaches DPoP authentication headers and prepends the HappyView instance URL to relative paths
+144
packages/docs/docs/sdk/oauth-client-browser.md
··· 1 + # Browser Client 2 + 3 + The browser client handles the full OAuth redirect flow for browser apps authenticating with a HappyView instance. It wraps the [OAuth Client](./oauth-client.md) with Web Crypto, localStorage, and AT Protocol handle/DID resolution. 4 + 5 + If you're starting a new app, consider using [`@happyview/lex-agent`](./lex-agent.md) with `@atproto/lex` instead — it provides type-safe XRPC calls and is the recommended way to interact with HappyView. This package is primarily useful if your app already uses `@atproto/oauth-client-browser` and you want to add HappyView authentication alongside it. 6 + 7 + ## Installation 8 + 9 + ```bash 10 + npm install @happyview/oauth-client-browser 11 + ``` 12 + 13 + ## Setup 14 + 15 + ```typescript 16 + import { HappyViewBrowserClient } from "@happyview/oauth-client-browser"; 17 + 18 + const client = new HappyViewBrowserClient({ 19 + instanceUrl: "https://happyview.example.com", 20 + clientKey: "hvc_your_client_key", 21 + }); 22 + ``` 23 + 24 + The client uses Web Crypto and localStorage by default. You can override either: 25 + 26 + ```typescript 27 + const client = new HappyViewBrowserClient({ 28 + instanceUrl: "https://happyview.example.com", 29 + clientKey: "hvc_your_client_key", 30 + crypto: myCustomCryptoAdapter, 31 + storage: myCustomStorageAdapter, 32 + }); 33 + ``` 34 + 35 + :::note 36 + The API client must be registered as a **public** client (no secret) with your app's origin in `allowed_origins`. See [Authentication — API clients](../getting-started/authentication.md#api-clients-confidential-vs-public). 37 + ::: 38 + 39 + ## Login 40 + 41 + `login()` resolves the user's handle, discovers their PDS, provisions a DPoP key, and redirects the browser to the PDS authorization server: 42 + 43 + ```typescript 44 + await client.login("alice.bsky.social"); 45 + // Browser redirects — code stops here 46 + ``` 47 + 48 + If you need the authorization URL without redirecting (e.g., for a popup or custom UI), use `prepareLogin()`: 49 + 50 + ```typescript 51 + const { authorizationUrl, did, state } = 52 + await client.prepareLogin("alice.bsky.social"); 53 + 54 + // Open in a popup, new tab, etc. 55 + window.open(authorizationUrl); 56 + ``` 57 + 58 + ### What happens during login 59 + 60 + 1. The handle is resolved to a DID via `resolveHandleToDid`. 61 + 2. The DID document is fetched to find the PDS URL. 62 + 3. The PDS's OAuth authorization server metadata is fetched. 63 + 4. A DPoP key is provisioned from HappyView. 64 + 5. PKCE challenge/verifier pairs are generated (one for HappyView's DPoP provisioning, one for the PDS authorization server). 65 + 6. The pending auth state is stored in localStorage. 66 + 7. The browser is redirected to the PDS authorization endpoint. 67 + 68 + ## OAuth callback 69 + 70 + Your app needs an `/oauth/callback` route. On that page, call `callback()` to complete the token exchange: 71 + 72 + ```typescript 73 + // On /oauth/callback 74 + const session = await client.callback(); 75 + // Session is now stored in localStorage and ready to use 76 + ``` 77 + 78 + `callback()` reads the `code` and `state` from the URL query string, exchanges the code for tokens at the PDS token endpoint, and registers the session with HappyView. The pending auth state is cleaned up automatically. 79 + 80 + ## Restore session 81 + 82 + On subsequent page loads, restore the session from localStorage instead of re-authenticating: 83 + 84 + ```typescript 85 + const session = await client.restore(); 86 + if (session) { 87 + // User is still logged in 88 + } 89 + ``` 90 + 91 + Returns `null` if no stored session is found. 92 + 93 + ## Authenticated requests 94 + 95 + The session's `fetchHandler` attaches DPoP proof headers automatically: 96 + 97 + ```typescript 98 + const response = await session.fetchHandler( 99 + "/xrpc/com.example.getStuff?limit=10", 100 + { method: "GET" }, 101 + ); 102 + 103 + const data = await response.json(); 104 + ``` 105 + 106 + Pass a relative path (prepends the HappyView instance URL) or a full URL (used as-is). 107 + 108 + ## Logout 109 + 110 + ```typescript 111 + await client.logout(session.did); 112 + ``` 113 + 114 + ## Resolution utilities 115 + 116 + The browser client exports the resolution functions it uses internally. These are useful if you need to resolve handles or discover PDS URLs outside of the login flow: 117 + 118 + ```typescript 119 + import { 120 + resolveHandleToDid, 121 + resolveDidDocument, 122 + resolvePdsUrl, 123 + resolveAuthServerMetadata, 124 + } from "@happyview/oauth-client-browser"; 125 + 126 + const did = await resolveHandleToDid("alice.bsky.social"); 127 + const doc = await resolveDidDocument(did); 128 + const pdsUrl = resolvePdsUrl(doc); 129 + const authMeta = await resolveAuthServerMetadata(pdsUrl); 130 + ``` 131 + 132 + ## Re-exports 133 + 134 + This package re-exports everything from `@happyview/oauth-client`, so you don't need to install the core package separately. All types, error classes, and utilities are available: 135 + 136 + ```typescript 137 + import { 138 + HappyViewBrowserClient, 139 + HappyViewSession, 140 + ApiError, 141 + type CryptoAdapter, 142 + type StorageAdapter, 143 + } from "@happyview/oauth-client-browser"; 144 + ```
+158
packages/docs/docs/sdk/oauth-client.md
··· 1 + # OAuth Client 2 + 3 + The core OAuth client handles DPoP key provisioning, session registration, and session restoration against a HappyView instance. It's platform-agnostic — you provide a `CryptoAdapter` and optional `StorageAdapter` for your environment. 4 + 5 + If you're building a browser app, use the [Browser Client](./oauth-client-browser.md) instead. It wraps this package with Web Crypto, localStorage, and a complete OAuth redirect flow. 6 + 7 + ## Installation 8 + 9 + ```bash 10 + npm install @happyview/oauth-client 11 + ``` 12 + 13 + ## Setup 14 + 15 + ```typescript 16 + import { HappyViewOAuthClient } from "@happyview/oauth-client"; 17 + 18 + const client = new HappyViewOAuthClient({ 19 + instanceUrl: "https://happyview.example.com", 20 + clientKey: "hvc_your_client_key", 21 + clientSecret: "hvs_your_secret", // optional, for confidential clients 22 + crypto: myCryptoAdapter, 23 + storage: myStorageAdapter, // optional, defaults to in-memory 24 + }); 25 + ``` 26 + 27 + The `clientSecret` parameter makes this a **confidential client**. Omit it for public clients (browser apps), which use PKCE instead. See [Authentication — API clients](../getting-started/authentication.md#api-clients-confidential-vs-public) for details. 28 + 29 + ## DPoP key provisioning 30 + 31 + Request a DPoP keypair from the HappyView instance. This is the first step of the [DPoP key provisioning flow](../getting-started/authentication.md#dpop-key-provisioning-for-third-party-apps). 32 + 33 + ```typescript 34 + const { provisionId, dpopKey, pkceVerifier } = 35 + await client.provisionDpopKey(); 36 + ``` 37 + 38 + For public clients, `pkceVerifier` is included and must be passed back when registering the session. For confidential clients it will be `undefined`. 39 + 40 + Use the returned `dpopKey` (a private JWK) as your DPoP keypair during your AT Protocol OAuth flow with the user's PDS. 41 + 42 + ## Session registration 43 + 44 + After completing OAuth authorization with the user's PDS, register the session with HappyView: 45 + 46 + ```typescript 47 + const session = await client.registerSession({ 48 + provisionId, 49 + pkceVerifier, // required for public clients 50 + did: "did:plc:abc123", 51 + accessToken: tokens.access_token, 52 + refreshToken: tokens.refresh_token, 53 + scopes: "atproto", 54 + pdsUrl: "https://bsky.social", 55 + issuer: tokens.iss, 56 + dpopKey, 57 + }); 58 + ``` 59 + 60 + The returned `HappyViewSession` is ready to make authenticated requests. The session data is also persisted to the `StorageAdapter` for later restoration. 61 + 62 + ## Making authenticated requests 63 + 64 + `HappyViewSession.fetchHandler` works like `fetch` but automatically attaches DPoP proof, authorization, and client key headers: 65 + 66 + ```typescript 67 + // Relative path — prepends the HappyView instance URL 68 + const response = await session.fetchHandler( 69 + "/xrpc/com.example.getStuff?limit=10", 70 + { method: "GET" }, 71 + ); 72 + 73 + // Absolute URL — used as-is 74 + const response = await session.fetchHandler( 75 + "https://other-service.example.com/xrpc/test.method", 76 + { method: "GET" }, 77 + ); 78 + ``` 79 + 80 + ## Session restoration 81 + 82 + Restore a previously stored session without re-authenticating: 83 + 84 + ```typescript 85 + // Restore the last active session 86 + const session = await client.restore(); 87 + 88 + // Restore a specific user's session 89 + const session = await client.restoreSession("did:plc:abc123"); 90 + ``` 91 + 92 + Returns `null` if no stored session is found. 93 + 94 + ## Logout 95 + 96 + ```typescript 97 + await client.deleteSession("did:plc:abc123"); 98 + ``` 99 + 100 + This deletes the session from both HappyView and local storage. 101 + 102 + ## Adapters 103 + 104 + ### CryptoAdapter 105 + 106 + Implement this interface for your platform's cryptographic primitives: 107 + 108 + ```typescript 109 + interface CryptoAdapter { 110 + generatePkceVerifier(): Promise<string>; 111 + computePkceChallenge(verifier: string): Promise<string>; 112 + signEs256(privateKey: JsonWebKey, payload: Uint8Array): Promise<Uint8Array>; 113 + sha256(data: Uint8Array): Promise<Uint8Array>; 114 + getRandomValues(length: number): Uint8Array; 115 + } 116 + ``` 117 + 118 + ### StorageAdapter 119 + 120 + Implement this interface to persist sessions: 121 + 122 + ```typescript 123 + interface StorageAdapter { 124 + get(key: string): Promise<string | null>; 125 + set(key: string, value: string): Promise<void>; 126 + delete(key: string): Promise<void>; 127 + } 128 + ``` 129 + 130 + If no `StorageAdapter` is provided, sessions are stored in memory and won't survive page reloads or process restarts. 131 + 132 + :::note 133 + The built-in `MemoryStorage` is exported for testing. In production, always provide a persistent storage adapter. 134 + ::: 135 + 136 + ## Error handling 137 + 138 + All errors extend `HappyViewError`: 139 + 140 + | Error | When | 141 + | --- | --- | 142 + | `ApiError` | HappyView API returned a non-OK response (has `status` and `body`) | 143 + | `AuthenticationError` | Authentication failed (default status 401) | 144 + | `InvalidStateError` | Missing or invalid OAuth state | 145 + | `TokenExchangeError` | Token exchange with the PDS failed (has `status` and `body`) | 146 + | `ResolutionError` | Handle or DID resolution failed | 147 + 148 + ```typescript 149 + import { ApiError } from "@happyview/oauth-client"; 150 + 151 + try { 152 + await client.registerSession(params); 153 + } catch (err) { 154 + if (err instanceof ApiError) { 155 + console.error(`API error ${err.status}:`, err.body); 156 + } 157 + } 158 + ```
+66
packages/docs/docs/sdk/overview.md
··· 1 + # JavaScript SDK 2 + 3 + HappyView provides JavaScript packages for building third-party apps that authenticate with a HappyView instance and make XRPC requests on behalf of users. 4 + 5 + | Package | Purpose | 6 + | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | 7 + | [`@happyview/lex-agent`](https://www.npmjs.com/package/@happyview/lex-agent) | Recommended — type-safe XRPC via [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex) `Client` with HappyView DPoP auth | 8 + | [`@happyview/oauth-client`](https://www.npmjs.com/package/@happyview/oauth-client) | Platform-agnostic core — DPoP key provisioning, session management, authenticated fetch | 9 + | [`@happyview/oauth-client-browser`](https://www.npmjs.com/package/@happyview/oauth-client-browser) | Browser OAuth wrapper for apps already using `@atproto/oauth-client-browser` | 10 + 11 + ## Which package do I need? 12 + 13 + **Starting a new app?** Use `@happyview/lex-agent` with `@atproto/lex`. It gives you type-safe XRPC calls through a `Client` that routes requests to your HappyView instance with DPoP authentication. This is the recommended way to interact with HappyView from JavaScript. 14 + 15 + **Already using `@atproto/oauth-client-browser`?** Add `@happyview/oauth-client-browser` to get a `HappyViewBrowserClient` that handles the HappyView-specific DPoP key provisioning and session registration on top of the standard AT Protocol OAuth flow. 16 + 17 + **Building a server-side app or something more custom?** Use `@happyview/oauth-client` directly and provide your own `CryptoAdapter` and `StorageAdapter`. 18 + 19 + ## How it works 20 + 21 + Third-party apps authenticate using HappyView's [DPoP key provisioning](../getting-started/authentication.md#dpop-key-provisioning-for-third-party-apps) flow: 22 + 23 + 1. The SDK requests a DPoP keypair from the HappyView instance. 24 + 2. Your app runs a standard AT Protocol OAuth flow with the user's PDS using that keypair. 25 + 3. The SDK registers the resulting tokens with HappyView. 26 + 4. All subsequent XRPC requests are authenticated with DPoP proofs — HappyView handles its own lexicons locally and proxies standard AT Protocol writes to the user's PDS. 27 + 28 + ## Quick start 29 + 30 + ```bash 31 + npm install @happyview/lex-agent @happyview/oauth-client-browser @atproto/lex 32 + ``` 33 + 34 + ```typescript 35 + import { Client } from "@atproto/lex"; 36 + import { HappyViewBrowserClient } from "@happyview/oauth-client-browser"; 37 + import { createAgent } from "@happyview/lex-agent"; 38 + 39 + // Set up the OAuth client 40 + const oauthClient = new HappyViewBrowserClient({ 41 + instanceUrl: "https://happyview.example.com", 42 + clientKey: "hvc_your_client_key", 43 + }); 44 + 45 + // Login — redirects to the user's PDS 46 + await oauthClient.login("alice.bsky.social"); 47 + 48 + // On /oauth/callback — complete the flow 49 + const session = await oauthClient.callback(); 50 + 51 + // Create a type-safe Lex client 52 + const agent = createAgent(session); 53 + const lex = new Client(agent); 54 + 55 + // Make type-safe XRPC calls 56 + const result = await lex.xrpc(myLexicons.com.example.getGame, { 57 + params: { slug: "celeste" }, 58 + }); 59 + ``` 60 + 61 + ## Next steps 62 + 63 + - [Lex Agent](./lex-agent.md): type-safe XRPC with `@atproto/lex` 64 + - [OAuth Client](./oauth-client.md): platform-agnostic core client 65 + - [Browser Client](./oauth-client-browser.md): browser OAuth redirect flow 66 + - [Authentication](../getting-started/authentication.md): full details on DPoP key provisioning and API client types
+26
packages/docs/sidebars.ts
··· 234 234 }, 235 235 { 236 236 type: "category", 237 + label: "JavaScript SDK", 238 + items: [ 239 + { 240 + type: "doc", 241 + id: "sdk/overview", 242 + label: "Overview", 243 + }, 244 + { 245 + type: "doc", 246 + id: "sdk/lex-agent", 247 + label: "Lex Agent", 248 + }, 249 + { 250 + type: "doc", 251 + id: "sdk/oauth-client", 252 + label: "OAuth Client", 253 + }, 254 + { 255 + type: "doc", 256 + id: "sdk/oauth-client-browser", 257 + label: "Browser Client", 258 + }, 259 + ], 260 + }, 261 + { 262 + type: "category", 237 263 label: "Reference", 238 264 items: [ 239 265 {