@atproto/lex-server 0.0.1

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE.txt +7 -0
  3. package/README.md +598 -0
  4. package/dist/errors.d.ts +13 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +39 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/example.d.ts +2 -0
  9. package/dist/example.d.ts.map +1 -0
  10. package/dist/example.js +36 -0
  11. package/dist/example.js.map +1 -0
  12. package/dist/index.d.ts +4 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +9 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/lex-auth-error.d.ts +15 -0
  17. package/dist/lex-auth-error.d.ts.map +1 -0
  18. package/dist/lex-auth-error.js +52 -0
  19. package/dist/lex-auth-error.js.map +1 -0
  20. package/dist/lex-server.d.ts +80 -0
  21. package/dist/lex-server.d.ts.map +1 -0
  22. package/dist/lex-server.js +285 -0
  23. package/dist/lex-server.js.map +1 -0
  24. package/dist/lib/drain-websocket.d.ts +6 -0
  25. package/dist/lib/drain-websocket.d.ts.map +1 -0
  26. package/dist/lib/drain-websocket.js +16 -0
  27. package/dist/lib/drain-websocket.js.map +1 -0
  28. package/dist/lib/sleep.d.ts +2 -0
  29. package/dist/lib/sleep.d.ts.map +1 -0
  30. package/dist/lib/sleep.js +22 -0
  31. package/dist/lib/sleep.js.map +1 -0
  32. package/dist/lib/www-authenticate.d.ts +7 -0
  33. package/dist/lib/www-authenticate.d.ts.map +1 -0
  34. package/dist/lib/www-authenticate.js +22 -0
  35. package/dist/lib/www-authenticate.js.map +1 -0
  36. package/dist/nodejs.d.ts +35 -0
  37. package/dist/nodejs.d.ts.map +1 -0
  38. package/dist/nodejs.js +236 -0
  39. package/dist/nodejs.js.map +1 -0
  40. package/dist/subscripotion.d.ts +2 -0
  41. package/dist/subscripotion.d.ts.map +1 -0
  42. package/dist/subscripotion.js +36 -0
  43. package/dist/subscripotion.js.map +1 -0
  44. package/dist/test.d.mts +2 -0
  45. package/dist/test.d.mts.map +1 -0
  46. package/dist/test.mjs +52 -0
  47. package/dist/test.mjs.map +1 -0
  48. package/nodejs.js +5 -0
  49. package/package.json +64 -0
  50. package/src/errors.ts +54 -0
  51. package/src/index.ts +8 -0
  52. package/src/lex-server.test.ts +1621 -0
  53. package/src/lex-server.ts +551 -0
  54. package/src/lib/drain-websocket.ts +23 -0
  55. package/src/lib/sleep.ts +25 -0
  56. package/src/lib/www-authenticate.ts +26 -0
  57. package/src/nodejs.test.ts +107 -0
  58. package/src/nodejs.ts +367 -0
  59. package/tsconfig.build.json +12 -0
  60. package/tsconfig.json +8 -0
  61. package/tsconfig.tests.json +9 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @atproto/lex-server
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add new XRPC server library
8
+
9
+ - Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:
10
+ - @atproto/lex-schema@0.0.6
11
+ - @atproto/lex-data@0.0.5
12
+ - @atproto/lex-cbor@0.0.5
13
+ - @atproto/lex-json@0.0.5
package/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Dual MIT/Apache-2.0 License
2
+
3
+ Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
4
+
5
+ Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
6
+
7
+ Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
package/README.md ADDED
@@ -0,0 +1,598 @@
1
+ # @atproto/lex-server
2
+
3
+ Request router for Atproto Lexicon protocols and schemas. See the [Changelog](./CHANGELOG.md) for version history.
4
+
5
+ ```bash
6
+ npm install @atproto/lex-server
7
+ ```
8
+
9
+ - Type-safe request routing based on Lexicon schemas
10
+ - Support for queries, procedures, and WebSocket subscriptions
11
+ - Built on Web standard `Request`/`Response` APIs (portable across runtimes)
12
+ - Custom authentication with credential passing
13
+ - Graceful shutdown with `AsyncDisposable` pattern
14
+
15
+ > [!IMPORTANT]
16
+ >
17
+ > This package is currently in **preview**. The API and features are subject to change before the stable release.
18
+
19
+ **What is this?**
20
+
21
+ Building AT Protocol servers requires handling XRPC requests, validating inputs against Lexicon schemas, managing authentication, and supporting real-time subscriptions. `@atproto/lex-server` automates this by:
22
+
23
+ 1. Routing requests to type-safe handlers based on Lexicon schemas
24
+ 2. Automatically validating request parameters and bodies
25
+ 3. Providing a flexible authentication system with custom strategies
26
+ 4. Supporting WebSocket subscriptions with backpressure handling
27
+
28
+ ```typescript
29
+ import { LexRouter } from '@atproto/lex-server'
30
+ import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
31
+ import * as app from './lexicons/app.js'
32
+
33
+ const router = new LexRouter({ upgradeWebSocket })
34
+ .add(app.bsky.actor.getProfile, async ({ params }) => {
35
+ const profile = await db.getProfile(params.actor)
36
+ return { body: profile }
37
+ })
38
+ .add(app.bsky.feed.post.create, {
39
+ auth: requireAuth,
40
+ handler: async ({ credentials, input }) => {
41
+ const result = await db.createPost(credentials.did, input.body)
42
+ return { body: result }
43
+ },
44
+ })
45
+
46
+ await serve(router, { port: 3000 })
47
+ ```
48
+
49
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
50
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
51
+
52
+ - [Quick Start](#quick-start)
53
+ - [LexRouter](#lexrouter)
54
+ - [Creating a Router](#creating-a-router)
55
+ - [Adding Routes](#adding-routes)
56
+ - [Handler Context](#handler-context)
57
+ - [Handler Output](#handler-output)
58
+ - [Queries and Procedures](#queries-and-procedures)
59
+ - [Query Handler](#query-handler)
60
+ - [Procedure Handler](#procedure-handler)
61
+ - [Binary Payloads](#binary-payloads)
62
+ - [Subscriptions](#subscriptions)
63
+ - [Authentication](#authentication)
64
+ - [Custom Authentication](#custom-authentication)
65
+ - [WWW-Authenticate Headers](#www-authenticate-headers)
66
+ - [Error Handling](#error-handling)
67
+ - [LexError](#lexerror)
68
+ - [LexServerAuthError](#lexserverautherror)
69
+ - [Error Handler Callback](#error-handler-callback)
70
+ - [Node.js Server](#nodejs-server)
71
+ - [serve()](#serve)
72
+ - [createServer()](#createserver)
73
+ - [toRequestListener()](#torequestlistener)
74
+ - [upgradeWebSocket()](#upgradewebsocket)
75
+ - [Advanced Usage](#advanced-usage)
76
+ - [Custom Response Objects](#custom-response-objects)
77
+ - [Response Headers](#response-headers)
78
+ - [Connection Info](#connection-info)
79
+ - [License](#license)
80
+
81
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
82
+
83
+ ## Quick Start
84
+
85
+ **1. Install the package**
86
+
87
+ ```bash
88
+ npm install @atproto/lex-server
89
+ ```
90
+
91
+ **2. Generate Lexicon schemas**
92
+
93
+ Use `@atproto/lex` to generate TypeScript schemas from your Lexicon definitions:
94
+
95
+ ```bash
96
+ lex install app.bsky.actor.getProfile
97
+ lex build
98
+ ```
99
+
100
+ **3. Create a router and add handlers**
101
+
102
+ ```typescript
103
+ import { LexRouter, LexError } from '@atproto/lex-server'
104
+ import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
105
+ import * as app from './lexicons/app.js'
106
+
107
+ const router = new LexRouter({ upgradeWebSocket })
108
+
109
+ // Add a query handler
110
+ router.add(app.bsky.actor.getProfile, async ({ params }) => {
111
+ const profile = await db.getProfile(params.actor)
112
+ if (!profile) {
113
+ throw new LexError('NotFound', 'Profile not found')
114
+ }
115
+ return { body: profile }
116
+ })
117
+
118
+ // Start the server
119
+ const server = await serve(router, { port: 3000 })
120
+ console.log('Server listening on port 3000')
121
+ ```
122
+
123
+ ## LexRouter
124
+
125
+ The `LexRouter` class is the core of `@atproto/lex-server`. It routes XRPC requests to type-safe handlers based on Lexicon schemas.
126
+
127
+ ### Creating a Router
128
+
129
+ ```typescript
130
+ import { LexRouter } from '@atproto/lex-server'
131
+ import { upgradeWebSocket } from '@atproto/lex-server/nodejs'
132
+
133
+ const router = new LexRouter({
134
+ // Required for WebSocket subscriptions (Node.js)
135
+ upgradeWebSocket,
136
+
137
+ // Optional: Handle unexpected errors
138
+ onHandlerError: ({ error, request, method }) => {
139
+ console.error(`Error in ${method.nsid}:`, error)
140
+ },
141
+
142
+ // Optional: WebSocket backpressure settings
143
+ highWaterMark: 250_000, // bytes (default: 250KB)
144
+ lowWaterMark: 50_000, // bytes (default: 50KB)
145
+ })
146
+ ```
147
+
148
+ ### Adding Routes
149
+
150
+ Routes are added using the `.add()` method, which accepts a Lexicon schema and a handler:
151
+
152
+ ```typescript
153
+ // Simple handler (no authentication)
154
+ router.add(schema, async ({ params, input, request }) => {
155
+ return { body: result }
156
+ })
157
+
158
+ // Handler with authentication
159
+ router.add(schema, {
160
+ auth: async ({ request, params }) => credentials,
161
+ handler: async ({ params, input, credentials, request }) => {
162
+ return { body: result }
163
+ },
164
+ })
165
+ ```
166
+
167
+ The router supports method chaining:
168
+
169
+ ```typescript
170
+ const router = new LexRouter()
171
+ .add(app.bsky.actor.getProfile, profileHandler)
172
+ .add(app.bsky.feed.getTimeline, timelineHandler)
173
+ .add(app.bsky.feed.post.create, postHandler)
174
+ ```
175
+
176
+ ### Handler Context
177
+
178
+ Handlers receive a context object with the following properties:
179
+
180
+ ```typescript
181
+ type LexRouterHandlerContext<Method, Credentials> = {
182
+ credentials: Credentials // Result of auth function (undefined if no auth)
183
+ input: InferMethodInput<Method> // Parsed request body (procedures only)
184
+ params: InferMethodParams<Method> // Validated URL query parameters
185
+ request: Request // Original Web Request object
186
+ connection?: ConnectionInfo // Network connection info
187
+ }
188
+ ```
189
+
190
+ ### Handler Output
191
+
192
+ Handlers can return various output formats:
193
+
194
+ ```typescript
195
+ // JSON response (encoding inferred from schema)
196
+ return { body: { key: 'value' } }
197
+
198
+ // With custom encoding
199
+ return { encoding: 'text/plain', body: 'Hello, world!' }
200
+
201
+ // With response headers
202
+ return { body: data, headers: { 'x-custom': 'value' } }
203
+
204
+ // Empty response (200 OK with no body)
205
+ return {}
206
+
207
+ // Custom Response object (full control)
208
+ return new Response(body, { status: 201, headers })
209
+
210
+ // Proxy Response
211
+ return fetch('https://example.com/data')
212
+ ```
213
+
214
+ ## Queries and Procedures
215
+
216
+ ### Query Handler
217
+
218
+ Queries handle `GET` requests and receive parameters from the URL query string:
219
+
220
+ ```typescript
221
+ import * as app from './lexicons/app.js'
222
+
223
+ router.add(app.bsky.actor.getProfile, async ({ params }) => {
224
+ // params.actor is typed and validated
225
+ const profile = await db.getProfile(params.actor)
226
+ return { body: profile }
227
+ })
228
+ ```
229
+
230
+ ### Procedure Handler
231
+
232
+ Procedures handle `POST` requests and receive a request body:
233
+
234
+ ```typescript
235
+ router.add(app.bsky.feed.post.create, async ({ input }) => {
236
+ // input.body contains the parsed and validated request body
237
+ const post = await db.createPost(input.body)
238
+ return { body: { uri: post.uri, cid: post.cid } }
239
+ })
240
+ ```
241
+
242
+ ### Binary Payloads
243
+
244
+ For endpoints that accept or return binary data:
245
+
246
+ ```typescript
247
+ // Binary input
248
+ router.add(app.example.uploadBlob, async ({ input }) => {
249
+ // input.body is a Request object for streaming
250
+ // input.encoding contains the content-type
251
+ const blob = await input.body.arrayBuffer()
252
+ return { body: { cid: await store(blob) } }
253
+ })
254
+
255
+ // Binary output
256
+ router.add(app.example.getBlob, async ({ params }) => {
257
+ const stream = await getBlob(params.cid)
258
+ return {
259
+ encoding: 'application/octet-stream',
260
+ body: stream,
261
+ }
262
+ })
263
+ ```
264
+
265
+ ## Subscriptions
266
+
267
+ Subscriptions provide real-time data over WebSocket connections. Handlers are async generators that yield messages:
268
+
269
+ ```typescript
270
+ import { LexRouter, LexError } from '@atproto/lex-server'
271
+ import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
272
+ import { scheduler } from 'node:timers/promises'
273
+
274
+ const router = new LexRouter({
275
+ upgradeWebSocket, // Required for WebSocket support in nodejs
276
+ })
277
+
278
+ router.add(com.example.stream, async function* ({ params, request }) {
279
+ const { cursor = 0, limit = 10 } = params
280
+ const { signal } = request
281
+
282
+ for (let i = 0; i < limit; i++) {
283
+ // Yield messages to the client
284
+ yield com.example.stream.message.$build({
285
+ data: `Message ${cursor + i}`,
286
+ cursor: cursor + i,
287
+ })
288
+
289
+ // Wait between messages (respects abort signal)
290
+ await scheduler.wait(1000, { signal })
291
+ }
292
+
293
+ // Throwing a LexError closes the connection with an error frame
294
+ throw new LexError('LimitReached', `Limit of ${limit} messages reached`)
295
+ })
296
+ ```
297
+
298
+ Messages are CBOR-encoded and sent as WebSocket binary frames. The router handles:
299
+
300
+ - WebSocket upgrade negotiation
301
+ - Backpressure management
302
+ - Graceful connection cleanup
303
+ - Error frame encoding
304
+
305
+ ## Authentication
306
+
307
+ ### Custom Authentication
308
+
309
+ Authentication is implemented through the `auth` function in handler configs:
310
+
311
+ ```typescript
312
+ import { LexError, LexServerAuthError } from '@atproto/lex-server'
313
+
314
+ type Credentials = { did: string; scope: string[] }
315
+
316
+ const requireAuth = async ({
317
+ request,
318
+ }: {
319
+ request: Request
320
+ }): Promise<Credentials> => {
321
+ const header = request.headers.get('authorization')
322
+ if (!header?.startsWith('Bearer ')) {
323
+ throw new LexServerAuthError(
324
+ 'AuthenticationRequired',
325
+ 'Bearer token required',
326
+ {
327
+ Bearer: { realm: 'api' },
328
+ },
329
+ )
330
+ }
331
+
332
+ const token = header.slice(7)
333
+ const session = await verifyToken(token)
334
+ if (!session) {
335
+ throw new LexServerAuthError(
336
+ 'InvalidToken',
337
+ 'Token is invalid or expired',
338
+ {
339
+ Bearer: { realm: 'api', error: 'invalid_token' },
340
+ },
341
+ )
342
+ }
343
+
344
+ return { did: session.did, scope: session.scope }
345
+ }
346
+
347
+ // Use with handlers
348
+ router.add(app.bsky.feed.post.create, {
349
+ auth: requireAuth,
350
+ handler: async ({ credentials, input }) => {
351
+ // credentials.did is available here
352
+ const post = await db.createPost(credentials.did, input.body)
353
+ return { body: post }
354
+ },
355
+ })
356
+ ```
357
+
358
+ The auth function:
359
+
360
+ 1. Is called **before** parsing the request body
361
+ 2. Receives `params`, `request`, and `connection` info
362
+ 3. Should throw `LexError` or `LexServerAuthError` on failure
363
+ 4. Returns credentials that are passed to the handler
364
+
365
+ ### WWW-Authenticate Headers
366
+
367
+ Use `LexServerAuthError` to include `WWW-Authenticate` headers in error responses:
368
+
369
+ ```typescript
370
+ import { LexServerAuthError } from '@atproto/lex-server'
371
+
372
+ // Simple Bearer challenge
373
+ throw new LexServerAuthError('AuthenticationRequired', 'Login required', {
374
+ Bearer: { realm: 'api' },
375
+ })
376
+ // WWW-Authenticate: Bearer realm="api"
377
+
378
+ // Multiple schemes
379
+ throw new LexServerAuthError('AuthenticationRequired', 'Auth required', {
380
+ Bearer: { realm: 'api', scope: 'read write' },
381
+ Basic: { realm: 'api' },
382
+ })
383
+ // WWW-Authenticate: Bearer realm="api", scope="read write", Basic realm="api"
384
+
385
+ // Token68 format
386
+ throw new LexServerAuthError('AuthenticationRequired', 'Auth required', {
387
+ Bearer: 'token68value',
388
+ })
389
+ // WWW-Authenticate: Bearer token68value
390
+ ```
391
+
392
+ ## Error Handling
393
+
394
+ ### LexError
395
+
396
+ Throw `LexError` to return structured XRPC error responses:
397
+
398
+ ```typescript
399
+ import { LexError } from '@atproto/lex-server'
400
+
401
+ router.add(app.bsky.actor.getProfile, async ({ params }) => {
402
+ const profile = await db.getProfile(params.actor)
403
+ if (!profile) {
404
+ throw new LexError('NotFound', 'Profile not found')
405
+ }
406
+ return { body: profile }
407
+ })
408
+ ```
409
+
410
+ Error responses follow the XRPC format:
411
+
412
+ ```json
413
+ {
414
+ "error": "NotFound",
415
+ "message": "Profile not found"
416
+ }
417
+ ```
418
+
419
+ ### LexServerAuthError
420
+
421
+ `LexServerAuthError` extends `LexError` with `WWW-Authenticate` header support:
422
+
423
+ ```typescript
424
+ import { LexServerAuthError } from '@atproto/lex-server'
425
+
426
+ throw new LexServerAuthError('AuthenticationRequired', 'Invalid credentials', {
427
+ Bearer: { realm: 'api' },
428
+ })
429
+ ```
430
+
431
+ This returns a 401 response with the `WWW-Authenticate` header.
432
+
433
+ ### Error Handler Callback
434
+
435
+ Use `onHandlerError` to log or report unexpected errors:
436
+
437
+ ```typescript
438
+ const router = new LexRouter({
439
+ onHandlerError: async ({ error, request, method }) => {
440
+ // Log errors (excluding expected abort signals)
441
+ console.error(`Error in ${method.nsid}:`, error)
442
+ await reportToSentry(error)
443
+ },
444
+ })
445
+ ```
446
+
447
+ > [!NOTE]
448
+ >
449
+ > The callback is only invoked for unexpected errors, not for `LexError` instances or request aborts.
450
+
451
+ ## Node.js Server
452
+
453
+ The `@atproto/lex-server/nodejs` subpath provides Node.js-specific utilities.
454
+
455
+ ### serve()
456
+
457
+ Start a server and begin listening:
458
+
459
+ ```typescript
460
+ import { serve } from '@atproto/lex-server/nodejs'
461
+
462
+ const server = await serve(router, { port: 3000 })
463
+ console.log('Server listening on port 3000')
464
+
465
+ // Graceful shutdown
466
+ await server.terminate()
467
+ ```
468
+
469
+ The server supports `AsyncDisposable`:
470
+
471
+ ```typescript
472
+ await using server = await serve(router, { port: 3000 })
473
+ // Server is automatically terminated when scope exits
474
+ ```
475
+
476
+ Options:
477
+
478
+ ```typescript
479
+ type StartServerOptions = {
480
+ port?: number
481
+ host?: string
482
+ gracefulTerminationTimeout?: number // ms to wait for connections to close
483
+ }
484
+ ```
485
+
486
+ ### createServer()
487
+
488
+ Create a server without starting it:
489
+
490
+ ```typescript
491
+ import { createServer } from '@atproto/lex-server/nodejs'
492
+
493
+ const server = createServer(router, {
494
+ gracefulTerminationTimeout: 5000,
495
+ })
496
+
497
+ server.listen(3000, () => {
498
+ console.log('Server listening')
499
+ })
500
+ ```
501
+
502
+ ### toRequestListener()
503
+
504
+ Convert a handler to an Express/Connect-compatible middleware:
505
+
506
+ ```typescript
507
+ import express from 'express'
508
+ import { toRequestListener } from '@atproto/lex-server/nodejs'
509
+
510
+ const app = express()
511
+
512
+ // Mount the XRPC router
513
+ app.use('/xrpc', toRequestListener(router.handle))
514
+
515
+ app.listen(3000)
516
+ ```
517
+
518
+ ### upgradeWebSocket()
519
+
520
+ Required for WebSocket subscription support in Node.js:
521
+
522
+ ```typescript
523
+ import { LexRouter } from '@atproto/lex-server'
524
+ import { upgradeWebSocket } from '@atproto/lex-server/nodejs'
525
+
526
+ const router = new LexRouter({ upgradeWebSocket })
527
+ ```
528
+
529
+ ## Advanced Usage
530
+
531
+ ### Custom Response Objects
532
+
533
+ Return a `Response` object for full control over the response:
534
+
535
+ ```typescript
536
+ router.add(schema, async ({ params }) => {
537
+ if (params.redirect) {
538
+ return Response.redirect('https://example.com', 302)
539
+ }
540
+
541
+ return new Response(JSON.stringify({ custom: true }), {
542
+ status: 201,
543
+ headers: {
544
+ 'Content-Type': 'application/json',
545
+ 'X-Custom-Header': 'value',
546
+ },
547
+ })
548
+ })
549
+ ```
550
+
551
+ ### Response Headers
552
+
553
+ Add headers to responses:
554
+
555
+ ```typescript
556
+ router.add(schema, async ({ params }) => {
557
+ return {
558
+ body: { data: 'value' },
559
+ headers: {
560
+ 'Cache-Control': 'public, max-age=3600',
561
+ 'X-Request-Id': crypto.randomUUID(),
562
+ },
563
+ }
564
+ })
565
+ ```
566
+
567
+ ### Connection Info
568
+
569
+ Access network connection information:
570
+
571
+ ```typescript
572
+ router.add(schema, async ({ connection }) => {
573
+ console.log('Remote address:', connection?.remoteAddr?.hostname)
574
+ console.log('Local address:', connection?.localAddr?.hostname)
575
+ return { body: { status: 'ok' } }
576
+ })
577
+ ```
578
+
579
+ Connection info structure:
580
+
581
+ ```typescript
582
+ type ConnectionInfo = {
583
+ localAddr?: {
584
+ hostname: string
585
+ port: number
586
+ transport: 'tcp' | 'udp'
587
+ }
588
+ remoteAddr?: {
589
+ hostname: string
590
+ port: number
591
+ transport: 'tcp' | 'udp'
592
+ }
593
+ }
594
+ ```
595
+
596
+ ## License
597
+
598
+ MIT or Apache2
@@ -0,0 +1,13 @@
1
+ import { LexError, LexErrorCode } from '@atproto/lex-data';
2
+ import { WWWAuthenticate } from './lib/www-authenticate.js';
3
+ export type { WWWAuthenticate };
4
+ export declare class LexServerAuthError<N extends LexErrorCode = LexErrorCode> extends LexError<N> {
5
+ readonly wwwAuthenticate: WWWAuthenticate;
6
+ name: string;
7
+ constructor(error: N, message: string, wwwAuthenticate?: WWWAuthenticate, options?: ErrorOptions);
8
+ get wwwAuthenticateHeader(): string;
9
+ toJSON(): import("@atproto/lex-data").LexErrorData<any>;
10
+ toResponse(): Response;
11
+ static from(cause: LexError, wwwAuthenticate?: WWWAuthenticate): LexServerAuthError;
12
+ }
13
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EACL,eAAe,EAEhB,MAAM,2BAA2B,CAAA;AAElC,YAAY,EAAE,eAAe,EAAE,CAAA;AAE/B,qBAAa,kBAAkB,CAC7B,CAAC,SAAS,YAAY,GAAG,YAAY,CACrC,SAAQ,QAAQ,CAAC,CAAC,CAAC;IAMjB,QAAQ,CAAC,eAAe,EAAE,eAAe;IAL3C,IAAI,SAAuB;gBAGzB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,MAAM,EACN,eAAe,GAAE,eAAoB,EAC9C,OAAO,CAAC,EAAE,YAAY;IAKxB,IAAI,qBAAqB,IAAI,MAAM,CAElC;IAED,MAAM;IAKN,UAAU,IAAI,QAAQ;IAatB,MAAM,CAAC,IAAI,CACT,KAAK,EAAE,QAAQ,EACf,eAAe,CAAC,EAAE,eAAe,GAChC,kBAAkB;CAMtB"}
package/dist/errors.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LexServerAuthError = void 0;
4
+ const lex_data_1 = require("@atproto/lex-data");
5
+ const www_authenticate_js_1 = require("./lib/www-authenticate.js");
6
+ class LexServerAuthError extends lex_data_1.LexError {
7
+ wwwAuthenticate;
8
+ name = 'LexServerAuthError';
9
+ constructor(error, message, wwwAuthenticate = {}, options) {
10
+ super(error, message, options);
11
+ this.wwwAuthenticate = wwwAuthenticate;
12
+ }
13
+ get wwwAuthenticateHeader() {
14
+ return (0, www_authenticate_js_1.formatWWWAuthenticateHeader)(this.wwwAuthenticate);
15
+ }
16
+ toJSON() {
17
+ const { cause } = this;
18
+ return cause instanceof lex_data_1.LexError ? cause.toJSON() : super.toJSON();
19
+ }
20
+ toResponse() {
21
+ const { wwwAuthenticateHeader } = this;
22
+ const headers = wwwAuthenticateHeader
23
+ ? new Headers({
24
+ 'WWW-Authenticate': wwwAuthenticateHeader,
25
+ 'Access-Control-Expose-Headers': 'WWW-Authenticate', // CORS
26
+ })
27
+ : undefined;
28
+ return Response.json(this.toJSON(), { status: 401, headers });
29
+ }
30
+ static from(cause, wwwAuthenticate) {
31
+ if (cause instanceof LexServerAuthError)
32
+ return cause;
33
+ return new LexServerAuthError(cause.error, cause.message, wwwAuthenticate, {
34
+ cause,
35
+ });
36
+ }
37
+ }
38
+ exports.LexServerAuthError = LexServerAuthError;
39
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";;;AAAA,gDAA0D;AAC1D,mEAGkC;AAIlC,MAAa,kBAEX,SAAQ,mBAAW;IAMR;IALX,IAAI,GAAG,oBAAoB,CAAA;IAE3B,YACE,KAAQ,EACR,OAAe,EACN,kBAAmC,EAAE,EAC9C,OAAsB;QAEtB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;QAHrB,oBAAe,GAAf,eAAe,CAAsB;IAIhD,CAAC;IAED,IAAI,qBAAqB;QACvB,OAAO,IAAA,iDAA2B,EAAC,IAAI,CAAC,eAAe,CAAC,CAAA;IAC1D,CAAC;IAED,MAAM;QACJ,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;QACtB,OAAO,KAAK,YAAY,mBAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;IACpE,CAAC;IAED,UAAU;QACR,MAAM,EAAE,qBAAqB,EAAE,GAAG,IAAI,CAAA;QAEtC,MAAM,OAAO,GAAG,qBAAqB;YACnC,CAAC,CAAC,IAAI,OAAO,CAAC;gBACV,kBAAkB,EAAE,qBAAqB;gBACzC,+BAA+B,EAAE,kBAAkB,EAAE,OAAO;aAC7D,CAAC;YACJ,CAAC,CAAC,SAAS,CAAA;QAEb,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;IAC/D,CAAC;IAED,MAAM,CAAC,IAAI,CACT,KAAe,EACf,eAAiC;QAEjC,IAAI,KAAK,YAAY,kBAAkB;YAAE,OAAO,KAAK,CAAA;QACrD,OAAO,IAAI,kBAAkB,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,eAAe,EAAE;YACzE,KAAK;SACN,CAAC,CAAA;IACJ,CAAC;CACF;AA7CD,gDA6CC","sourcesContent":["import { LexError, LexErrorCode } from '@atproto/lex-data'\nimport {\n WWWAuthenticate,\n formatWWWAuthenticateHeader,\n} from './lib/www-authenticate.js'\n\nexport type { WWWAuthenticate }\n\nexport class LexServerAuthError<\n N extends LexErrorCode = LexErrorCode,\n> extends LexError<N> {\n name = 'LexServerAuthError'\n\n constructor(\n error: N,\n message: string,\n readonly wwwAuthenticate: WWWAuthenticate = {},\n options?: ErrorOptions,\n ) {\n super(error, message, options)\n }\n\n get wwwAuthenticateHeader(): string {\n return formatWWWAuthenticateHeader(this.wwwAuthenticate)\n }\n\n toJSON() {\n const { cause } = this\n return cause instanceof LexError ? cause.toJSON() : super.toJSON()\n }\n\n toResponse(): Response {\n const { wwwAuthenticateHeader } = this\n\n const headers = wwwAuthenticateHeader\n ? new Headers({\n 'WWW-Authenticate': wwwAuthenticateHeader,\n 'Access-Control-Expose-Headers': 'WWW-Authenticate', // CORS\n })\n : undefined\n\n return Response.json(this.toJSON(), { status: 401, headers })\n }\n\n static from(\n cause: LexError,\n wwwAuthenticate?: WWWAuthenticate,\n ): LexServerAuthError {\n if (cause instanceof LexServerAuthError) return cause\n return new LexServerAuthError(cause.error, cause.message, wwwAuthenticate, {\n cause,\n })\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=example.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../src/example.ts"],"names":[],"mappings":""}