···11+# node-client-public-example
22+33+a simple CLI demonstrating [`@atcute/oauth-node-client`](../node-client) with a public (loopback)
44+client.
55+66+this example shows how to authenticate with AT Protocol OAuth without needing to set up a
77+confidential client with keys. it's useful for CLI tools, local development, and testing.
88+99+## usage
1010+1111+```bash
1212+# authenticate and show your profile
1313+bun run start alice.bsky.social
1414+1515+# or use a DID
1616+bun run start did:plc:z72i7hdynmk6r22z27h6tvur
1717+```
1818+1919+the CLI will:
2020+2121+1. start a local callback server on a random port
2222+2. print an authorization URL for you to open
2323+3. wait for you to authorize the app
2424+4. fetch and display your profile information
2525+2626+## how it works
2727+2828+loopback clients use `http://localhost` as the client origin, which the OAuth server recognizes as a
2929+public client. no keys or client registration are required.
3030+3131+```typescript
3232+import { OAuthClient, scope } from '@atcute/oauth-node-client';
3333+3434+const oauth = new OAuthClient({
3535+ metadata: {
3636+ // no client_id needed - it's built automatically from redirect_uris and scope
3737+ redirect_uris: ['http://127.0.0.1:PORT/callback'],
3838+ scope: [scope.rpc({ lxm: ['app.bsky.actor.getProfile'], aud: '*' })],
3939+ },
4040+ // no keyset - this makes it a public client
4141+ actorResolver: /* ... */,
4242+ stores: /* ... */,
4343+});
4444+```
4545+4646+the library automatically builds the `client_id` from the redirect URIs and scope.
···11# @atcute/oauth-node-client
2233-atproto OAuth client for Node.js (plus Deno, Bun, and other server runtimes). this package
44-implements a **confidential client** that authenticates using `private_key_jwt`.
33+atproto OAuth client for Node.js (plus Deno, Bun, and other server runtimes).
44+55+supports both:
66+77+- **confidential clients** - authenticate with `private_key_jwt`, longer session lifetimes (up to 180
88+ days), requires key management and hosted metadata
99+- **public clients** - no authentication (`token_endpoint_auth_method: 'none'`), shorter sessions (2
1010+ weeks max), simpler setup for CLI tools and local development
511612```sh
713npm install @atcute/oauth-node-client
814```
9151010-## usage
1616+## confidential clients
11171218examples below use Hono, but any web framework works.
1313-1414-```ts
1515-import { Hono } from 'hono';
1616-1717-const app = new Hono();
1818-```
19192020### key management
2121···247247```ts
248248await session.signOut();
249249```
250250+251251+## public clients
252252+253253+public clients don't require key management or hosted metadata. they're ideal for CLI tools, local
254254+development, and native apps. the tradeoff is shorter session lifetimes (2 weeks max vs 180 days for
255255+confidential clients).
256256+257257+### loopback clients
258258+259259+loopback clients use `http://localhost` as their origin, which authorization servers recognize as a
260260+public client. no client registration or hosted metadata is required - the library builds the
261261+`client_id` automatically from your redirect URIs and scopes.
262262+263263+```ts
264264+import { MemoryStore, OAuthClient, scope, type StoredState } from '@atcute/oauth-node-client';
265265+import {
266266+ CompositeDidDocumentResolver,
267267+ CompositeHandleResolver,
268268+ LocalActorResolver,
269269+ PlcDidDocumentResolver,
270270+ WebDidDocumentResolver,
271271+ WellKnownHandleResolver,
272272+} from '@atcute/identity-resolver';
273273+import { NodeDnsHandleResolver } from '@atcute/identity-resolver-node';
274274+275275+// use any available port for the callback server
276276+const port = 8080;
277277+const redirectUri = `http://127.0.0.1:${port}/callback`;
278278+279279+const oauth = new OAuthClient({
280280+ metadata: {
281281+ // no client_id needed - built automatically as:
282282+ // http://localhost?redirect_uri=http://127.0.0.1:8080/callback&scope=...
283283+ redirect_uris: [redirectUri],
284284+ scope: [scope.rpc({ lxm: ['app.bsky.actor.getProfile'], aud: '*' })],
285285+ },
286286+ // no keyset - this makes it a public client
287287+288288+ stores: {
289289+ sessions: new MemoryStore(),
290290+ states: new MemoryStore<string, StoredState>({ maxSize: 10, ttl: 10 * 60_000 }),
291291+ },
292292+293293+ actorResolver: new LocalActorResolver({
294294+ handleResolver: new CompositeHandleResolver({
295295+ methods: {
296296+ dns: new NodeDnsHandleResolver(),
297297+ http: new WellKnownHandleResolver(),
298298+ },
299299+ }),
300300+ didDocumentResolver: new CompositeDidDocumentResolver({
301301+ methods: {
302302+ plc: new PlcDidDocumentResolver(),
303303+ web: new WebDidDocumentResolver(),
304304+ },
305305+ }),
306306+ }),
307307+});
308308+```
309309+310310+loopback redirect URIs must use `127.0.0.1` or `[::1]` (not `localhost`). the port can be any
311311+available port - authorization servers ignore the port when matching loopback redirect URIs per RFC
312312+8252.
313313+314314+see the [node-client-public-example](../node-client-public-example) package for a complete CLI
315315+example.
316316+317317+### discoverable public clients
318318+319319+for public clients that need a discoverable `client_id` (e.g., mobile apps or web apps without a
320320+backend), provide a `client_id` URL pointing to hosted metadata:
321321+322322+```ts
323323+const oauth = new OAuthClient({
324324+ metadata: {
325325+ client_id: 'https://example.com/oauth-client-metadata.json',
326326+ redirect_uris: ['https://example.com/callback'],
327327+ scope: 'atproto',
328328+ },
329329+ stores: { /* ... */ },
330330+ actorResolver: /* ... */,
331331+});
332332+```
333333+334334+the hosted metadata should set `token_endpoint_auth_method: 'none'` and omit `jwks`/`jwks_uri`.
250335251336## custom stores
252337
+11
packages/oauth/node-client/lib/index.ts
···991010export {
1111 buildClientMetadata,
1212+ buildPublicClientMetadata,
1213 scope,
1314 type AtprotoAuthorizationServerMetadata,
1415 type AtprotoProtectedResourceMetadata,
1516 type ConfidentialClientMetadata,
1717+ type DiscoverablePublicClientMetadata,
1818+ type LoopbackClientMetadata,
1619 type OAuthAuthorizationServerMetadata,
1720 type OAuthClientMetadata,
1821 type OAuthProtectedResourceMetadata,
1922 type OAuthResponseMode,
2323+ type PublicClientMetadata,
2024} from '@atcute/oauth-types';
21252226export {
···2630 type AuthorizeTarget,
2731 type CallbackOptions,
2832 type CallbackResult,
3333+ type ConfidentialOAuthClientOptions,
2934 type OAuthClientOptions,
3035 type OAuthClientStores,
3636+ type PublicOAuthClientOptions,
3137 type RestoreOptions,
3238} from './oauth-client.js';
3339···50565157export type { AuthorizationServerMetadataCache } from './resolvers/authorization-server-metadata.js';
5258export type { ProtectedResourceMetadataCache } from './resolvers/protected-resource-metadata.js';
5959+export type {
6060+ ClientAuthMethod,
6161+ ConfidentialClientAuthMethod,
6262+ PublicClientAuthMethod,
6363+} from './oauth-client-auth.js';
5364export type { SessionStore, StoredSession } from './types/sessions.js';
5465export type { StateStore, StoredState } from './types/states.js';
5566export type { TokenSet } from './types/token-set.js';