@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/nodejs.ts
CHANGED
|
@@ -28,6 +28,36 @@ function isUpgradeRequest(request: Request, upgrade: string): boolean {
|
|
|
28
28
|
)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Upgrades an HTTP request to a WebSocket connection for Node.js.
|
|
33
|
+
*
|
|
34
|
+
* This function must be passed to the {@link LexRouter} constructor to enable
|
|
35
|
+
* subscription (WebSocket) support on Node.js. It creates a WebSocket instance
|
|
36
|
+
* and a placeholder response that signals the need for protocol upgrade.
|
|
37
|
+
*
|
|
38
|
+
* The actual upgrade is handled internally when the response is sent through
|
|
39
|
+
* {@link sendResponse}.
|
|
40
|
+
*
|
|
41
|
+
* @param request - The incoming HTTP request to upgrade
|
|
42
|
+
* @returns An object containing the WebSocket and upgrade response
|
|
43
|
+
* @throws {TypeError} If the request is not a valid WebSocket upgrade request
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { LexRouter } from '@atproto/lex-server'
|
|
48
|
+
* import { upgradeWebSocket } from '@atproto/lex-server/nodejs'
|
|
49
|
+
*
|
|
50
|
+
* // Pass to router for subscription support
|
|
51
|
+
* const router = new LexRouter({ upgradeWebSocket })
|
|
52
|
+
*
|
|
53
|
+
* // Now you can add subscription handlers
|
|
54
|
+
* router.add(subscribeRepos, async function* (ctx) {
|
|
55
|
+
* for await (const event of eventStream) {
|
|
56
|
+
* yield event
|
|
57
|
+
* }
|
|
58
|
+
* })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
31
61
|
export function upgradeWebSocket(request: Request): {
|
|
32
62
|
response: Response
|
|
33
63
|
socket: WebSocket
|
|
@@ -106,7 +136,34 @@ function handleWebSocketUpgrade(
|
|
|
106
136
|
})
|
|
107
137
|
}
|
|
108
138
|
|
|
109
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Sends a fetch API Response through a Node.js ServerResponse.
|
|
141
|
+
*
|
|
142
|
+
* Handles both regular HTTP responses and WebSocket upgrades. For WebSocket
|
|
143
|
+
* upgrades (status 101), delegates to the WebSocket upgrade handler.
|
|
144
|
+
*
|
|
145
|
+
* This function is used internally by {@link toRequestListener} and
|
|
146
|
+
* {@link createServer}, but can be used directly for custom integrations.
|
|
147
|
+
*
|
|
148
|
+
* @param req - The Node.js IncomingMessage
|
|
149
|
+
* @param res - The Node.js ServerResponse to write to
|
|
150
|
+
* @param response - The fetch API Response to send
|
|
151
|
+
* @throws {TypeError} If headers have already been sent
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* import http from 'node:http'
|
|
156
|
+
* import { sendResponse } from '@atproto/lex-server/nodejs'
|
|
157
|
+
*
|
|
158
|
+
* const server = http.createServer(async (req, res) => {
|
|
159
|
+
* const response = new Response('Hello, World!', {
|
|
160
|
+
* headers: { 'Content-Type': 'text/plain' }
|
|
161
|
+
* })
|
|
162
|
+
* await sendResponse(req, res, response)
|
|
163
|
+
* })
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export async function sendResponse(
|
|
110
167
|
req: IncomingMessage,
|
|
111
168
|
res: ServerResponse,
|
|
112
169
|
response: Response,
|
|
@@ -124,7 +181,7 @@ async function sendResponse(
|
|
|
124
181
|
res.statusMessage = response.statusText
|
|
125
182
|
|
|
126
183
|
for (const [key, value] of response.headers) {
|
|
127
|
-
res.
|
|
184
|
+
res.setHeader(key, value)
|
|
128
185
|
}
|
|
129
186
|
|
|
130
187
|
if (response.body != null && req.method !== 'HEAD') {
|
|
@@ -242,18 +299,50 @@ function toBody(req: IncomingMessage): null | ReadableStream<Uint8Array> {
|
|
|
242
299
|
return Readable.toWeb(req) as ReadableStream<Uint8Array>
|
|
243
300
|
}
|
|
244
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Network address type for Node.js TCP connections.
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* const addr: NetAddr = {
|
|
308
|
+
* transport: 'tcp',
|
|
309
|
+
* hostname: '192.168.1.100',
|
|
310
|
+
* port: 54321
|
|
311
|
+
* }
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
245
314
|
export type NetAddr = {
|
|
315
|
+
/** Always 'tcp' for Node.js HTTP connections. */
|
|
246
316
|
transport: 'tcp'
|
|
317
|
+
/** The IP address of the remote client. */
|
|
247
318
|
hostname: string
|
|
319
|
+
/** The port number of the remote client. */
|
|
248
320
|
port: number
|
|
249
321
|
}
|
|
250
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Connection metadata for Node.js HTTP requests.
|
|
325
|
+
*
|
|
326
|
+
* Provides information about the client connection, including the remote
|
|
327
|
+
* address and a promise that resolves when the connection is fully closed
|
|
328
|
+
* (including WebSocket connections).
|
|
329
|
+
*/
|
|
251
330
|
export type NodeConnectionInfo = {
|
|
331
|
+
/** Promise that resolves when the connection is fully closed. */
|
|
252
332
|
completed: Promise<void>
|
|
333
|
+
/** The remote address of the client, if available. */
|
|
253
334
|
remoteAddr: NetAddr | undefined
|
|
254
335
|
}
|
|
255
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Interface for objects that can handle fetch-style requests.
|
|
339
|
+
*
|
|
340
|
+
* Used by {@link createServer} and {@link serve} to accept either
|
|
341
|
+
* a fetch handler function or an object with a `fetch` method
|
|
342
|
+
* (like {@link LexRouter}).
|
|
343
|
+
*/
|
|
256
344
|
export interface HandlerObject {
|
|
345
|
+
/** The fetch handler method. */
|
|
257
346
|
fetch: FetchHandler
|
|
258
347
|
}
|
|
259
348
|
|
|
@@ -284,6 +373,32 @@ function toConnectionInfo(req: IncomingMessage): NodeConnectionInfo {
|
|
|
284
373
|
}
|
|
285
374
|
}
|
|
286
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Converts a fetch-style handler to a Node.js request listener.
|
|
378
|
+
*
|
|
379
|
+
* The returned listener can be used with Node.js HTTP servers directly,
|
|
380
|
+
* or as middleware in frameworks like Express (supports the `next` callback).
|
|
381
|
+
*
|
|
382
|
+
* @typeParam Request - The request class type (default: IncomingMessage)
|
|
383
|
+
* @typeParam Response - The response class type (default: ServerResponse)
|
|
384
|
+
* @param fetchHandler - The fetch-style handler function
|
|
385
|
+
* @returns A Node.js RequestListener compatible with http.createServer
|
|
386
|
+
*
|
|
387
|
+
* @example Using as Express middleware
|
|
388
|
+
* ```typescript
|
|
389
|
+
* import express from 'express'
|
|
390
|
+
* import { toRequestListener } from '@atproto/lex-server/nodejs'
|
|
391
|
+
* import { LexRouter } from '@atproto/lex-server'
|
|
392
|
+
*
|
|
393
|
+
* const router = new LexRouter()
|
|
394
|
+
* // Register handlers...
|
|
395
|
+
*
|
|
396
|
+
* const app = express()
|
|
397
|
+
*
|
|
398
|
+
* // Mount the XRPC router
|
|
399
|
+
* app.use('/xrpc', toRequestListener(router.fetch))
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
287
402
|
export function toRequestListener<
|
|
288
403
|
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
289
404
|
Response extends typeof ServerResponse<
|
|
@@ -310,15 +425,54 @@ export function toRequestListener<
|
|
|
310
425
|
}) satisfies RequestListener<Request, Response>
|
|
311
426
|
}
|
|
312
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Options for creating an XRPC server.
|
|
430
|
+
*
|
|
431
|
+
* Extends Node.js {@link ServerOptions} with additional options for graceful shutdown.
|
|
432
|
+
*/
|
|
313
433
|
export type CreateServerOptions<
|
|
314
434
|
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
315
435
|
Response extends typeof ServerResponse<
|
|
316
436
|
InstanceType<Request>
|
|
317
437
|
> = typeof ServerResponse,
|
|
318
438
|
> = ServerOptions<Request, Response> & {
|
|
439
|
+
/**
|
|
440
|
+
* Timeout in milliseconds for graceful termination.
|
|
441
|
+
*
|
|
442
|
+
* When `terminate()` is called, the server will wait up to this duration
|
|
443
|
+
* for active connections to complete before forcibly closing them.
|
|
444
|
+
*/
|
|
319
445
|
gracefulTerminationTimeout?: number
|
|
320
446
|
}
|
|
321
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Extended HTTP server with graceful shutdown support.
|
|
450
|
+
*
|
|
451
|
+
* Extends the standard Node.js HttpServer with a `terminate()` method
|
|
452
|
+
* for graceful shutdown and implements `AsyncDisposable` for use with
|
|
453
|
+
* `await using`.
|
|
454
|
+
*
|
|
455
|
+
* @typeParam Request - The request class type
|
|
456
|
+
* @typeParam Response - The response class type
|
|
457
|
+
*
|
|
458
|
+
* @example Graceful shutdown
|
|
459
|
+
* ```typescript
|
|
460
|
+
* const server = createServer(router)
|
|
461
|
+
* server.listen(3000)
|
|
462
|
+
*
|
|
463
|
+
* process.on('SIGTERM', async () => {
|
|
464
|
+
* console.log('Shutting down...')
|
|
465
|
+
* await server.terminate()
|
|
466
|
+
* console.log('Server stopped')
|
|
467
|
+
* })
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
470
|
+
* @example Using with await using
|
|
471
|
+
* ```typescript
|
|
472
|
+
* await using server = await serve(router, { port: 3000 })
|
|
473
|
+
* // Server will be automatically terminated when scope exits
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
322
476
|
export interface Server<
|
|
323
477
|
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
324
478
|
Response extends typeof ServerResponse<
|
|
@@ -326,10 +480,56 @@ export interface Server<
|
|
|
326
480
|
> = typeof ServerResponse,
|
|
327
481
|
> extends HttpServer<Request, Response>,
|
|
328
482
|
AsyncDisposable {
|
|
483
|
+
/**
|
|
484
|
+
* Gracefully terminates the server.
|
|
485
|
+
*
|
|
486
|
+
* Stops accepting new connections and waits for active connections
|
|
487
|
+
* to complete (up to `gracefulTerminationTimeout`).
|
|
488
|
+
*
|
|
489
|
+
* @returns Promise that resolves when the server is fully stopped
|
|
490
|
+
*/
|
|
329
491
|
terminate(): Promise<void>
|
|
330
492
|
[Symbol.asyncDispose](): Promise<void>
|
|
331
493
|
}
|
|
332
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Creates an HTTP server configured for XRPC request handling.
|
|
497
|
+
*
|
|
498
|
+
* The server includes graceful shutdown support and can be used with
|
|
499
|
+
* either a fetch handler function or an object with a `fetch` method
|
|
500
|
+
* (like {@link LexRouter}).
|
|
501
|
+
*
|
|
502
|
+
* Note: This creates the server but does not start listening. Call
|
|
503
|
+
* `server.listen()` to start the server, or use {@link serve} for
|
|
504
|
+
* a combined create-and-listen operation.
|
|
505
|
+
*
|
|
506
|
+
* @typeParam Request - The request class type
|
|
507
|
+
* @typeParam Response - The response class type
|
|
508
|
+
* @param handler - A fetch handler or object with fetch method
|
|
509
|
+
* @param options - Server configuration options
|
|
510
|
+
* @returns An HTTP server with graceful shutdown support
|
|
511
|
+
*
|
|
512
|
+
* @example Basic usage
|
|
513
|
+
* ```typescript
|
|
514
|
+
* import { LexRouter } from '@atproto/lex-server'
|
|
515
|
+
* import { createServer, upgradeWebSocket } from '@atproto/lex-server/nodejs'
|
|
516
|
+
*
|
|
517
|
+
* const router = new LexRouter({ upgradeWebSocket })
|
|
518
|
+
* router.add(myMethod, myHandler)
|
|
519
|
+
*
|
|
520
|
+
* const server = createServer(router)
|
|
521
|
+
* server.listen(3000, () => {
|
|
522
|
+
* console.log('Server listening on port 3000')
|
|
523
|
+
* })
|
|
524
|
+
* ```
|
|
525
|
+
*
|
|
526
|
+
* @example With graceful termination timeout
|
|
527
|
+
* ```typescript
|
|
528
|
+
* const server = createServer(router, {
|
|
529
|
+
* gracefulTerminationTimeout: 10000 // 10 seconds
|
|
530
|
+
* })
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
333
533
|
export function createServer<
|
|
334
534
|
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
335
535
|
Response extends typeof ServerResponse<
|
|
@@ -375,6 +575,23 @@ export function createServer<
|
|
|
375
575
|
return server as Server<Request, Response>
|
|
376
576
|
}
|
|
377
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Combined options for creating and starting an XRPC server.
|
|
580
|
+
*
|
|
581
|
+
* Includes both server creation options and network listen options.
|
|
582
|
+
*
|
|
583
|
+
* @typeParam Request - The request class type
|
|
584
|
+
* @typeParam Response - The response class type
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* ```typescript
|
|
588
|
+
* const options: StartServerOptions = {
|
|
589
|
+
* port: 3000,
|
|
590
|
+
* host: '0.0.0.0',
|
|
591
|
+
* gracefulTerminationTimeout: 10000
|
|
592
|
+
* }
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
378
595
|
export type StartServerOptions<
|
|
379
596
|
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
380
597
|
Response extends typeof ServerResponse<
|
|
@@ -382,6 +599,67 @@ export type StartServerOptions<
|
|
|
382
599
|
> = typeof ServerResponse,
|
|
383
600
|
> = ListenOptions & CreateServerOptions<Request, Response>
|
|
384
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Creates and starts an HTTP server, returning when it's ready to accept connections.
|
|
604
|
+
*
|
|
605
|
+
* This is a convenience function that combines {@link createServer} and `server.listen()`
|
|
606
|
+
* into a single async operation. The returned promise resolves once the server
|
|
607
|
+
* is actively listening.
|
|
608
|
+
*
|
|
609
|
+
* @typeParam Request - The request class type
|
|
610
|
+
* @typeParam Response - The response class type
|
|
611
|
+
* @param handler - A fetch handler or object with fetch method (like {@link LexRouter})
|
|
612
|
+
* @param options - Combined server and listen options
|
|
613
|
+
* @returns Promise resolving to the running server
|
|
614
|
+
*
|
|
615
|
+
* @example Basic usage
|
|
616
|
+
* ```typescript
|
|
617
|
+
* import { LexRouter } from '@atproto/lex-server'
|
|
618
|
+
* import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
|
|
619
|
+
*
|
|
620
|
+
* const router = new LexRouter({ upgradeWebSocket })
|
|
621
|
+
*
|
|
622
|
+
* // Register handlers
|
|
623
|
+
* router.add(getProfile, async (ctx) => {
|
|
624
|
+
* return { body: await db.getProfile(ctx.params.actor) }
|
|
625
|
+
* })
|
|
626
|
+
*
|
|
627
|
+
* // Start server on port 3000
|
|
628
|
+
* const server = await serve(router, { port: 3000 })
|
|
629
|
+
* console.log('Server listening on port 3000')
|
|
630
|
+
*
|
|
631
|
+
* // Graceful shutdown
|
|
632
|
+
* process.on('SIGTERM', () => server.terminate())
|
|
633
|
+
* process.on('SIGINT', () => server.terminate())
|
|
634
|
+
* ```
|
|
635
|
+
*
|
|
636
|
+
* @example With all options
|
|
637
|
+
* ```typescript
|
|
638
|
+
* const server = await serve(router, {
|
|
639
|
+
* port: 3000,
|
|
640
|
+
* host: '0.0.0.0',
|
|
641
|
+
* gracefulTerminationTimeout: 15000,
|
|
642
|
+
* })
|
|
643
|
+
* ```
|
|
644
|
+
*
|
|
645
|
+
* @example Using with await using (auto-cleanup)
|
|
646
|
+
* ```typescript
|
|
647
|
+
* async function main() {
|
|
648
|
+
* await using server = await serve(router, { port: 3000 })
|
|
649
|
+
*
|
|
650
|
+
* // Server is running...
|
|
651
|
+
* console.log('Server listening on port 3000')
|
|
652
|
+
*
|
|
653
|
+
* // Wait for termination signal
|
|
654
|
+
* await Promise.race([
|
|
655
|
+
* once(process, 'SIGINT'),
|
|
656
|
+
* once(process, 'SIGTERM'),
|
|
657
|
+
* ])
|
|
658
|
+
*
|
|
659
|
+
* // Server will be automatically terminated when scope exits
|
|
660
|
+
* }
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
385
663
|
export async function serve<
|
|
386
664
|
Request extends typeof IncomingMessage = typeof IncomingMessage,
|
|
387
665
|
Response extends typeof ServerResponse<
|
package/src/service-auth.ts
CHANGED
|
@@ -17,41 +17,150 @@ import { LexRouterAuth } from './lex-server.js'
|
|
|
17
17
|
const BEARER_PREFIX = 'Bearer '
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Callback function to check and record nonce uniqueness.
|
|
21
|
+
*
|
|
22
|
+
* Used to prevent replay attacks by ensuring each nonce is only used once.
|
|
23
|
+
* The implementation must track nonces for at least the `maxAge` duration
|
|
24
|
+
* (default 5 minutes before and after the current time).
|
|
25
|
+
*
|
|
26
|
+
* @param nonce - The nonce string from the JWT token
|
|
27
|
+
* @returns Promise resolving to `true` if the nonce is unique (first time seen),
|
|
28
|
+
* `false` if it has been seen before
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Using Redis for nonce tracking
|
|
33
|
+
* const checkNonce: UniqueNonceChecker = async (nonce) => {
|
|
34
|
+
* const key = `nonce:${nonce}`
|
|
35
|
+
* const result = await redis.setnx(key, '1')
|
|
36
|
+
* if (result === 1) {
|
|
37
|
+
* await redis.expire(key, 600) // 10 minutes TTL
|
|
38
|
+
* return true
|
|
39
|
+
* }
|
|
40
|
+
* return false
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
21
43
|
*/
|
|
22
44
|
export type UniqueNonceChecker = (nonce: string) => Promise<boolean>
|
|
23
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Configuration options for AT Protocol service authentication.
|
|
48
|
+
*
|
|
49
|
+
* Service auth is used for server-to-server communication in the AT Protocol,
|
|
50
|
+
* where one service authenticates to another using signed JWT tokens tied to
|
|
51
|
+
* the caller's DID.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const options: ServiceAuthOptions = {
|
|
56
|
+
* audience: 'did:web:api.example.com',
|
|
57
|
+
* unique: async (nonce) => nonceStore.checkAndAdd(nonce),
|
|
58
|
+
* maxAge: 300, // 5 minutes
|
|
59
|
+
* // Optional DID resolver options
|
|
60
|
+
* plcDirectoryUrl: 'https://plc.directory'
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
24
64
|
export type ServiceAuthOptions = CreateDidResolverOptions & {
|
|
25
65
|
/**
|
|
26
|
-
* Expected audience ("aud") claim in the JWT token.
|
|
27
|
-
*
|
|
66
|
+
* Expected audience ("aud") claim in the JWT token.
|
|
67
|
+
*
|
|
68
|
+
* This should be the DID of your service. The token must include this
|
|
69
|
+
* value in its `aud` claim to be accepted. Set to `null` to skip
|
|
70
|
+
* audience verification (not recommended for production).
|
|
28
71
|
*/
|
|
29
72
|
audience: null | DidString
|
|
30
73
|
/**
|
|
31
|
-
* Function to check and record nonce uniqueness.
|
|
32
|
-
* be unique within {@link ServiceAuthOptions.maxAge} seconds before and after
|
|
33
|
-
* the current time.
|
|
74
|
+
* Function to check and record nonce uniqueness.
|
|
34
75
|
*
|
|
35
|
-
*
|
|
76
|
+
* This is critical for preventing replay attacks. The value checked here
|
|
77
|
+
* must be unique within `maxAge` seconds before and after the current time.
|
|
78
|
+
*
|
|
79
|
+
* @param nonce - The nonce to check
|
|
80
|
+
* @returns Promise resolving to `true` if unique, `false` if seen before
|
|
36
81
|
*/
|
|
37
82
|
unique: UniqueNonceChecker
|
|
38
83
|
/**
|
|
39
84
|
* Maximum age of the JWT token in seconds.
|
|
40
85
|
*
|
|
86
|
+
* Tokens with `iat` (issued at) or `exp` (expiry) timestamps outside
|
|
87
|
+
* this window from the current time will be rejected.
|
|
88
|
+
*
|
|
41
89
|
* @default 300 (5 minutes)
|
|
42
90
|
*/
|
|
43
91
|
maxAge?: number
|
|
44
92
|
}
|
|
45
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Credentials returned after successful service authentication.
|
|
96
|
+
*
|
|
97
|
+
* Contains the verified DID, resolved DID document, and parsed JWT token.
|
|
98
|
+
* These are available in handler context as `ctx.credentials`.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* router.add(protectedMethod, {
|
|
103
|
+
* handler: async (ctx) => {
|
|
104
|
+
* const { did, didDocument, jwt } = ctx.credentials
|
|
105
|
+
* console.log('Request from:', did)
|
|
106
|
+
* console.log('Token expires:', new Date(jwt.payload.exp * 1000))
|
|
107
|
+
* return { body: { callerDid: did } }
|
|
108
|
+
* },
|
|
109
|
+
* auth: serviceAuth({ audience: myDid, unique: checkNonce })
|
|
110
|
+
* })
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
46
113
|
export type ServiceAuthCredentials = {
|
|
114
|
+
/** The verified AT Protocol DID of the caller. */
|
|
47
115
|
did: AtprotoDid
|
|
116
|
+
/** The resolved DID document of the caller. */
|
|
48
117
|
didDocument: AtprotoDidDocument
|
|
118
|
+
/** The parsed and validated JWT token. */
|
|
49
119
|
jwt: ParsedJwt
|
|
50
120
|
}
|
|
51
121
|
|
|
52
122
|
/**
|
|
53
|
-
* Creates an authentication handler for
|
|
54
|
-
*
|
|
123
|
+
* Creates an authentication handler for verifying AT Protocol service auth JWTs.
|
|
124
|
+
*
|
|
125
|
+
* Service auth is the standard authentication mechanism for server-to-server
|
|
126
|
+
* communication in the AT Protocol. It uses JWT bearer tokens signed by the
|
|
127
|
+
* caller's DID signing key, with the signature verified against the public
|
|
128
|
+
* key in the caller's DID document.
|
|
129
|
+
*
|
|
130
|
+
* The handler performs the following validations:
|
|
131
|
+
* - Extracts and parses the Bearer token from the Authorization header
|
|
132
|
+
* - Validates JWT structure and claims (aud, exp, iat, lxm, nonce)
|
|
133
|
+
* - Resolves the issuer's DID document
|
|
134
|
+
* - Verifies the JWT signature against the `#atproto` verification method
|
|
135
|
+
* - Checks nonce uniqueness to prevent replay attacks
|
|
136
|
+
*
|
|
137
|
+
* @param options - Configuration options for service auth
|
|
138
|
+
* @returns An auth handler function for use with {@link LexRouter.add}
|
|
139
|
+
*
|
|
140
|
+
* @example Basic usage
|
|
141
|
+
* ```typescript
|
|
142
|
+
* import { LexRouter, serviceAuth } from '@atproto/lex-server'
|
|
143
|
+
*
|
|
144
|
+
* const router = new LexRouter()
|
|
145
|
+
*
|
|
146
|
+
* const auth = serviceAuth({
|
|
147
|
+
* audience: 'did:web:api.example.com',
|
|
148
|
+
* unique: async (nonce) => {
|
|
149
|
+
* // Check if nonce has been seen, return true if unique
|
|
150
|
+
* const isNew = await redis.setnx(`nonce:${nonce}`, '1')
|
|
151
|
+
* if (isNew) await redis.expire(`nonce:${nonce}`, 600)
|
|
152
|
+
* return isNew
|
|
153
|
+
* }
|
|
154
|
+
* })
|
|
155
|
+
*
|
|
156
|
+
* router.add(myMethod, {
|
|
157
|
+
* handler: async (ctx) => {
|
|
158
|
+
* console.log('Authenticated as:', ctx.credentials.did)
|
|
159
|
+
* return { body: { success: true } }
|
|
160
|
+
* },
|
|
161
|
+
* auth
|
|
162
|
+
* })
|
|
163
|
+
* ```
|
|
55
164
|
*/
|
|
56
165
|
export function serviceAuth({
|
|
57
166
|
audience,
|
|
@@ -190,17 +299,50 @@ async function parseJwtBearer(
|
|
|
190
299
|
return parseJwt(token, options)
|
|
191
300
|
}
|
|
192
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Options for parsing and validating a JWT token.
|
|
304
|
+
*/
|
|
193
305
|
export type ParseJwtOptions = {
|
|
306
|
+
/** Maximum age in seconds for token validity window. */
|
|
194
307
|
maxAge: number
|
|
308
|
+
/** Expected audience claim, or null to skip audience verification. */
|
|
195
309
|
audience: null | DidString
|
|
310
|
+
/** Function to check nonce uniqueness. */
|
|
196
311
|
unique: UniqueNonceChecker
|
|
312
|
+
/** Expected lexicon method NSID for the `lxm` claim. */
|
|
197
313
|
lxm: string
|
|
198
314
|
}
|
|
199
315
|
|
|
316
|
+
/**
|
|
317
|
+
* A parsed and partially validated JWT token.
|
|
318
|
+
*
|
|
319
|
+
* Contains the decoded header and payload, along with the raw bytes
|
|
320
|
+
* needed for signature verification.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const jwt: ParsedJwt = {
|
|
325
|
+
* header: { alg: 'ES256K', typ: 'JWT' },
|
|
326
|
+
* payload: {
|
|
327
|
+
* iss: 'did:plc:abc123',
|
|
328
|
+
* aud: 'did:web:api.example.com',
|
|
329
|
+
* exp: 1704067200,
|
|
330
|
+
* iat: 1704066900,
|
|
331
|
+
* lxm: 'com.atproto.sync.getBlob'
|
|
332
|
+
* },
|
|
333
|
+
* message: new Uint8Array([...]),
|
|
334
|
+
* signature: new Uint8Array([...])
|
|
335
|
+
* }
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
200
338
|
export type ParsedJwt = {
|
|
339
|
+
/** The decoded JWT header containing algorithm and type. */
|
|
201
340
|
header: HeaderObject
|
|
341
|
+
/** The decoded JWT payload containing claims. */
|
|
202
342
|
payload: PayloadObject
|
|
343
|
+
/** The raw header.payload bytes for signature verification. */
|
|
203
344
|
message: Uint8Array
|
|
345
|
+
/** The decoded signature bytes. */
|
|
204
346
|
signature: Uint8Array
|
|
205
347
|
}
|
|
206
348
|
|