diff --git a/packages/lex-agent/LICENSE.md b/packages/lex-agent/LICENSE.md new file mode 100644 --- /dev/null +++ b/packages/lex-agent/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Lexicon Community + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/lex-agent/README.md b/packages/lex-agent/README.md new file mode 100644 --- /dev/null +++ b/packages/lex-agent/README.md @@ -0,0 +1,46 @@ +# @happyview/lex-agent + +Adapter that creates an [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex) `Agent` from a `HappyViewSession`. This lets you use `@atproto/lex`'s type-safe `Client` and `xrpc()` calls with HappyView's DPoP authentication. + +All XRPC requests made through the agent are routed to your HappyView instance. HappyView handles requests for its own lexicons locally and proxies standard AT Protocol methods (e.g., `com.atproto.repo.createRecord`) to the user's PDS. + +## Installation + +```bash +npm install @happyview/lex-agent @atproto/lex +``` + +`@atproto/lex` is a peer dependency (`>=0.0.20`). + +## Usage + +```typescript +import { Client } from "@atproto/lex"; +import { HappyViewBrowserClient } from "@happyview/oauth-client-browser"; +import { createAgent } from "@happyview/lex-agent"; + +// Authenticate with HappyView +const client = new HappyViewBrowserClient({ + instanceUrl: "https://happyview.example.com", + clientKey: "hvc_your_client_key", +}); +const session = await client.restore(); + +// Create a Lex agent from the session +const agent = createAgent(session); +const lex = new Client(agent); + +// Make type-safe XRPC calls +const game = await lex.xrpc(myLexicons.com.example.getGame, { + params: { slug: "celeste" }, +}); +``` + +## API + +### `createAgent(session: HappyViewSession): Agent` + +Creates an `@atproto/lex` `Agent` from a `HappyViewSession`. The returned agent: + +- Exposes the session's DID via `agent.did` +- Delegates all fetch requests to `session.fetchHandler`, which attaches DPoP authentication headers and prepends the HappyView instance URL diff --git a/packages/oauth-client-browser/LICENSE.md b/packages/oauth-client-browser/LICENSE.md new file mode 100644 --- /dev/null +++ b/packages/oauth-client-browser/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Lexicon Community + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/oauth-client-browser/README.md b/packages/oauth-client-browser/README.md new file mode 100644 --- /dev/null +++ b/packages/oauth-client-browser/README.md @@ -0,0 +1,89 @@ +# @happyview/oauth-client-browser + +Browser OAuth client for authenticating with a [HappyView](https://github.com/gamesgamesgamesgamesgames/happyview) instance using AT Protocol. + +Built on top of [`@happyview/oauth-client`](https://www.npmjs.com/package/@happyview/oauth-client) with Web Crypto and localStorage adapters included. + +## Installation + +```bash +npm install @happyview/oauth-client-browser +``` + +## Usage + +### Setup + +```typescript +import { HappyViewBrowserClient } from "@happyview/oauth-client-browser"; + +const client = new HappyViewBrowserClient({ + instanceUrl: "https://happyview.example.com", + clientKey: "hvc_your_client_key", +}); +``` + +### Login + +Redirects the user to their PDS authorization server: + +```typescript +await client.login("alice.bsky.social"); +// User is redirected to their PDS for authorization +``` + +If you need the authorization URL without an immediate redirect (e.g., to open in a popup), use `prepareLogin`: + +```typescript +const { authorizationUrl, did, state } = + await client.prepareLogin("alice.bsky.social"); +``` + +### OAuth Callback + +On the `/oauth/callback` route, call `callback()` to complete the token exchange: + +```typescript +const session = await client.callback(); +// Session is now stored in localStorage +``` + +### Restore Session + +On subsequent page loads, restore the session from localStorage: + +```typescript +const session = await client.restore(); +if (session) { + // User is still logged in +} +``` + +### Authenticated Requests + +The session's `fetchHandler` attaches DPoP proof headers automatically. Pass it a path (relative to the HappyView instance) or a full URL: + +```typescript +const response = await session.fetchHandler( + "/xrpc/com.example.getStuff?limit=10", + { method: "GET" }, +); +``` + +### Logout + +```typescript +await client.logout("did:plc:abc123"); +``` + +## Exports + +This package re-exports everything from `@happyview/oauth-client`, plus: + +- `HappyViewBrowserClient` -- the main browser client +- `LocalStorageAdapter` -- `StorageAdapter` backed by `window.localStorage` +- `WebCryptoAdapter` -- `CryptoAdapter` backed by the Web Crypto API +- `resolveHandleToDid` -- resolve an AT Protocol handle to a DID +- `resolveDidDocument` -- fetch a DID document +- `resolvePdsUrl` -- extract the PDS URL from a DID document +- `resolveAuthServerMetadata` -- fetch OAuth authorization server metadata from a PDS diff --git a/packages/oauth-client/LICENSE.md b/packages/oauth-client/LICENSE.md new file mode 100644 --- /dev/null +++ b/packages/oauth-client/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Lexicon Community + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/oauth-client/README.md b/packages/oauth-client/README.md new file mode 100644 --- /dev/null +++ b/packages/oauth-client/README.md @@ -0,0 +1,111 @@ +# @happyview/oauth-client + +Core OAuth client for authenticating with a [HappyView](https://github.com/gamesgamesgamesgamesgames/happyview) instance. + +This is a platform-agnostic package. If you're building a browser app, use [`@happyview/oauth-client-browser`](https://www.npmjs.com/package/@happyview/oauth-client-browser) instead. It wraps this package with Web Crypto, localStorage, and a complete OAuth redirect flow. + +## Installation + +```bash +npm install @happyview/oauth-client +``` + +## Usage + +`HappyViewOAuthClient` manages DPoP key provisioning, session registration, and session restoration lifecycle. You provide a `CryptoAdapter` and optional `StorageAdapter` for your platform. + +```typescript +import { HappyViewOAuthClient } from "@happyview/oauth-client"; + +const client = new HappyViewOAuthClient({ + instanceUrl: "https://happyview.example.com", + clientKey: "hvc_your_client_key", + clientSecret: "hvs_your_secret", // optional, for confidential clients (server-to-server) + crypto: myCryptoAdapter, + storage: myStorageAdapter, // optional, defaults to in-memory +}); +``` + +### DPoP Key Provisioning + +Request a DPoP keypair from the HappyView instance: + +```typescript +const { provisionId, dpopKey, pkceVerifier } = await client.provisionDpopKey(); +``` + +### Session Registration + +After completing OAuth authorization with the user's PDS, register the session with HappyView: + +```typescript +const session = await client.registerSession({ + provisionId, + pkceVerifier, + did: "did:plc:abc123", + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + scopes: "atproto", + pdsUrl: "https://pds.example.com", + issuer: tokens.iss, + dpopKey, +}); +``` + +### Making Authenticated Requests + +The returned `HappyViewSession` provides a `fetchHandler` that automatically attaches DPoP proof headers: + +```typescript +const response = await session.fetchHandler("/xrpc/com.example.getStuff", { + method: "GET", +}); +``` + +### Session Restoration + +Restore a previously stored session: + +```typescript +// Restore the last active session +const session = await client.restore(); + +// Or restore a specific user's session +const session = await client.restoreSession("did:plc:abc123"); +``` + +### Logout + +```typescript +await client.deleteSession("did:plc:abc123"); +``` + +## Adapters + +### CryptoAdapter + +Implement this interface for your platform's cryptographic primitives: + +```typescript +interface CryptoAdapter { + generatePkceVerifier(): Promise; + computePkceChallenge(verifier: string): Promise; + signEs256(privateKey: JsonWebKey, payload: Uint8Array): Promise; + sha256(data: Uint8Array): Promise; + getRandomValues(length: number): Uint8Array; +} +``` + +### StorageAdapter + +Implement this interface to persist sessions across restarts: + +```typescript +interface StorageAdapter { + get(key: string): Promise; + set(key: string, value: string): Promise; + delete(key: string): Promise; +} +``` + +If no `StorageAdapter` is provided, sessions are stored in memory and will not survive page reloads or process restarts.