@atproto/lex-server 0.0.11 → 0.0.13

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 (42) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +38 -21
  3. package/dist/errors.d.ts +28 -58
  4. package/dist/errors.d.ts.map +1 -1
  5. package/dist/errors.js +72 -72
  6. package/dist/errors.js.map +1 -1
  7. package/dist/index.d.ts +1 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -4
  10. package/dist/index.js.map +1 -1
  11. package/dist/{lex-server.d.ts → lex-router.d.ts} +55 -21
  12. package/dist/lex-router.d.ts.map +1 -0
  13. package/dist/{lex-server.js → lex-router.js} +169 -73
  14. package/dist/lex-router.js.map +1 -0
  15. package/dist/lib/drain-websocket.d.ts +7 -0
  16. package/dist/lib/drain-websocket.d.ts.map +1 -1
  17. package/dist/lib/drain-websocket.js +11 -0
  18. package/dist/lib/drain-websocket.js.map +1 -1
  19. package/dist/lib/www-authenticate.d.ts +4 -3
  20. package/dist/lib/www-authenticate.d.ts.map +1 -1
  21. package/dist/lib/www-authenticate.js +29 -16
  22. package/dist/lib/www-authenticate.js.map +1 -1
  23. package/dist/nodejs.d.ts +1 -1
  24. package/dist/nodejs.d.ts.map +1 -1
  25. package/dist/nodejs.js +1 -1
  26. package/dist/nodejs.js.map +1 -1
  27. package/dist/service-auth.d.ts +1 -1
  28. package/dist/service-auth.d.ts.map +1 -1
  29. package/dist/service-auth.js.map +1 -1
  30. package/package.json +9 -8
  31. package/src/errors.test.ts +262 -0
  32. package/src/errors.ts +103 -78
  33. package/src/index.ts +1 -7
  34. package/src/{lex-server.test.ts → lex-router.test.ts} +591 -24
  35. package/src/{lex-server.ts → lex-router.ts} +275 -119
  36. package/src/lib/drain-websocket.ts +11 -0
  37. package/src/lib/www-authenticate.test.ts +134 -0
  38. package/src/lib/www-authenticate.ts +36 -17
  39. package/src/nodejs.ts +2 -2
  40. package/src/service-auth.ts +1 -1
  41. package/dist/lex-server.d.ts.map +0 -1
  42. package/dist/lex-server.js.map +0 -1
@@ -1,4 +1,5 @@
1
- import { InferMethodInput, InferMethodMessage, InferMethodOutput, InferMethodOutputBody, InferMethodOutputEncoding, InferMethodParams, Main, NsidString, Procedure, Query, Subscription } from '@atproto/lex-schema';
1
+ import { DidString, InferMethodInput, InferMethodMessage, InferMethodOutput, InferMethodOutputBody, InferMethodOutputEncoding, InferMethodParams, Main, NsidString, Procedure, Query, Subscription } from '@atproto/lex-schema';
2
+ import { LexServerError } from './errors.js';
2
3
  type Awaitable<T> = T | Promise<T>;
3
4
  /**
4
5
  * Union type representing the supported Lexicon method types.
@@ -270,7 +271,7 @@ export type LexRouterSubscriptionConfig<Method extends Subscription = Subscripti
270
271
  * ```typescript
271
272
  * const authHandler: LexRouterAuth<UserCredentials> = async (ctx) => {
272
273
  * const token = ctx.request.headers.get('authorization')
273
- * if (!token) throw new LexError('AuthenticationRequired', 'Missing token')
274
+ * if (!token) throw new LexServerAuthError('AuthenticationRequired', 'Missing token')
274
275
  * return { userId: await verifyToken(token) }
275
276
  * }
276
277
  * ```
@@ -299,8 +300,9 @@ export type LexRouterAuthContext<Method extends LexMethod = LexMethod> = {
299
300
  * // Simple token-based auth
300
301
  * const tokenAuth: LexRouterAuth<{ userId: string }> = async ({ request }) => {
301
302
  * const token = request.headers.get('authorization')?.replace('Bearer ', '')
302
- * if (!token) throw new LexError('AuthenticationRequired', 'Token required')
303
+ * if (!token) throw new LexServerAuthError('AuthenticationRequired', 'Token required')
303
304
  * const userId = await verifyToken(token)
305
+ * if (!userId) throw new LexServerAuthError('AuthenticationRequired', 'Invalid token')
304
306
  * return { userId }
305
307
  * }
306
308
  *
@@ -315,14 +317,18 @@ export type LexRouterAuth<Credentials = unknown, Method extends LexMethod = LexM
315
317
  *
316
318
  * Used for logging and monitoring errors that occur during request handling.
317
319
  */
