@atproto/lex-server 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +94 -0
- package/dist/errors.js.map +1 -1
- package/dist/lex-server.d.ts +481 -2
- package/dist/lex-server.d.ts.map +1 -1
- package/dist/lex-server.js +103 -0
- package/dist/lex-server.js.map +1 -1
- package/dist/lib/www-authenticate.d.ts +66 -0
- package/dist/lib/www-authenticate.d.ts.map +1 -1
- package/dist/lib/www-authenticate.js +28 -0
- package/dist/lib/www-authenticate.js.map +1 -1
- package/dist/nodejs.d.ts +279 -0
- package/dist/nodejs.d.ts.map +1 -1
- package/dist/nodejs.js +184 -1
- package/dist/nodejs.js.map +1 -1
- package/dist/service-auth.d.ts +151 -9
- package/dist/service-auth.d.ts.map +1 -1
- package/dist/service-auth.js +41 -2
- package/dist/service-auth.js.map +1 -1
- package/package.json +11 -11
- package/src/errors.ts +94 -0
- package/src/lex-server.test.ts +3 -2
- package/src/lex-server.ts +482 -2
- package/src/lib/www-authenticate.ts +66 -0
- package/src/nodejs.ts +280 -2
- package/src/service-auth.ts +151 -9
package/src/lex-server.ts
CHANGED
|
@@ -19,37 +19,140 @@ import {
|
|
|
19
19
|
import { drainWebsocket } from './lib/drain-websocket.js'
|
|
20
20
|
|
|
21
21
|
type Awaitable<T> = T | Promise<T>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Union type representing the supported Lexicon method types.
|
|
25
|
+
*
|
|
26
|
+
* - `Query`: Read-only methods invoked via HTTP GET
|
|
27
|
+
* - `Procedure`: Methods that may modify state, invoked via HTTP POST
|
|
28
|
+
* - `Subscription`: Real-time streaming methods over WebSocket
|
|
29
|
+
*/
|
|
22
30
|
export type LexMethod = Query | Procedure | Subscription
|
|
23
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Network address for TCP or UDP connections.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const addr: NetAddr = {
|
|
38
|
+
* hostname: '127.0.0.1',
|
|
39
|
+
* port: 3000,
|
|
40
|
+
* transport: 'tcp'
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
24
44
|
export type NetAddr = {
|
|
45
|
+
/** The hostname or IP address of the connection. */
|
|
25
46
|
hostname: string
|
|
47
|
+
/** The port number of the connection. */
|
|
26
48
|
port: number
|
|
49
|
+
/** The transport protocol used. */
|
|
27
50
|
transport: 'tcp' | 'udp'
|
|
28
51
|
}
|
|
29
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Unix domain socket address.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const addr: UnixAddr = {
|
|
59
|
+
* path: '/var/run/app.sock',
|
|
60
|
+
* transport: 'unix'
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
30
64
|
export type UnixAddr = {
|
|
65
|
+
/** The filesystem path to the Unix socket. */
|
|
31
66
|
path: string
|
|
67
|
+
/** The transport protocol used. */
|
|
32
68
|
transport: 'unix' | 'unixpacket'
|
|
33
69
|
}
|
|
34
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Union type for all supported address types.
|
|
73
|
+
*
|
|
74
|
+
* Can be a network address ({@link NetAddr}), Unix socket address ({@link UnixAddr}),
|
|
75
|
+
* or `undefined` when the address is not available.
|
|
76
|
+
*/
|
|
35
77
|
export type Addr = NetAddr | UnixAddr | undefined
|
|
36
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Metadata about the client connection for an incoming request.
|
|
81
|
+
*
|
|
82
|
+
* @typeParam A - The address type, defaults to {@link Addr}
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const info: ConnectionInfo<NetAddr> = {
|
|
87
|
+
* remoteAddr: { hostname: '192.168.1.1', port: 54321, transport: 'tcp' },
|
|
88
|
+
* completed: new Promise((resolve) => socket.on('close', resolve))
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
37
92
|
export type ConnectionInfo<A extends Addr = Addr> = {
|
|
93
|
+
/** The remote address of the client, if available. */
|
|
38
94
|
remoteAddr: A
|
|
95
|
+
/** Promise that resolves when the connection is fully closed. */
|
|
39
96
|
completed: Promise<void>
|
|
40
97
|
}
|
|
41
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Function signature for handling HTTP requests in the XRPC router.
|
|
101
|
+
*
|
|
102
|
+
* This is the standard fetch-style handler that processes incoming requests
|
|
103
|
+
* and returns responses. It is used both internally by the router and can
|
|
104
|
+
* be used to integrate with other HTTP frameworks.
|
|
105
|
+
*
|
|
106
|
+
* @param request - The incoming HTTP request
|
|
107
|
+
* @param connection - Optional connection metadata including remote address
|
|
108
|
+
* @returns A promise resolving to the HTTP response
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const handler: FetchHandler = async (request, connection) => {
|
|
113
|
+
* console.log('Request from:', connection?.remoteAddr)
|
|
114
|
+
* return new Response('Hello, World!')
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
42
118
|
export type FetchHandler = (
|
|
43
119
|
request: Request,
|
|
44
120
|
connection?: ConnectionInfo,
|
|
45
121
|
) => Promise<Response>
|
|
46
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Context object passed to XRPC method handlers.
|
|
125
|
+
*
|
|
126
|
+
* Contains all the information needed to process a request, including
|
|
127
|
+
* parsed parameters, authentication credentials, and the raw request object.
|
|
128
|
+
*
|
|
129
|
+
* @typeParam Method - The Lexicon method type (Query, Procedure, or Subscription)
|
|
130
|
+
* @typeParam Credentials - The type of authentication credentials, determined by the auth handler
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const handler: LexRouterMethodHandler<MyMethod, UserCredentials> = async (ctx) => {
|
|
135
|
+
* const { credentials, params, input, signal } = ctx
|
|
136
|
+
* // credentials.userId is available if auth handler returns UserCredentials
|
|
137
|
+
* // params contains validated query parameters
|
|
138
|
+
* // input contains the request body (for procedures)
|
|
139
|
+
* // signal can be used to abort long-running operations
|
|
140
|
+
* return { body: { result: 'success' } }
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
47
144
|
export type LexRouterHandlerContext<Method extends LexMethod, Credentials> = {
|
|
145
|
+
/** Authentication credentials returned by the auth handler. */
|
|
48
146
|
credentials: Credentials
|
|
147
|
+
/** Parsed and validated request input (body for procedures, undefined for queries). */
|
|
49
148
|
input: InferMethodInput<Method, Body>
|
|
149
|
+
/** Parsed and validated URL query parameters. */
|
|
50
150
|
params: InferMethodParams<Method>
|
|
151
|
+
/** The original HTTP request object. */
|
|
51
152
|
request: Request
|
|
153
|
+
/** Abort signal that is triggered when the request is cancelled. */
|
|
52
154
|
signal: AbortSignal
|
|
155
|
+
/** Connection metadata including remote address. */
|
|
53
156
|
connection?: ConnectionInfo
|
|
54
157
|
}
|
|
55
158
|
|
|
@@ -57,6 +160,35 @@ type AsOptionalPayloadOptions<T> = T extends undefined | void
|
|
|
57
160
|
? { encoding?: undefined; body?: undefined }
|
|
58
161
|
: T
|
|
59
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Return type for XRPC method handlers (queries and procedures).
|
|
165
|
+
*
|
|
166
|
+
* Handlers can return either:
|
|
167
|
+
* - A raw {@link Response} object for full control over the HTTP response
|
|
168
|
+
* - An object with `body`, optional `encoding`, and optional `headers`
|
|
169
|
+
*
|
|
170
|
+
* For JSON methods, the body is automatically serialized. For other encodings,
|
|
171
|
+
* the body must be a valid {@link BodyInit} type.
|
|
172
|
+
*
|
|
173
|
+
* @typeParam Method - The Lexicon method type (Query or Procedure)
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* // Return JSON body (most common)
|
|
178
|
+
* return { body: { users: [...] } }
|
|
179
|
+
*
|
|
180
|
+
* // Return with custom headers
|
|
181
|
+
* return {
|
|
182
|
+
* body: { data: 'value' },
|
|
183
|
+
* headers: { 'Cache-Control': 'max-age=3600' }
|
|
184
|
+
* }
|
|
185
|
+
*
|
|
186
|
+
* // Return raw Response for full control
|
|
187
|
+
* return new Response(binaryData, {
|
|
188
|
+
* headers: { 'Content-Type': 'application/octet-stream' }
|
|
189
|
+
* })
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
60
192
|
export type LexRouterHandlerOutput<Method extends Query | Procedure> =
|
|
61
193
|
| Response
|
|
62
194
|
| ({
|
|
@@ -69,6 +201,23 @@ export type LexRouterHandlerOutput<Method extends Query | Procedure> =
|
|
|
69
201
|
}
|
|
70
202
|
: AsOptionalPayloadOptions<InferMethodOutput<Method, BodyInit>>))
|
|
71
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Handler function for XRPC query and procedure methods.
|
|
206
|
+
*
|
|
207
|
+
* Receives a context object with request details and credentials,
|
|
208
|
+
* and returns either a Response or a structured output object.
|
|
209
|
+
*
|
|
210
|
+
* @typeParam Method - The Lexicon method type (Query or Procedure)
|
|
211
|
+
* @typeParam Credentials - The type of authentication credentials
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const getProfile: LexRouterMethodHandler<GetProfileMethod, UserCredentials> = async (ctx) => {
|
|
216
|
+
* const profile = await db.getProfile(ctx.params.actor)
|
|
217
|
+
* return { body: profile }
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
72
221
|
export type LexRouterMethodHandler<
|
|
73
222
|
Method extends Query | Procedure = Query | Procedure,
|
|
74
223
|
Credentials = unknown,
|
|
@@ -76,14 +225,56 @@ export type LexRouterMethodHandler<
|
|
|
76
225
|
ctx: LexRouterHandlerContext<Method, Credentials>,
|
|
77
226
|
) => Awaitable<LexRouterHandlerOutput<Method>>
|
|
78
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Configuration object for registering an XRPC method with authentication.
|
|
230
|
+
*
|
|
231
|
+
* Used when you need to specify both a handler and an auth function.
|
|
232
|
+
*
|
|
233
|
+
* @typeParam Method - The Lexicon method type (Query or Procedure)
|
|
234
|
+
* @typeParam Credentials - The type of authentication credentials
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const config: LexRouterMethodConfig<GetProfileMethod, UserCredentials> = {
|
|
239
|
+
* handler: async (ctx) => {
|
|
240
|
+
* return { body: await getProfile(ctx.params.actor) }
|
|
241
|
+
* },
|
|
242
|
+
* auth: async ({ request }) => {
|
|
243
|
+
* return verifyToken(request.headers.get('authorization'))
|
|
244
|
+
* }
|
|
245
|
+
* }
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
79
248
|
export type LexRouterMethodConfig<
|
|
80
249
|
Method extends Query | Procedure = Query | Procedure,
|
|
81
250
|
Credentials = unknown,
|
|
82
251
|
> = {
|
|
252
|
+
/** The handler function that processes the request. */
|
|
83
253
|
handler: LexRouterMethodHandler<Method, Credentials>
|
|
254
|
+
/** Authentication function that validates credentials before the handler runs. */
|
|
84
255
|
auth: LexRouterAuth<Credentials, Method>
|
|
85
256
|
}
|
|
86
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Handler function for XRPC subscription methods (WebSocket streams).
|
|
260
|
+
*
|
|
261
|
+
* Returns an async iterable that yields messages to be sent over the WebSocket.
|
|
262
|
+
* The connection remains open until the iterable completes or an error occurs.
|
|
263
|
+
*
|
|
264
|
+
* @typeParam Method - The Lexicon subscription method type
|
|
265
|
+
* @typeParam Credentials - The type of authentication credentials
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```typescript
|
|
269
|
+
* const subscribeRepos: LexRouterSubscriptionHandler<SubscribeReposMethod> = async function* (ctx) {
|
|
270
|
+
* const cursor = ctx.params.cursor ?? 0
|
|
271
|
+
* for await (const event of eventStream.since(cursor)) {
|
|
272
|
+
* if (ctx.signal.aborted) break
|
|
273
|
+
* yield { $type: 'com.atproto.sync.subscribeRepos#commit', ...event }
|
|
274
|
+
* }
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
87
278
|
export type LexRouterSubscriptionHandler<
|
|
88
279
|
Method extends Subscription = Subscription,
|
|
89
280
|
Credentials = unknown,
|
|
@@ -91,65 +282,325 @@ export type LexRouterSubscriptionHandler<
|
|
|
91
282
|
ctx: LexRouterHandlerContext<Method, Credentials>,
|
|
92
283
|
) => AsyncIterable<InferMethodMessage<Method>>
|
|
93
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Configuration object for registering an XRPC subscription with authentication.
|
|
287
|
+
*
|
|
288
|
+
* Used when you need to specify both a handler and an auth function for subscriptions.
|
|
289
|
+
*
|
|
290
|
+
* @typeParam Method - The Lexicon subscription method type
|
|
291
|
+
* @typeParam Credentials - The type of authentication credentials
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const config: LexRouterSubscriptionConfig<SubscribeReposMethod, ServiceCredentials> = {
|
|
296
|
+
* handler: async function* (ctx) {
|
|
297
|
+
* for await (const event of eventStream) {
|
|
298
|
+
* yield event
|
|
299
|
+
* }
|
|
300
|
+
* },
|
|
301
|
+
* auth: async ({ request }) => {
|
|
302
|
+
* return verifyServiceAuth(request)
|
|
303
|
+
* }
|
|
304
|
+
* }
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
94
307
|
export type LexRouterSubscriptionConfig<
|
|
95
308
|
Method extends Subscription = Subscription,
|
|
96
309
|
Credentials = unknown,
|
|
97
310
|
> = {
|
|
311
|
+
/** The handler function that yields subscription messages. */
|
|
98
312
|
handler: LexRouterSubscriptionHandler<Method, Credentials>
|
|
313
|
+
/** Authentication function that validates credentials before the handler runs. */
|
|
99
314
|
auth: LexRouterAuth<Credentials, Method>
|
|
100
315
|
}
|
|
101
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Context object passed to authentication handlers.
|
|
319
|
+
*
|
|
320
|
+
* Contains the information needed to authenticate a request before
|
|
321
|
+
* the main handler is invoked.
|
|
322
|
+
*
|
|
323
|
+
* @typeParam Method - The Lexicon method type
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* const authHandler: LexRouterAuth<UserCredentials> = async (ctx) => {
|
|
328
|
+
* const token = ctx.request.headers.get('authorization')
|
|
329
|
+
* if (!token) throw new LexError('AuthenticationRequired', 'Missing token')
|
|
330
|
+
* return { userId: await verifyToken(token) }
|
|
331
|
+
* }
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
102
334
|
export type LexRouterAuthContext<Method extends LexMethod = LexMethod> = {
|
|
335
|
+
/** The Lexicon method definition being called. */
|
|
103
336
|
method: Method
|
|
337
|
+
/** Parsed and validated URL query parameters. */
|
|
104
338
|
params: InferMethodParams<Method>
|
|
339
|
+
/** The original HTTP request object. */
|
|
105
340
|
request: Request
|
|
341
|
+
/** Connection metadata including remote address. */
|
|
106
342
|
connection?: ConnectionInfo
|
|
107
343
|
}
|
|
108
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Authentication handler function for XRPC methods.
|
|
347
|
+
*
|
|
348
|
+
* Called before the main handler to validate authentication credentials.
|
|
349
|
+
* Should return the validated credentials or throw an error if authentication fails.
|
|
350
|
+
*
|
|
351
|
+
* @typeParam Credentials - The type of credentials to return on success
|
|
352
|
+
* @typeParam Method - The Lexicon method type
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```typescript
|
|
356
|
+
* // Simple token-based auth
|
|
357
|
+
* const tokenAuth: LexRouterAuth<{ userId: string }> = async ({ request }) => {
|
|
358
|
+
* const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
|
359
|
+
* if (!token) throw new LexError('AuthenticationRequired', 'Token required')
|
|
360
|
+
* const userId = await verifyToken(token)
|
|
361
|
+
* return { userId }
|
|
362
|
+
* }
|
|
363
|
+
*
|
|
364
|
+
* // Using with serviceAuth for AT Protocol service authentication
|
|
365
|
+
* import { serviceAuth } from '@atproto/lex-server'
|
|
366
|
+
* const auth = serviceAuth({ audience: 'did:web:example.com', unique: checkNonce })
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
109
369
|
export type LexRouterAuth<
|
|
110
370
|
Credentials = unknown,
|
|
111
371
|
Method extends LexMethod = LexMethod,
|
|
112
372
|
> = (ctx: LexRouterAuthContext<Method>) => Credentials | Promise<Credentials>
|
|
113
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Context object passed to error handler callbacks.
|
|
376
|
+
*
|
|
377
|
+
* Used for logging and monitoring errors that occur during request handling.
|
|
378
|
+
*/
|
|
114
379
|
export type LexErrorHandlerContext = {
|
|
380
|
+
/** The error that was thrown during handling. */
|
|
115
381
|
error: unknown
|
|
382
|
+
/** The original HTTP request that triggered the error. */
|
|
116
383
|
request: Request
|
|
384
|
+
/** The Lexicon method that was being executed. */
|
|
117
385
|
method: LexMethod
|
|
118
386
|
}
|
|
119
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Function that upgrades an HTTP request to a WebSocket connection.
|
|
390
|
+
*
|
|
391
|
+
* This is platform-specific: Deno provides this natively, while Node.js
|
|
392
|
+
* requires the `upgradeWebSocket` function from this package.
|
|
393
|
+
*
|
|
394
|
+
* @param request - The HTTP request to upgrade
|
|
395
|
+
* @returns An object containing the WebSocket and the upgrade response
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* // In Node.js, use the provided upgradeWebSocket function
|
|
400
|
+
* import { upgradeWebSocket } from '@atproto/lex-server/nodejs'
|
|
401
|
+
*
|
|
402
|
+
* const router = new LexRouter({ upgradeWebSocket })
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
120
405
|
export type UpgradeWebSocket = (request: Request) => {
|
|
406
|
+
/** The WebSocket instance for bidirectional communication. */
|
|
121
407
|
socket: WebSocket
|
|
408
|
+
/** The HTTP response to return (101 Switching Protocols). */
|
|
122
409
|
response: Response
|
|
123
410
|
}
|
|
124
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Configuration options for the {@link LexRouter}.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```typescript
|
|
417
|
+
* const options: LexRouterOptions = {
|
|
418
|
+
* upgradeWebSocket,
|
|
419
|
+
* onHandlerError: async ({ error, request, method }) => {
|
|
420
|
+
* console.error(`Error in ${method.nsid}:`, error)
|
|
421
|
+
* await reportToSentry(error)
|
|
422
|
+
* },
|
|
423
|
+
* highWaterMark: 64 * 1024, // 64KB
|
|
424
|
+
* lowWaterMark: 16 * 1024 // 16KB
|
|
425
|
+
* }
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
125
428
|
export type LexRouterOptions = {
|
|
429
|
+
/**
|
|
430
|
+
* Function to upgrade HTTP requests to WebSocket connections.
|
|
431
|
+
* Required for subscription methods. Defaults to Deno's built-in
|
|
432
|
+
* upgradeWebSocket if available.
|
|
433
|
+
*/
|
|
126
434
|
upgradeWebSocket?: UpgradeWebSocket
|
|
435
|
+
/**
|
|
436
|
+
* Callback invoked when an error occurs during request handling.
|
|
437
|
+
* Useful for logging and error reporting. Not called for client-induced
|
|
438
|
+
* errors (e.g., request abortion).
|
|
439
|
+
*/
|
|
127
440
|
onHandlerError?: (ctx: LexErrorHandlerContext) => void | Promise<void>
|
|
441
|
+
/**
|
|
442
|
+
* High water mark for WebSocket backpressure (in bytes).
|
|
443
|
+
* When buffered data exceeds this, the handler will wait before sending more.
|
|
444
|
+
*/
|
|
128
445
|
highWaterMark?: number
|
|
446
|
+
/**
|
|
447
|
+
* Low water mark for WebSocket backpressure (in bytes).
|
|
448
|
+
* The handler resumes sending when buffered data drops below this.
|
|
449
|
+
*/
|
|
129
450
|
lowWaterMark?: number
|
|
130
451
|
}
|
|
131
452
|
|
|
453
|
+
/**
|
|
454
|
+
* XRPC router for handling AT Protocol Lexicon methods.
|
|
455
|
+
*
|
|
456
|
+
* The router handles HTTP routing, parameter parsing, input validation,
|
|
457
|
+
* authentication, and response serialization for XRPC methods. It supports
|
|
458
|
+
* queries (GET), procedures (POST), and subscriptions (WebSocket).
|
|
459
|
+
*
|
|
460
|
+
* @example Setting up a basic XRPC server
|
|
461
|
+
* ```typescript
|
|
462
|
+
* import { LexRouter } from '@atproto/lex-server'
|
|
463
|
+
* import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
|
|
464
|
+
* import { getProfile, createPost, subscribeRepos } from './lexicons'
|
|
465
|
+
*
|
|
466
|
+
* const router = new LexRouter({ upgradeWebSocket })
|
|
467
|
+
*
|
|
468
|
+
* // Register a query handler (GET request)
|
|
469
|
+
* router.add(getProfile, async (ctx) => {
|
|
470
|
+
* const profile = await db.getProfile(ctx.params.actor)
|
|
471
|
+
* return { body: profile }
|
|
472
|
+
* })
|
|
473
|
+
*
|
|
474
|
+
* // Register a procedure handler with authentication (POST request)
|
|
475
|
+
* router.add(createPost, {
|
|
476
|
+
* handler: async (ctx) => {
|
|
477
|
+
* const post = await db.createPost(ctx.credentials.did, ctx.input.body)
|
|
478
|
+
* return { body: { uri: post.uri, cid: post.cid } }
|
|
479
|
+
* },
|
|
480
|
+
* auth: async ({ request }) => {
|
|
481
|
+
* return verifyAccessToken(request)
|
|
482
|
+
* }
|
|
483
|
+
* })
|
|
484
|
+
*
|
|
485
|
+
* // Register a subscription handler (WebSocket)
|
|
486
|
+
* router.add(subscribeRepos, async function* (ctx) {
|
|
487
|
+
* for await (const event of eventStream.since(ctx.params.cursor)) {
|
|
488
|
+
* if (ctx.signal.aborted) break
|
|
489
|
+
* yield event
|
|
490
|
+
* }
|
|
491
|
+
* })
|
|
492
|
+
*
|
|
493
|
+
* // Start the server
|
|
494
|
+
* const server = await serve(router, { port: 3000 })
|
|
495
|
+
* console.log('XRPC server listening on port 3000')
|
|
496
|
+
* ```
|
|
497
|
+
*
|
|
498
|
+
* @example Using with service authentication
|
|
499
|
+
* ```typescript
|
|
500
|
+
* import { LexRouter, serviceAuth } from '@atproto/lex-server'
|
|
501
|
+
*
|
|
502
|
+
* const router = new LexRouter()
|
|
503
|
+
*
|
|
504
|
+
* const auth = serviceAuth({
|
|
505
|
+
* audience: 'did:web:api.example.com',
|
|
506
|
+
* unique: async (nonce) => {
|
|
507
|
+
* // Check and record nonce uniqueness
|
|
508
|
+
* return await nonceStore.checkAndAdd(nonce)
|
|
509
|
+
* }
|
|
510
|
+
* })
|
|
511
|
+
*
|
|
512
|
+
* router.add(protectedMethod, {
|
|
513
|
+
* handler: async (ctx) => {
|
|
514
|
+
* // ctx.credentials contains { did, didDocument, jwt }
|
|
515
|
+
* return { body: { callerDid: ctx.credentials.did } }
|
|
516
|
+
* },
|
|
517
|
+
* auth
|
|
518
|
+
* })
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
132
521
|
export class LexRouter {
|
|
133
|
-
|
|
134
|
-
|
|
522
|
+
/** Map of NSID strings to their fetch handlers. */
|
|
523
|
+
readonly handlers: Map<NsidString, FetchHandler> = new Map()
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Creates a new XRPC router.
|
|
527
|
+
*
|
|
528
|
+
* @param options - Router configuration options
|
|
529
|
+
*/
|
|
135
530
|
constructor(readonly options: LexRouterOptions = {}) {}
|
|
136
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Registers a subscription handler without authentication.
|
|
534
|
+
*
|
|
535
|
+
* @param ns - The Lexicon namespace definition for the subscription
|
|
536
|
+
* @param handler - Async generator function that yields subscription messages
|
|
537
|
+
* @returns This router instance for chaining
|
|
538
|
+
*/
|
|
137
539
|
add<M extends Subscription>(
|
|
138
540
|
ns: Main<M>,
|
|
139
541
|
handler: LexRouterSubscriptionHandler<M, void>,
|
|
140
542
|
): this
|
|
543
|
+
/**
|
|
544
|
+
* Registers a subscription handler with authentication.
|
|
545
|
+
*
|
|
546
|
+
* @param ns - The Lexicon namespace definition for the subscription
|
|
547
|
+
* @param config - Configuration object with handler and auth function
|
|
548
|
+
* @returns This router instance for chaining
|
|
549
|
+
*/
|
|
141
550
|
add<M extends Subscription, Credentials>(
|
|
142
551
|
ns: Main<M>,
|
|
143
552
|
config: LexRouterSubscriptionConfig<M, Credentials>,
|
|
144
553
|
): this
|
|
554
|
+
/**
|
|
555
|
+
* Registers a query or procedure handler without authentication.
|
|
556
|
+
*
|
|
557
|
+
* @param ns - The Lexicon namespace definition for the method
|
|
558
|
+
* @param handler - Handler function that processes requests
|
|
559
|
+
* @returns This router instance for chaining
|
|
560
|
+
*/
|
|
145
561
|
add<M extends Query | Procedure>(
|
|
146
562
|
ns: Main<M>,
|
|
147
563
|
handler: LexRouterMethodHandler<M, void>,
|
|
148
564
|
): this
|
|
565
|
+
/**
|
|
566
|
+
* Registers a query or procedure handler with authentication.
|
|
567
|
+
*
|
|
568
|
+
* @param ns - The Lexicon namespace definition for the method
|
|
569
|
+
* @param config - Configuration object with handler and auth function
|
|
570
|
+
* @returns This router instance for chaining
|
|
571
|
+
*/
|
|
149
572
|
add<M extends Query | Procedure, Credentials>(
|
|
150
573
|
ns: Main<M>,
|
|
151
574
|
config: LexRouterMethodConfig<M, Credentials>,
|
|
152
575
|
): this
|
|
576
|
+
/**
|
|
577
|
+
* Registers a Lexicon method handler.
|
|
578
|
+
*
|
|
579
|
+
* This is the unified overload that accepts any method type with optional authentication.
|
|
580
|
+
*
|
|
581
|
+
* @param ns - The Lexicon namespace definition
|
|
582
|
+
* @param config - Handler function or configuration object
|
|
583
|
+
* @returns This router instance for chaining
|
|
584
|
+
*
|
|
585
|
+
* @throws {TypeError} If a method with the same NSID is already registered
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```typescript
|
|
589
|
+
* // Register without auth (credentials will be void)
|
|
590
|
+
* router.add(myQuery, async (ctx) => {
|
|
591
|
+
* return { body: { data: 'value' } }
|
|
592
|
+
* })
|
|
593
|
+
*
|
|
594
|
+
* // Register with auth
|
|
595
|
+
* router.add(myProcedure, {
|
|
596
|
+
* handler: async (ctx) => {
|
|
597
|
+
* console.log('Caller:', ctx.credentials.userId)
|
|
598
|
+
* return { body: { success: true } }
|
|
599
|
+
* },
|
|
600
|
+
* auth: async ({ request }) => ({ userId: await verifyToken(request) })
|
|
601
|
+
* })
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
153
604
|
add<M extends LexMethod, Credentials = unknown>(
|
|
154
605
|
ns: Main<M>,
|
|
155
606
|
config: M extends Subscription
|
|
@@ -448,6 +899,35 @@ export class LexRouter {
|
|
|
448
899
|
)
|
|
449
900
|
}
|
|
450
901
|
|
|
902
|
+
/**
|
|
903
|
+
* The main fetch handler for processing XRPC requests.
|
|
904
|
+
*
|
|
905
|
+
* Routes incoming requests to the appropriate method handler based on the
|
|
906
|
+
* NSID in the URL path. Returns appropriate error responses for invalid
|
|
907
|
+
* paths or unimplemented methods.
|
|
908
|
+
*
|
|
909
|
+
* This handler can be used directly with HTTP servers that support the
|
|
910
|
+
* fetch API pattern, or converted to a Node.js request listener using
|
|
911
|
+
* `toRequestListener()`.
|
|
912
|
+
*
|
|
913
|
+
* @param request - The incoming HTTP request
|
|
914
|
+
* @param connection - Optional connection metadata
|
|
915
|
+
* @returns A promise resolving to the HTTP response
|
|
916
|
+
*
|
|
917
|
+
* @example
|
|
918
|
+
* ```typescript
|
|
919
|
+
* // Use with Deno
|
|
920
|
+
* Deno.serve(router.fetch)
|
|
921
|
+
*
|
|
922
|
+
* // Use with Bun
|
|
923
|
+
* Bun.serve({ fetch: router.fetch })
|
|
924
|
+
*
|
|
925
|
+
* // Use with Node.js
|
|
926
|
+
* import { toRequestListener } from '@atproto/lex-server/nodejs'
|
|
927
|
+
* const listener = toRequestListener(router.fetch)
|
|
928
|
+
* http.createServer(listener).listen(3000)
|
|
929
|
+
* ```
|
|
930
|
+
*/
|
|
451
931
|
fetch: FetchHandler = async (
|
|
452
932
|
request: Request,
|
|
453
933
|
connection?: ConnectionInfo,
|
|
@@ -1,9 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type representing the value of a WWW-Authenticate HTTP header.
|
|
3
|
+
*
|
|
4
|
+
* Supports multiple authentication schemes, each with optional parameters.
|
|
5
|
+
* Parameters can be provided as a token68 string (for schemes like Bearer)
|
|
6
|
+
* or as key-value pairs.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 | RFC 7235 Section 4.1}
|
|
9
|
+
*
|
|
10
|
+
* @example Bearer scheme with parameters
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const auth: WWWAuthenticate = {
|
|
13
|
+
* Bearer: {
|
|
14
|
+
* realm: 'api.example.com',
|
|
15
|
+
* error: 'InvalidToken',
|
|
16
|
+
* error_description: 'The token has expired'
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* // Formats to: Bearer realm="api.example.com", error="InvalidToken", error_description="The token has expired"
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Multiple schemes
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const auth: WWWAuthenticate = {
|
|
25
|
+
* Bearer: { realm: 'api' },
|
|
26
|
+
* Basic: { realm: 'api' }
|
|
27
|
+
* }
|
|
28
|
+
* // Formats to: Bearer realm="api", Basic realm="api"
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example Token68 value (no parameters)
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const auth: WWWAuthenticate = {
|
|
34
|
+
* Bearer: 'base64encodedvalue=='
|
|
35
|
+
* }
|
|
36
|
+
* // Formats to: Bearer base64encodedvalue==
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
1
39
|
export type WWWAuthenticate = {
|
|
2
40
|
[authScheme in string]?:
|
|
3
41
|
| string // token68
|
|
4
42
|
| { [authParam in string]?: string }
|
|
5
43
|
}
|
|
6
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Formats a WWWAuthenticate object into an HTTP header string.
|
|
47
|
+
*
|
|
48
|
+
* Converts the structured authentication scheme and parameter data into
|
|
49
|
+
* the proper WWW-Authenticate header format per RFC 7235.
|
|
50
|
+
*
|
|
51
|
+
* @param wwwAuthenticate - The authentication schemes and parameters
|
|
52
|
+
* @returns Formatted header string ready for use in HTTP responses
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const header = formatWWWAuthenticateHeader({
|
|
57
|
+
* Bearer: {
|
|
58
|
+
* realm: 'api.example.com',
|
|
59
|
+
* error: 'MissingToken'
|
|
60
|
+
* }
|
|
61
|
+
* })
|
|
62
|
+
* // Returns: 'Bearer realm="api.example.com", error="MissingToken"'
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example Empty or undefined values
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const header = formatWWWAuthenticateHeader({
|
|
68
|
+
* Bearer: { realm: 'api', error: undefined }
|
|
69
|
+
* })
|
|
70
|
+
* // Returns: 'Bearer realm="api"' (undefined values are omitted)
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
7
73
|
export function formatWWWAuthenticateHeader(
|
|
8
74
|
wwwAuthenticate: WWWAuthenticate,
|
|
9
75
|
): string {
|