@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/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
- private handlers: Map<NsidString, FetchHandler> = new Map()
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 {