318
- export type LexErrorHandlerContext = {
319
- /** The error that was thrown during handling. */
320
- error: unknown;
321
- /** The original HTTP request that triggered the error. */
320
+ export type HandlerErrorContext = {
322
321
  request: Request;
323
- /** The Lexicon method that was being executed. */
324
322
  method: LexMethod;
323
+ error: LexServerError;
324
+ };
325
+ export type HandlerErrorHook = (ctx: HandlerErrorContext) => void | Promise<void>;
326
+ export type SocketErrorContext = {
327
+ request: Request;
328
+ method: Subscription;
329
+ error: unknown;
325
330
  };
331
+ export type SocketErrorHook = (ctx: SocketErrorContext) => void | Promise<void>;
326
332
  /**
327
333
  * Function that upgrades an HTTP request to a WebSocket connection.
328
334
  *
@@ -346,6 +352,10 @@ export type UpgradeWebSocket = (request: Request) => {
346
352
  /** The HTTP response to return (101 Switching Protocols). */
347
353
  response: Response;
348
354
  };
355
+ export type HealthCheckHandler = (request: Request) => Awaitable<{
356
+ [x: string]: unknown;
357
+ status: 'ok';
358
+ }>;
349
359
  /**
350
360
  * Configuration options for the {@link LexRouter}.
351
361
  *
@@ -364,25 +374,45 @@ export type UpgradeWebSocket = (request: Request) => {
364
374
  */
365
375
  export type LexRouterOptions = {
366
376
  /**
367
- * Function to upgrade HTTP requests to WebSocket connections.
368
- * Required for subscription methods. Defaults to Deno's built-in
369
- * upgradeWebSocket if available.
377
+ * Function to upgrade HTTP requests to WebSocket connections. Required for
378
+ * subscription methods. Defaults to Deno's built-in
379
+ * {@link globalThis.upgradeWebSocket} if available. For NodeJS, use the
380
+ * homonymous export from `@atproto/lex-server/nodejs`.
370
381
  */
371
382
  upgradeWebSocket?: UpgradeWebSocket;
372
383
  /**
373
- * Callback invoked when an error occurs during request handling.
374
- * Useful for logging and error reporting. Not called for client-induced
375
- * errors (e.g., request abortion).
384
+ * Callback invoked when an error occurs during request handling. Useful for
385
+ * logging and error reporting. Not called for client-induced errors (e.g.,
386
+ * request abortion).
387
+ */
388
+ onHandlerError?: HandlerErrorHook;
389
+ /**
390
+ * Optional hook for handling errors during generation of WebSocket messages.
376
391
  */
377
- onHandlerError?: (ctx: LexErrorHandlerContext) => void | Promise<void>;
392
+ onSocketError?: SocketErrorHook;
378
393
  /**
379
- * High water mark for WebSocket backpressure (in bytes).
380
- * When buffered data exceeds this, the handler will wait before sending more.
394
+ * Optional health check handler. If provided, this function will be called
395
+ * for requests to the /xrpc/_health endpoint, allowing for custom health
396
+ * check logic and responses.
397
+ *
398
+ * If not provided, the server will respond to /xrpc/_health requests with a
399
+ * default JSON response of `{ status: 'ok' }`.
400
+ */
401
+ healthCheck?: HealthCheckHandler;
402
+ /**
403
+ * Optional fallback handler for requests that are not /xrpc/ paths. Can be
404
+ * used to serve static files or other routes. If not provided, non-/xrpc/
405
+ * requests will return 404 responses.
406
+ */
407
+ fallback?: FetchHandler;
408
+ /**
409
+ * High water mark for WebSocket backpressure (in bytes). When buffered data
410
+ * exceeds this, the handler will wait before sending more.
381
411
  */
382
412
  highWaterMark?: number;
383
413
  /**
384
- * Low water mark for WebSocket backpressure (in bytes).
385
- * The handler resumes sending when buffered data drops below this.
414
+ * Low water mark for WebSocket backpressure (in bytes). The handler resumes
415
+ * sending when buffered data drops below this.
386
416
  */
387
417
  lowWaterMark?: number;
388
418
  };
@@ -527,7 +557,7 @@ export declare class LexRouter {
527
557
  add<M extends LexMethod, Credentials = unknown>(ns: Main<M>, config: M extends Subscription ? LexRouterSubscriptionHandler<M, Credentials> | LexRouterSubscriptionConfig<M, Credentials> : M extends Query | Procedure ? LexRouterMethodHandler<M, Credentials> | LexRouterMethodConfig<M, Credentials> : never): this;
528
558
  private buildMethodHandler;
529
559
  private buildSubscriptionHandler;
530
- private handleError;
560
+ private handlerError;
531
561
  /**
532
562
  * The main fetch handler for processing XRPC requests.
533
563
  *
@@ -559,5 +589,9 @@ export declare class LexRouter {
559
589
  */
560
590
  fetch: FetchHandler;
561
591
  }
592
+ export type ServiceProxyInfo = {
593
+ did: DidString;
594
+ serviceId: string;
595
+ };
562
596
  export {};
563
- //# sourceMappingURL=lex-server.d.ts.map
597
+ //# sourceMappingURL=lex-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lex-router.d.ts","sourceRoot":"","sources":["../src/lex-router.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,10 @@ const lex_cbor_1 = require("@atproto/lex-cbor");
5
5
  const lex_data_1 = require("@atproto/lex-data");
6
6
  const lex_json_1 = require("@atproto/lex-json");
7
7
  const lex_schema_1 = require("@atproto/lex-schema");
8
+ const errors_js_1 = require("./errors.js");
8
9
  const drain_websocket_js_1 = require("./lib/drain-websocket.js");
10
+ const XRPC_PATH_PREFIX = '/xrpc/';
11
+ const XRPC_HEALTH_CHECK_PATH = '/xrpc/_health';
9
12
  /**
10
13
  * XRPC router for handling AT Protocol Lexicon methods.
11
14
  *
@@ -88,16 +91,17 @@ class LexRouter {
88
91
  }
89
92
  add(ns, config) {
90
93
  const method = (0, lex_schema_1.getMain)(ns);
91
- if (this.handlers.has(method.nsid)) {
94
+ const nsid = normalizeNsid(method.nsid);
95
+ if (this.handlers.has(nsid)) {
92
96
  throw new TypeError(`Method ${method.nsid} already registered`);
93
97
  }
94
98
  const methodConfig = typeof config === 'function'
95
99
  ? { handler: config, auth: undefined }
96
100
  : config;
97
- const fetch = method.type === 'subscription'
101
+ const handler = method.type === 'subscription'
98
102
  ? this.buildSubscriptionHandler(method, methodConfig.handler, methodConfig.auth)
99
103
  : this.buildMethodHandler(method, methodConfig.handler, methodConfig.auth);
100
- this.handlers.set(method.nsid, fetch);
104
+ this.handlers.set(nsid, handler);
101
105
  return this;
102
106
  }
103
107
  buildMethodHandler(method, methodHandler, auth) {
@@ -111,7 +115,7 @@ class LexRouter {
111
115
  (method.type === 'query' &&
112
116
  request.method !== 'GET' &&
113
117
  request.method !== 'HEAD')) {
114
- return Response.json({ error: 'InvalidRequest', message: 'Method not allowed' }, { status: 405 });
118
+ return invalidRequestResponse('Method not allowed', 405);
115
119
  }
116
120
  try {
117
121
  const url = new URL(request.url);
@@ -149,34 +153,28 @@ class LexRouter {
149
153
  });
150
154
  }
151
155
  catch (error) {
152
- return this.handleError(request, method, error);
156
+ return this.handlerError(request, method, error);
153
157
  }
154
158
  };
155
159
  }
156
160
  buildSubscriptionHandler(method, methodHandler, auth) {
157
- const { onHandlerError, upgradeWebSocket = globalThis.Deno?.upgradeWebSocket, } = this.options;
161
+ const { onSocketError, upgradeWebSocket = globalThis.Deno?.upgradeWebSocket, } = this.options;
158
162
  if (!upgradeWebSocket) {
159
163
  throw new TypeError('WebSocket upgrade not supported in this environment. Please provide an upgradeWebSocket option when creating the LexRouter.');
160
164
  }
161
165
  return async (request, connection) => {
162
166
  if (request.method !== 'GET') {
163
- return Response.json({ error: 'InvalidRequest', message: 'Method not allowed' }, { status: 405 });
167
+ return invalidRequestResponse('Method not allowed', 405);
164
168
  }
165
169
  if (request.headers.get('connection')?.toLowerCase() !== 'upgrade' ||
166
170
  request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
167
- return Response.json({
168
- error: 'InvalidRequest',
169
- message: 'XRPC subscriptions are only available over WebSocket',
170
- }, {
171
- status: 426,
172
- headers: {
173
- Connection: 'Upgrade',
174
- Upgrade: 'websocket',
175
- },
171
+ return invalidRequestResponse('XRPC subscriptions are only available over WebSocket', 426, {
172
+ Connection: 'Upgrade',
173
+ Upgrade: 'websocket',
176
174
  });
177
175
  }
178
176
  if (request.signal.aborted) {
179
- return Response.json({ error: 'RequestAborted', message: 'The request was aborted' }, { status: 499 });
177
+ return invalidRequestResponse('Request aborted', 499);
180
178
  }
181
179
  try {
182
180
  const { response, socket } = upgradeWebSocket(request);
@@ -186,11 +184,6 @@ class LexRouter {
186
184
  const abortController = new AbortController();
187
185
  const { signal } = abortController;
188
186
  const abort = () => abortController.abort();
189
- const onMessage = (event) => {
190
- const error = new lex_data_1.LexError('InvalidRequest', 'XRPC subscriptions do not accept messages', { cause: event });
191
- socket.send(encodeErrorFrame(error));
192
- socket.close(1008, error.error);
193
- };
194
187
  const onOpen = async () => {
195
188
  try {
196
189
  const url = new URL(request.url);
@@ -208,10 +201,23 @@ class LexRouter {
208
201
  signal,
209
202
  });
210
203
  const iterator = iterable[Symbol.asyncIterator]();
211
- signal.addEventListener('abort', async () => {
212
- // @NOTE will cause the process to crash if this throws
213
- await iterator.return?.();
214
- });
204
+ if (iterator.return) {
205
+ signal.addEventListener('abort', () => {
206
+ // @NOTE if iterator.return() throws, and no onSocketError is
207
+ // provided, or if onSocketError itself throws, the error will
208
+ // be unhandled, causing the process to crash. This is
209
+ // intentional, as it surfaces critical errors that occur
210
+ // during cleanup of the subscription.
211
+ void new Promise((resolve) => {
212
+ // Wrapping in new Promise to catch any potential sync errors thrown by iterator.return()
213
+ resolve(iterator.return());
214
+ }).catch(onSocketError
215
+ ? (error) => onSocketError({ request, method, error })
216
+ : null);
217
+ }, {
218
+ once: true,
219
+ });
220
+ }
215
221
  while (!signal.aborted && socket.readyState === 1) {
216
222
  const result = await iterator.next();
217
223
  if (result.done)
@@ -230,17 +236,24 @@ class LexRouter {
230
236
  catch (error) {
231
237
  // If the socket is still open, send an error frame before closing
232
238
  if (socket.readyState === 1) {
233
- const lexError = error instanceof lex_data_1.LexError
234
- ? error
235
- : new lex_data_1.LexError('InternalError', 'An internal error occurred');
236
- socket.send(encodeErrorFrame(lexError));
237
- socket.close(
239
+ const isLexError = error instanceof lex_data_1.LexError;
238
240
  // https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
239
- error instanceof lex_data_1.LexError ? 1008 : 1011, lexError.error);
241
+ const code = isLexError && method.errors?.includes(error.error)
242
+ ? 1008 // Policy Violation for known LexErrors
243
+ : 1011; // Internal Error for unexpected errors
244
+ if (isLexError) {
245
+ socket.send(encodeErrorFrame(error.toJSON()));
246
+ socket.close(code, error.error);
247
+ }
248
+ else {
249
+ const error = 'InternalServerError';
250
+ const message = 'An internal error occurred';
251
+ socket.send(encodeErrorFrame({ error, message }));
252
+ socket.close(code, error);
253
+ }
240
254
  }
241
- // Only report unexpected processing errors
242
- if (onHandlerError && !isAbortReason(request.signal, error)) {
243
- await onHandlerError({ error, request, method });
255
+ if (onSocketError && !isAbortReason(signal, error)) {
256
+ await onSocketError({ request, method, error });
244
257
  }
245
258
  }
246
259
  finally {
@@ -254,20 +267,20 @@ class LexRouter {
254
267
  return response;
255
268
  }
256
269
  catch (error) {
257
- return this.handleError(request, method, error);
270
+ return this.handlerError(request, method, error);
258
271
  }
259
272
  };
260
273
  }
261
- async handleError(request, method, error) {
274
+ async handlerError(request, method, cause) {
262
275
  // Only report unexpected processing errors
276
+ if (isAbortReason(request.signal, cause)) {
277
+ return Response.json({ error: 'RequestAborted' }, { status: 499 });
278
+ }
279
+ const error = errors_js_1.LexServerError.from(cause);
263
280
  const { onHandlerError } = this.options;
264
- if (onHandlerError && !isAbortReason(request.signal, error)) {
281
+ if (onHandlerError)
265
282
  await onHandlerError({ error, request, method });
266
- }
267
- if (error instanceof lex_data_1.LexError) {
268
- return error.toResponse();
269
- }
270
- return Response.json({ error: 'InternalError', message: 'An internal error occurred' }, { status: 500 });
283
+ return error.toResponse();
271
284
  }
272
285
  /**
273
286
  * The main fetch handler for processing XRPC requests.
@@ -299,17 +312,47 @@ class LexRouter {
299
312
  * ```
300
313
  */
301
314
  fetch = async (request, connection) => {
302
- const nsid = extractMethodNsid(request);
303
- const fetch = nsid
304
- ? this.handlers.get(nsid)
305
- : undefined;
306
- if (fetch)
307
- return fetch(request, connection);
308
- if (!nsid || !(0, lex_schema_1.isNsidString)(nsid)) {
309
- return Response.json({
310
- error: 'InvalidRequest',
311
- message: 'Invalid XRPC method path',
312
- }, { status: 404 });
315
+ const { pathname } = new URL(request.url);
316
+ const atprotoProxy = request.headers.get('atproto-proxy');
317
+ if (!pathname.startsWith(XRPC_PATH_PREFIX)) {
318
+ // Handle non XRPC paths
319
+ const { fallback } = this.options;
320
+ if (fallback)
321
+ return fallback(request, connection);
322
+ return new Response('Not Found', { status: 404 });
323
+ }
324
+ if (pathname === XRPC_HEALTH_CHECK_PATH) {
325
+ if (request.method !== 'GET') {
326
+ return invalidRequestResponse('Method not allowed', 405);
327
+ }
328
+ if (atprotoProxy != null) {
329
+ return invalidRequestResponse('atproto-proxy header is not allowed on health check endpoint');
330
+ }
331
+ const { healthCheck } = this.options;
332
+ const data = healthCheck ? await healthCheck(request) : { status: 'ok' };
333
+ return Response.json(data);
334
+ }
335
+ const subPath = pathname.slice(XRPC_PATH_PREFIX.length);
336
+ if (!(0, lex_schema_1.isNsidString)(subPath)) {
337
+ return invalidRequestResponse('Invalid NSID in URL path');
338
+ }
339
+ const nsid = normalizeNsid(subPath);
340
+ if (atprotoProxy == null) {
341
+ const handler = this.handlers.get(nsid);
342
+ if (handler)
343
+ return handler(request, connection);
344
+ }
345
+ else {
346
+ // Handle service proxying logic.
347
+ const proxyInfo = parseAtprotoProxyHeader(atprotoProxy);
348
+ if (!proxyInfo) {
349
+ return invalidRequestResponse(`Invalid atproto-proxy header value: ${atprotoProxy}`);
350
+ }
351
+ // @TODO actually implement service proxying logic here. The reason it was
352
+ // not done already is because we want to perform all the heavy lifting
353
+ // here, while still allowing the possibility to override the endpoint
354
+ // resolution, etc.
355
+ // @NOTE see ./service-auth.ts for potential common code (did resolver, etc.)
313
356
  }
314
357
  return Response.json({
315
358
  error: 'MethodNotImplemented',
@@ -318,16 +361,6 @@ class LexRouter {
318
361
  };
319
362
  }
320
363
  exports.LexRouter = LexRouter;
321
- function extractMethodNsid(request) {
322
- const { pathname } = new URL(request.url);
323
- if (!pathname.startsWith('/xrpc/'))
324
- return null;
325
- if (pathname.includes('/', 6))
326
- return null;
327
- // We don't really need to validate the NSID here, the existence of the route
328
- // (which is looked up based on an NSID) is sufficient.
329
- return pathname.slice(6);
330
- }
331
364
  async function getProcedureInput(request) {
332
365
  const encodingRaw = request.headers
333
366
  .get('content-type')
@@ -341,7 +374,10 @@ async function getProcedureInput(request) {
341
374
  ? 'application/octet-stream'
342
375
  : undefined);
343
376
  if (!this.input.matchesEncoding(encoding)) {
344
- throw new lex_data_1.LexError('InvalidRequest', `Invalid content-type: ${encoding}`);
377
+ throw new errors_js_1.LexServerError(400, {
378
+ error: 'InvalidRequest',
379
+ message: `Invalid content-type: ${encoding}`,
380
+ });
345
381
  }
346
382
  if (this.input.encoding === 'application/json') {
347
383
  // @TODO limit size?
@@ -361,14 +397,27 @@ async function getQueryInput(request) {
361
397
  if (request.body ||
362
398
  request.headers.has('content-type') ||
363
399
  request.headers.has('content-length')) {
364
- throw new lex_data_1.LexError('InvalidRequest', 'GET requests must not have a body');
400
+ throw new errors_js_1.LexServerError(400, {
401
+ error: 'InvalidRequest',
402
+ message: 'GET requests must not have a body',
403
+ });
365
404
  }
366
405
  return undefined;
367
406
  }
407
+ function onMessage(_event) {
408
+ const error = 'InvalidRequest';
409
+ const message = 'XRPC subscriptions do not accept messages';
410
+ this.send(encodeErrorFrame({ error, message }));
411
+ // 1003 indicates that an endpoint is terminating the connection
412
+ // because it has received a type of data it cannot accept (e.g., an
413
+ // endpoint that understands only text data MAY send this if it
414
+ // receives a binary message).
415
+ this.close(1003, error);
416
+ }
368
417
  // Pre-encoded frame header for error frames
369
418
  const ERROR_FRAME_HEADER = /*#__PURE__*/ (0, lex_cbor_1.encode)({ op: -1 });
370
- function encodeErrorFrame(error) {
371
- return (0, lex_data_1.ui8Concat)([ERROR_FRAME_HEADER, (0, lex_cbor_1.encode)(error.toJSON())]);
419
+ function encodeErrorFrame(errorData) {
420
+ return (0, lex_data_1.ui8Concat)([ERROR_FRAME_HEADER, (0, lex_cbor_1.encode)(errorData)]);
372
421
  }
373
422
  // Pre-encoded frame header for message frames with unknown type
374
423
  const UNKNOWN_MESSAGE_FRAME_HEADER = /*#__PURE__*/ (0, lex_cbor_1.encode)({ op: 1 });
@@ -392,9 +441,56 @@ function encodeMessageFrame(method, value) {
392
441
  return (0, lex_data_1.ui8Concat)([UNKNOWN_MESSAGE_FRAME_HEADER, (0, lex_cbor_1.encode)(value)]);
393
442
  }
394
443
  function isAbortReason(signal, error) {
395
- if (!signal.aborted || signal.reason == null)
396
- return false;
397
- return (error === signal.reason ||
398
- (error instanceof Error && error.cause === signal.reason));
444
+ return (signal.aborted &&
445
+ signal.reason != null &&
446
+ error instanceof Error &&
447
+ (error === signal.reason || error.cause === signal.reason));
448
+ }
449
+ function parseAtprotoProxyHeader(value) {
450
+ // /!\ Hot path
451
+ // (fast) sanity check to avoid unnecessary parsing for non-DID values
452
+ if (!value.startsWith('did:'))
453
+ return null;
454
+ // The format is expected to be `did:example:service#serviceId`
455
+ const hashIndex = value.indexOf('#');
456
+ if (hashIndex === -1)
457
+ return null;
458
+ const fragmentIndex = hashIndex + 1;
459
+ // Basic validation if the fragment
460
+ if (fragmentIndex === value.length)
461
+ return null;
462
+ if (value.includes('#', fragmentIndex))
463
+ return null;
464
+ if (value.includes(' ', fragmentIndex))
465
+ return null;
466
+ const did = value.slice(0, hashIndex);
467
+ if (!(0, lex_schema_1.isDidString)(did))
468
+ return null;
469
+ const serviceId = value.slice(fragmentIndex);
470
+ return { did, serviceId };
471
+ }
472
+ function normalizeNsid(nsid) {
473
+ const lastDotIdx = nsid.lastIndexOf('.');
474
+ // The domain name part of the NSID is case-insensitive, but the last part is
475
+ // case-sensitive. Normalize the domain part to lowercase.
476
+ if (lastDotIdx !== -1 && hasUpperCase(nsid, 0, lastDotIdx)) {
477
+ return `${nsid.slice(0, lastDotIdx).toLowerCase()}.${nsid.slice(lastDotIdx + 1)}`;
478
+ }
479
+ return nsid;
480
+ }
481
+ function hasUpperCase(str, start = 0, end = str.length) {
482
+ for (let i = start; i < end; i++) {
483
+ const code = str.charCodeAt(i);
484
+ if (code >= 0x41 && code <= 0x5a) {
485
+ return true;
486
+ }
487
+ }
488
+ return false;
489
+ }
490
+ function invalidRequestResponse(message, status = 400, headers) {
491
+ return Response.json({
492
+ error: 'InvalidRequest',
493
+ message,
494
+ }, { status, headers });
399
495
  }
400
- //# sourceMappingURL=lex-server.js.map
496
+ //# sourceMappingURL=lex-router.js.map