@atproto/lex-server 0.0.8 → 0.0.10

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.
@@ -3,50 +3,192 @@ import { DidString } from '@atproto/lex-schema';
3
3
  import { CreateDidResolverOptions } from '@atproto-labs/did-resolver';
4
4
  import { LexRouterAuth } from './lex-server.js';
5
5
  /**
6
- * A function to check and record nonce uniqueness.
6
+ * Callback function to check and record nonce uniqueness.
7
+ *
8
+ * Used to prevent replay attacks by ensuring each nonce is only used once.
9
+ * The implementation must track nonces for at least the `maxAge` duration
10
+ * (default 5 minutes before and after the current time).
11
+ *
12
+ * @param nonce - The nonce string from the JWT token
13
+ * @returns Promise resolving to `true` if the nonce is unique (first time seen),
14
+ * `false` if it has been seen before
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Using Redis for nonce tracking
19
+ * const checkNonce: UniqueNonceChecker = async (nonce) => {
20
+ * const key = `nonce:${nonce}`
21
+ * const result = await redis.setnx(key, '1')
22
+ * if (result === 1) {
23
+ * await redis.expire(key, 600) // 10 minutes TTL
24
+ * return true
25
+ * }
26
+ * return false
27
+ * }
28
+ * ```
7
29
  */
8
30
  export type UniqueNonceChecker = (nonce: string) => Promise<boolean>;
31
+ /**
32
+ * Configuration options for AT Protocol service authentication.
33
+ *
34
+ * Service auth is used for server-to-server communication in the AT Protocol,
35
+ * where one service authenticates to another using signed JWT tokens tied to
36
+ * the caller's DID.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const options: ServiceAuthOptions = {
41
+ * audience: 'did:web:api.example.com',
42
+ * unique: async (nonce) => nonceStore.checkAndAdd(nonce),
43
+ * maxAge: 300, // 5 minutes
44
+ * // Optional DID resolver options
45
+ * plcDirectoryUrl: 'https://plc.directory'
46
+ * }
47
+ * ```
48
+ */
9
49
  export type ServiceAuthOptions = CreateDidResolverOptions & {
10
50
  /**
11
- * Expected audience ("aud") claim in the JWT token. Set to `null` to skip
12
- * audience verification (not recommended).
51
+ * Expected audience ("aud") claim in the JWT token.
52
+ *
53
+ * This should be the DID of your service. The token must include this
54
+ * value in its `aud` claim to be accepted. Set to `null` to skip
55
+ * audience verification (not recommended for production).
13
56
  */
14
57
  audience: null | DidString;
15
58
  /**
16
- * Function to check and record nonce uniqueness. The value checked here must
17
- * be unique within {@link ServiceAuthOptions.maxAge} seconds before and after
18
- * the current time.
59
+ * Function to check and record nonce uniqueness.
19
60
  *
20
- * @param nonce - The nonce to check.
61
+ * This is critical for preventing replay attacks. The value checked here
62
+ * must be unique within `maxAge` seconds before and after the current time.
63
+ *
64
+ * @param nonce - The nonce to check
65
+ * @returns Promise resolving to `true` if unique, `false` if seen before
21
66
  */
22
67
  unique: UniqueNonceChecker;
23
68
  /**
24
69
  * Maximum age of the JWT token in seconds.
25
70
  *
71
+ * Tokens with `iat` (issued at) or `exp` (expiry) timestamps outside
72
+ * this window from the current time will be rejected.
73
+ *
26
74
  * @default 300 (5 minutes)
27
75
  */
28
76
  maxAge?: number;
29
77
  };
78
+ /**
79
+ * Credentials returned after successful service authentication.
80
+ *
81
+ * Contains the verified DID, resolved DID document, and parsed JWT token.
82
+ * These are available in handler context as `ctx.credentials`.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * router.add(protectedMethod, {
87
+ * handler: async (ctx) => {
88
+ * const { did, didDocument, jwt } = ctx.credentials
89
+ * console.log('Request from:', did)
90
+ * console.log('Token expires:', new Date(jwt.payload.exp * 1000))
91
+ * return { body: { callerDid: did } }
92
+ * },
93
+ * auth: serviceAuth({ audience: myDid, unique: checkNonce })
94
+ * })
95
+ * ```
96
+ */
30
97
  export type ServiceAuthCredentials = {
98
+ /** The verified AT Protocol DID of the caller. */
31
99
  did: AtprotoDid;
100
+ /** The resolved DID document of the caller. */
32
101
  didDocument: AtprotoDidDocument;
102
+ /** The parsed and validated JWT token. */
33
103
  jwt: ParsedJwt;
34
104
  };
35
105
  /**
36
- * Creates an authentication handler for LexRouter that verifies AT protocol
37
- * "service auth" JWT bearer tokens signed by decentralized identifiers (DIDs).
106
+ * Creates an authentication handler for verifying AT Protocol service auth JWTs.
107
+ *
108
+ * Service auth is the standard authentication mechanism for server-to-server
109
+ * communication in the AT Protocol. It uses JWT bearer tokens signed by the
110
+ * caller's DID signing key, with the signature verified against the public
111
+ * key in the caller's DID document.
112
+ *
113
+ * The handler performs the following validations:
114
+ * - Extracts and parses the Bearer token from the Authorization header
115
+ * - Validates JWT structure and claims (aud, exp, iat, lxm, nonce)
116
+ * - Resolves the issuer's DID document
117
+ * - Verifies the JWT signature against the `#atproto` verification method
118
+ * - Checks nonce uniqueness to prevent replay attacks
119
+ *
120
+ * @param options - Configuration options for service auth
121
+ * @returns An auth handler function for use with {@link LexRouter.add}
122
+ *
123
+ * @example Basic usage
124
+ * ```typescript
125
+ * import { LexRouter, serviceAuth } from '@atproto/lex-server'
126
+ *
127
+ * const router = new LexRouter()
128
+ *
129
+ * const auth = serviceAuth({
130
+ * audience: 'did:web:api.example.com',
131
+ * unique: async (nonce) => {
132
+ * // Check if nonce has been seen, return true if unique
133
+ * const isNew = await redis.setnx(`nonce:${nonce}`, '1')
134
+ * if (isNew) await redis.expire(`nonce:${nonce}`, 600)
135
+ * return isNew
136
+ * }
137
+ * })
138
+ *
139
+ * router.add(myMethod, {
140
+ * handler: async (ctx) => {
141
+ * console.log('Authenticated as:', ctx.credentials.did)
142
+ * return { body: { success: true } }
143
+ * },
144
+ * auth
145
+ * })
146
+ * ```
38
147
  */
39
148
  export declare function serviceAuth({ audience, maxAge, unique, ...options }: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials>;
149
+ /**
150
+ * Options for parsing and validating a JWT token.
151
+ */
40
152
  export type ParseJwtOptions = {
153
+ /** Maximum age in seconds for token validity window. */
41
154
  maxAge: number;
155
+ /** Expected audience claim, or null to skip audience verification. */
42
156
  audience: null | DidString;
157
+ /** Function to check nonce uniqueness. */
43
158
  unique: UniqueNonceChecker;
159
+ /** Expected lexicon method NSID for the `lxm` claim. */
44
160
  lxm: string;
45
161
  };
162
+ /**
163
+ * A parsed and partially validated JWT token.
164
+ *
165
+ * Contains the decoded header and payload, along with the raw bytes
166
+ * needed for signature verification.
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * const jwt: ParsedJwt = {
171
+ * header: { alg: 'ES256K', typ: 'JWT' },
172
+ * payload: {
173
+ * iss: 'did:plc:abc123',
174
+ * aud: 'did:web:api.example.com',
175
+ * exp: 1704067200,
176
+ * iat: 1704066900,
177
+ * lxm: 'com.atproto.sync.getBlob'
178
+ * },
179
+ * message: new Uint8Array([...]),
180
+ * signature: new Uint8Array([...])
181
+ * }
182
+ * ```
183
+ */
46
184
  export type ParsedJwt = {
185
+ /** The decoded JWT header containing algorithm and type. */
47
186
  header: HeaderObject;
187
+ /** The decoded JWT payload containing claims. */
48
188
  payload: PayloadObject;
189
+ /** The raw header.payload bytes for signature verification. */
49
190
  message: Uint8Array;
191
+ /** The decoded signature bytes. */
50
192
  signature: Uint8Array;
51
193
  };
52
194
  type HeaderObject = {
@@ -1 +1 @@
1
- {"version":3,"file":"service-auth.d.ts","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,kBAAkB,EAGnB,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,SAAS,EAAe,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EACL,wBAAwB,EAEzB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAI/C;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEpE,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG;IAC1D;;;OAGG;IACH,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAA;IAC1B;;;;;;OAMG;IACH,MAAM,EAAE,kBAAkB,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,GAAG,EAAE,UAAU,CAAA;IACf,WAAW,EAAE,kBAAkB,CAAA;IAC/B,GAAG,EAAE,SAAS,CAAA;CACf,CAAA;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,MAAe,EACf,MAAM,EACN,GAAG,OAAO,EACX,EAAE,kBAAkB,GAAG,aAAa,CAAC,sBAAsB,CAAC,CAyD5D;AA2ED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,EAAE,UAAU,CAAA;IACnB,SAAS,EAAE,UAAU,CAAA;CACtB,CAAA;AA4HD,KAAK,YAAY,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AASjD,KAAK,aAAa,GAAG;IACnB,GAAG,EAAE,SAAS,CAAA;IACd,GAAG,EAAE,SAAS,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AACD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,aAAa,CAalE"}
1
+ {"version":3,"file":"service-auth.d.ts","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,kBAAkB,EAGnB,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,SAAS,EAAe,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EACL,wBAAwB,EAEzB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEpE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG;IAC1D;;;;;;OAMG;IACH,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAA;IAC1B;;;;;;;;OAQG;IACH,MAAM,EAAE,kBAAkB,CAAA;IAC1B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,kDAAkD;IAClD,GAAG,EAAE,UAAU,CAAA;IACf,+CAA+C;IAC/C,WAAW,EAAE,kBAAkB,CAAA;IAC/B,0CAA0C;IAC1C,GAAG,EAAE,SAAS,CAAA;CACf,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,MAAe,EACf,MAAM,EACN,GAAG,OAAO,EACX,EAAE,kBAAkB,GAAG,aAAa,CAAC,sBAAsB,CAAC,CAyD5D;AA2ED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAA;IAC1B,0CAA0C;IAC1C,MAAM,EAAE,kBAAkB,CAAA;IAC1B,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,4DAA4D;IAC5D,MAAM,EAAE,YAAY,CAAA;IACpB,iDAAiD;IACjD,OAAO,EAAE,aAAa,CAAA;IACtB,+DAA+D;IAC/D,OAAO,EAAE,UAAU,CAAA;IACnB,mCAAmC;IACnC,SAAS,EAAE,UAAU,CAAA;CACtB,CAAA;AA4HD,KAAK,YAAY,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AASjD,KAAK,aAAa,GAAG;IACnB,GAAG,EAAE,SAAS,CAAA;IACd,GAAG,EAAE,SAAS,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AACD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,aAAa,CAalE"}
@@ -11,8 +11,47 @@ const did_resolver_1 = require("@atproto-labs/did-resolver");
11
11
  const errors_js_1 = require("./errors.js");
12
12
  const BEARER_PREFIX = 'Bearer ';
13
13
  /**
14
- * Creates an authentication handler for LexRouter that verifies AT protocol
15
- * "service auth" JWT bearer tokens signed by decentralized identifiers (DIDs).
14
+ * Creates an authentication handler for verifying AT Protocol service auth JWTs.
15
+ *
16
+ * Service auth is the standard authentication mechanism for server-to-server
17
+ * communication in the AT Protocol. It uses JWT bearer tokens signed by the
18
+ * caller's DID signing key, with the signature verified against the public
19
+ * key in the caller's DID document.
20
+ *
21
+ * The handler performs the following validations:
22
+ * - Extracts and parses the Bearer token from the Authorization header
23
+ * - Validates JWT structure and claims (aud, exp, iat, lxm, nonce)
24
+ * - Resolves the issuer's DID document
25
+ * - Verifies the JWT signature against the `#atproto` verification method
26
+ * - Checks nonce uniqueness to prevent replay attacks
27
+ *
28
+ * @param options - Configuration options for service auth
29
+ * @returns An auth handler function for use with {@link LexRouter.add}
30
+ *
31
+ * @example Basic usage
32
+ * ```typescript
33
+ * import { LexRouter, serviceAuth } from '@atproto/lex-server'
34
+ *
35
+ * const router = new LexRouter()
36
+ *
37
+ * const auth = serviceAuth({
38
+ * audience: 'did:web:api.example.com',
39
+ * unique: async (nonce) => {
40
+ * // Check if nonce has been seen, return true if unique
41
+ * const isNew = await redis.setnx(`nonce:${nonce}`, '1')
42
+ * if (isNew) await redis.expire(`nonce:${nonce}`, 600)
43
+ * return isNew
44
+ * }
45
+ * })
46
+ *
47
+ * router.add(myMethod, {
48
+ * handler: async (ctx) => {
49
+ * console.log('Authenticated as:', ctx.credentials.did)
50
+ * return { body: { success: true } }
51
+ * },
52
+ * auth
53
+ * })
54
+ * ```
16
55
  */
17
56
  function serviceAuth({ audience, maxAge = 5 * 60, unique, ...options }) {
18
57
  const didResolver = (0, did_resolver_1.createDidResolver)(options);
@@ -1 +1 @@
1
- {"version":3,"file":"service-auth.js","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from '@atproto/crypto'\nimport {\n AtprotoDid,\n AtprotoDidDocument,\n Did,\n matchesIdentifier,\n} from '@atproto/did'\nimport { fromBase64, isPlainObject, utf8FromBase64 } from '@atproto/lex-data'\nimport { DidString, isDidString } from '@atproto/lex-schema'\nimport {\n CreateDidResolverOptions,\n createDidResolver,\n} from '@atproto-labs/did-resolver'\nimport { LexServerAuthError } from './errors.js'\nimport { LexRouterAuth } from './lex-server.js'\n\nconst BEARER_PREFIX = 'Bearer '\n\n/**\n * A function to check and record nonce uniqueness.\n */\nexport type UniqueNonceChecker = (nonce: string) => Promise<boolean>\n\nexport type ServiceAuthOptions = CreateDidResolverOptions & {\n /**\n * Expected audience (\"aud\") claim in the JWT token. Set to `null` to skip\n * audience verification (not recommended).\n */\n audience: null | DidString\n /**\n * Function to check and record nonce uniqueness. The value checked here must\n * be unique within {@link ServiceAuthOptions.maxAge} seconds before and after\n * the current time.\n *\n * @param nonce - The nonce to check.\n */\n unique: UniqueNonceChecker\n /**\n * Maximum age of the JWT token in seconds.\n *\n * @default 300 (5 minutes)\n */\n maxAge?: number\n}\n\nexport type ServiceAuthCredentials = {\n did: AtprotoDid\n didDocument: AtprotoDidDocument\n jwt: ParsedJwt\n}\n\n/**\n * Creates an authentication handler for LexRouter that verifies AT protocol\n * \"service auth\" JWT bearer tokens signed by decentralized identifiers (DIDs).\n */\nexport function serviceAuth({\n audience,\n maxAge = 5 * 60,\n unique,\n ...options\n}: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials> {\n const didResolver = createDidResolver(options)\n\n return async ({ request, method }) => {\n const { signal } = request\n const jwt = await parseJwtBearer(request, {\n lxm: method.nsid,\n maxAge,\n audience,\n unique,\n })\n\n let didDocument: AtprotoDidDocument = await didResolver\n .resolve(jwt.payload.iss, { signal })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n const key = getAtprotoSigningKey(didDocument)\n\n if (!key || !(await verifyJwt(jwt, key))) {\n signal.throwIfAborted()\n\n // Try refreshing the DID document in case it was updated\n didDocument = await didResolver\n .resolve(jwt.payload.iss, { signal, noCache: true })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n // Verify again with the fresh key (if it changed)\n const keyFresh = getAtprotoSigningKey(didDocument)\n if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n )\n }\n }\n\n return {\n did: didDocument.id,\n didDocument,\n jwt,\n }\n }\n}\n\nasync function verifyJwt(jwt: ParsedJwt, key: Did<'key'>) {\n try {\n return await crypto.verifySignature(key, jwt.message, jwt.signature, {\n jwtAlg: jwt.header.alg,\n allowMalleableSig: true,\n })\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not verify JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n { cause },\n )\n }\n}\n\nfunction getAtprotoSigningKey(\n didDocument: AtprotoDidDocument,\n): null | Did<'key'> {\n try {\n const key = didDocument.verificationMethod?.find(\n isAtprotoVerificationMethod,\n didDocument,\n )\n\n if (key?.publicKeyMultibase) {\n if (key.type === 'EcdsaSecp256r1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n } else if (key.type === 'Multikey') {\n const parsed = crypto.parseMultikey(key.publicKeyMultibase)\n return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)\n }\n }\n } catch {\n // Invalid key, ignore\n }\n\n return null\n}\n\nfunction isAtprotoVerificationMethod<\n V extends string | { id: string; type: string; publicKeyMultibase?: string },\n>(\n this: AtprotoDidDocument,\n vm: V,\n): vm is Exclude<V, string> & {\n id: `${string}#atproto`\n} {\n return typeof vm === 'object' && matchesIdentifier(this.id, 'atproto', vm.id)\n}\n\nasync function parseJwtBearer(\n request: Request,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const authorization = request.headers.get('authorization')\n if (!authorization?.startsWith(BEARER_PREFIX)) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Bearer token required',\n { Bearer: { error: 'MissingBearer' } },\n )\n }\n\n const token = authorization.slice(BEARER_PREFIX.length).trim()\n\n return parseJwt(token, options)\n}\n\nexport type ParseJwtOptions = {\n maxAge: number\n audience: null | DidString\n unique: UniqueNonceChecker\n lxm: string\n}\n\nexport type ParsedJwt = {\n header: HeaderObject\n payload: PayloadObject\n message: Uint8Array\n signature: Uint8Array\n}\n\nasync function parseJwt(\n token: string,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const {\n length,\n 0: headerB64,\n 1: payloadB64,\n 2: signatureB64,\n } = token.split('.')\n if (length !== 3) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let header: HeaderObject\n try {\n header = jsonFromBase64(headerB64, isHeaderObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (\n header.alg === 'none' ||\n // service tokens are not OAuth 2.0 access tokens\n // https://datatracker.ietf.org/doc/html/rfc9068\n header.typ === 'at+jwt' ||\n // \"refresh+jwt\" is a non-standard type used by the @atproto packages\n header.typ === 'refresh+jwt' ||\n // \"DPoP\" proofs are not meant to be used as service tokens\n // https://datatracker.ietf.org/doc/html/rfc9449\n header.typ === 'dpop+jwt'\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let payload: PayloadObject\n try {\n payload = jsonFromBase64(payloadB64, isPayloadObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (options.audience !== null && options.audience !== payload.aud) {\n throw new LexServerAuthError('AuthenticationRequired', 'Invalid audience', {\n Bearer: { error: 'InvalidAudience' },\n })\n }\n\n const now = Math.floor(Date.now() / 1000)\n\n if (payload.nbf != null && now < payload.nbf) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token not yet valid',\n { Bearer: { error: 'JwtNotYetValid' } },\n )\n }\n\n if (now > payload.exp) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token expired',\n { Bearer: { error: 'JwtExpired' } },\n )\n }\n\n // Prevent issuer from generating very long-lived tokens\n if (\n timeDiff(now, payload.exp) > options.maxAge ||\n timeDiff(now, payload.iat) > options.maxAge\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token too old',\n { Bearer: { error: 'JwtTooOld' } },\n )\n }\n\n if (payload.lxm != null && typeof payload.lxm !== options.lxm) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT lexicon method (\"lxm\")',\n { Bearer: { error: 'BadJwtLexiconMethod' } },\n )\n }\n\n if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Replay attack detected: nonce is not unique',\n { Bearer: { error: 'NonceNotUnique' } },\n )\n }\n\n return {\n header,\n payload,\n message: textEncoder.encode(`${headerB64}.${payloadB64}`),\n signature: fromBase64(signatureB64, 'base64url'),\n }\n}\n\nconst textEncoder = /*#__PURE__*/ new TextEncoder()\n\ntype HeaderObject = { alg: string; typ?: string }\nfunction isHeaderObject(obj: unknown): obj is HeaderObject {\n return (\n isPlainObject(obj) &&\n typeof obj.alg === 'string' &&\n (obj.typ === undefined || typeof obj.typ === 'string')\n )\n}\n\ntype PayloadObject = {\n iss: DidString\n aud: DidString\n exp: number\n iat?: number\n nbf?: number\n lxm?: string\n nonce?: string\n}\nexport function isPayloadObject(obj: unknown): obj is PayloadObject {\n return (\n isPlainObject(obj) &&\n typeof obj.iss === 'string' &&\n typeof obj.aud === 'string' &&\n (obj.lxm === undefined || typeof obj.lxm === 'string') &&\n (obj.nonce === undefined || typeof obj.nonce === 'string') &&\n (obj.iat === undefined || isPositiveInt(obj.iat)) &&\n (obj.nbf === undefined || isPositiveInt(obj.nbf)) &&\n isPositiveInt(obj.exp) &&\n isDidString(obj.iss) &&\n isDidString(obj.aud)\n )\n}\n\nfunction timeDiff(t1: number, t2?: number): number {\n if (t2 === undefined) return 0\n return Math.abs(t1 - t2)\n}\n\nfunction isPositiveInt(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value > 0\n}\n\nfunction jsonFromBase64<T>(b64: string, isType: (obj: unknown) => obj is T): T {\n const obj = JSON.parse(utf8FromBase64(b64, 'base64url'))\n if (isType(obj)) return obj\n throw new Error('Invalid type')\n}\n"]}
1
+ {"version":3,"file":"service-auth.js","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from '@atproto/crypto'\nimport {\n AtprotoDid,\n AtprotoDidDocument,\n Did,\n matchesIdentifier,\n} from '@atproto/did'\nimport { fromBase64, isPlainObject, utf8FromBase64 } from '@atproto/lex-data'\nimport { DidString, isDidString } from '@atproto/lex-schema'\nimport {\n CreateDidResolverOptions,\n createDidResolver,\n} from '@atproto-labs/did-resolver'\nimport { LexServerAuthError } from './errors.js'\nimport { LexRouterAuth } from './lex-server.js'\n\nconst BEARER_PREFIX = 'Bearer '\n\n/**\n * Callback function to check and record nonce uniqueness.\n *\n * Used to prevent replay attacks by ensuring each nonce is only used once.\n * The implementation must track nonces for at least the `maxAge` duration\n * (default 5 minutes before and after the current time).\n *\n * @param nonce - The nonce string from the JWT token\n * @returns Promise resolving to `true` if the nonce is unique (first time seen),\n * `false` if it has been seen before\n *\n * @example\n * ```typescript\n * // Using Redis for nonce tracking\n * const checkNonce: UniqueNonceChecker = async (nonce) => {\n * const key = `nonce:${nonce}`\n * const result = await redis.setnx(key, '1')\n * if (result === 1) {\n * await redis.expire(key, 600) // 10 minutes TTL\n * return true\n * }\n * return false\n * }\n * ```\n */\nexport type UniqueNonceChecker = (nonce: string) => Promise<boolean>\n\n/**\n * Configuration options for AT Protocol service authentication.\n *\n * Service auth is used for server-to-server communication in the AT Protocol,\n * where one service authenticates to another using signed JWT tokens tied to\n * the caller's DID.\n *\n * @example\n * ```typescript\n * const options: ServiceAuthOptions = {\n * audience: 'did:web:api.example.com',\n * unique: async (nonce) => nonceStore.checkAndAdd(nonce),\n * maxAge: 300, // 5 minutes\n * // Optional DID resolver options\n * plcDirectoryUrl: 'https://plc.directory'\n * }\n * ```\n */\nexport type ServiceAuthOptions = CreateDidResolverOptions & {\n /**\n * Expected audience (\"aud\") claim in the JWT token.\n *\n * This should be the DID of your service. The token must include this\n * value in its `aud` claim to be accepted. Set to `null` to skip\n * audience verification (not recommended for production).\n */\n audience: null | DidString\n /**\n * Function to check and record nonce uniqueness.\n *\n * This is critical for preventing replay attacks. The value checked here\n * must be unique within `maxAge` seconds before and after the current time.\n *\n * @param nonce - The nonce to check\n * @returns Promise resolving to `true` if unique, `false` if seen before\n */\n unique: UniqueNonceChecker\n /**\n * Maximum age of the JWT token in seconds.\n *\n * Tokens with `iat` (issued at) or `exp` (expiry) timestamps outside\n * this window from the current time will be rejected.\n *\n * @default 300 (5 minutes)\n */\n maxAge?: number\n}\n\n/**\n * Credentials returned after successful service authentication.\n *\n * Contains the verified DID, resolved DID document, and parsed JWT token.\n * These are available in handler context as `ctx.credentials`.\n *\n * @example\n * ```typescript\n * router.add(protectedMethod, {\n * handler: async (ctx) => {\n * const { did, didDocument, jwt } = ctx.credentials\n * console.log('Request from:', did)\n * console.log('Token expires:', new Date(jwt.payload.exp * 1000))\n * return { body: { callerDid: did } }\n * },\n * auth: serviceAuth({ audience: myDid, unique: checkNonce })\n * })\n * ```\n */\nexport type ServiceAuthCredentials = {\n /** The verified AT Protocol DID of the caller. */\n did: AtprotoDid\n /** The resolved DID document of the caller. */\n didDocument: AtprotoDidDocument\n /** The parsed and validated JWT token. */\n jwt: ParsedJwt\n}\n\n/**\n * Creates an authentication handler for verifying AT Protocol service auth JWTs.\n *\n * Service auth is the standard authentication mechanism for server-to-server\n * communication in the AT Protocol. It uses JWT bearer tokens signed by the\n * caller's DID signing key, with the signature verified against the public\n * key in the caller's DID document.\n *\n * The handler performs the following validations:\n * - Extracts and parses the Bearer token from the Authorization header\n * - Validates JWT structure and claims (aud, exp, iat, lxm, nonce)\n * - Resolves the issuer's DID document\n * - Verifies the JWT signature against the `#atproto` verification method\n * - Checks nonce uniqueness to prevent replay attacks\n *\n * @param options - Configuration options for service auth\n * @returns An auth handler function for use with {@link LexRouter.add}\n *\n * @example Basic usage\n * ```typescript\n * import { LexRouter, serviceAuth } from '@atproto/lex-server'\n *\n * const router = new LexRouter()\n *\n * const auth = serviceAuth({\n * audience: 'did:web:api.example.com',\n * unique: async (nonce) => {\n * // Check if nonce has been seen, return true if unique\n * const isNew = await redis.setnx(`nonce:${nonce}`, '1')\n * if (isNew) await redis.expire(`nonce:${nonce}`, 600)\n * return isNew\n * }\n * })\n *\n * router.add(myMethod, {\n * handler: async (ctx) => {\n * console.log('Authenticated as:', ctx.credentials.did)\n * return { body: { success: true } }\n * },\n * auth\n * })\n * ```\n */\nexport function serviceAuth({\n audience,\n maxAge = 5 * 60,\n unique,\n ...options\n}: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials> {\n const didResolver = createDidResolver(options)\n\n return async ({ request, method }) => {\n const { signal } = request\n const jwt = await parseJwtBearer(request, {\n lxm: method.nsid,\n maxAge,\n audience,\n unique,\n })\n\n let didDocument: AtprotoDidDocument = await didResolver\n .resolve(jwt.payload.iss, { signal })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n const key = getAtprotoSigningKey(didDocument)\n\n if (!key || !(await verifyJwt(jwt, key))) {\n signal.throwIfAborted()\n\n // Try refreshing the DID document in case it was updated\n didDocument = await didResolver\n .resolve(jwt.payload.iss, { signal, noCache: true })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n // Verify again with the fresh key (if it changed)\n const keyFresh = getAtprotoSigningKey(didDocument)\n if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n )\n }\n }\n\n return {\n did: didDocument.id,\n didDocument,\n jwt,\n }\n }\n}\n\nasync function verifyJwt(jwt: ParsedJwt, key: Did<'key'>) {\n try {\n return await crypto.verifySignature(key, jwt.message, jwt.signature, {\n jwtAlg: jwt.header.alg,\n allowMalleableSig: true,\n })\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not verify JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n { cause },\n )\n }\n}\n\nfunction getAtprotoSigningKey(\n didDocument: AtprotoDidDocument,\n): null | Did<'key'> {\n try {\n const key = didDocument.verificationMethod?.find(\n isAtprotoVerificationMethod,\n didDocument,\n )\n\n if (key?.publicKeyMultibase) {\n if (key.type === 'EcdsaSecp256r1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n } else if (key.type === 'Multikey') {\n const parsed = crypto.parseMultikey(key.publicKeyMultibase)\n return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)\n }\n }\n } catch {\n // Invalid key, ignore\n }\n\n return null\n}\n\nfunction isAtprotoVerificationMethod<\n V extends string | { id: string; type: string; publicKeyMultibase?: string },\n>(\n this: AtprotoDidDocument,\n vm: V,\n): vm is Exclude<V, string> & {\n id: `${string}#atproto`\n} {\n return typeof vm === 'object' && matchesIdentifier(this.id, 'atproto', vm.id)\n}\n\nasync function parseJwtBearer(\n request: Request,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const authorization = request.headers.get('authorization')\n if (!authorization?.startsWith(BEARER_PREFIX)) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Bearer token required',\n { Bearer: { error: 'MissingBearer' } },\n )\n }\n\n const token = authorization.slice(BEARER_PREFIX.length).trim()\n\n return parseJwt(token, options)\n}\n\n/**\n * Options for parsing and validating a JWT token.\n */\nexport type ParseJwtOptions = {\n /** Maximum age in seconds for token validity window. */\n maxAge: number\n /** Expected audience claim, or null to skip audience verification. */\n audience: null | DidString\n /** Function to check nonce uniqueness. */\n unique: UniqueNonceChecker\n /** Expected lexicon method NSID for the `lxm` claim. */\n lxm: string\n}\n\n/**\n * A parsed and partially validated JWT token.\n *\n * Contains the decoded header and payload, along with the raw bytes\n * needed for signature verification.\n *\n * @example\n * ```typescript\n * const jwt: ParsedJwt = {\n * header: { alg: 'ES256K', typ: 'JWT' },\n * payload: {\n * iss: 'did:plc:abc123',\n * aud: 'did:web:api.example.com',\n * exp: 1704067200,\n * iat: 1704066900,\n * lxm: 'com.atproto.sync.getBlob'\n * },\n * message: new Uint8Array([...]),\n * signature: new Uint8Array([...])\n * }\n * ```\n */\nexport type ParsedJwt = {\n /** The decoded JWT header containing algorithm and type. */\n header: HeaderObject\n /** The decoded JWT payload containing claims. */\n payload: PayloadObject\n /** The raw header.payload bytes for signature verification. */\n message: Uint8Array\n /** The decoded signature bytes. */\n signature: Uint8Array\n}\n\nasync function parseJwt(\n token: string,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const {\n length,\n 0: headerB64,\n 1: payloadB64,\n 2: signatureB64,\n } = token.split('.')\n if (length !== 3) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let header: HeaderObject\n try {\n header = jsonFromBase64(headerB64, isHeaderObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (\n header.alg === 'none' ||\n // service tokens are not OAuth 2.0 access tokens\n // https://datatracker.ietf.org/doc/html/rfc9068\n header.typ === 'at+jwt' ||\n // \"refresh+jwt\" is a non-standard type used by the @atproto packages\n header.typ === 'refresh+jwt' ||\n // \"DPoP\" proofs are not meant to be used as service tokens\n // https://datatracker.ietf.org/doc/html/rfc9449\n header.typ === 'dpop+jwt'\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let payload: PayloadObject\n try {\n payload = jsonFromBase64(payloadB64, isPayloadObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (options.audience !== null && options.audience !== payload.aud) {\n throw new LexServerAuthError('AuthenticationRequired', 'Invalid audience', {\n Bearer: { error: 'InvalidAudience' },\n })\n }\n\n const now = Math.floor(Date.now() / 1000)\n\n if (payload.nbf != null && now < payload.nbf) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token not yet valid',\n { Bearer: { error: 'JwtNotYetValid' } },\n )\n }\n\n if (now > payload.exp) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token expired',\n { Bearer: { error: 'JwtExpired' } },\n )\n }\n\n // Prevent issuer from generating very long-lived tokens\n if (\n timeDiff(now, payload.exp) > options.maxAge ||\n timeDiff(now, payload.iat) > options.maxAge\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token too old',\n { Bearer: { error: 'JwtTooOld' } },\n )\n }\n\n if (payload.lxm != null && typeof payload.lxm !== options.lxm) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT lexicon method (\"lxm\")',\n { Bearer: { error: 'BadJwtLexiconMethod' } },\n )\n }\n\n if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Replay attack detected: nonce is not unique',\n { Bearer: { error: 'NonceNotUnique' } },\n )\n }\n\n return {\n header,\n payload,\n message: textEncoder.encode(`${headerB64}.${payloadB64}`),\n signature: fromBase64(signatureB64, 'base64url'),\n }\n}\n\nconst textEncoder = /*#__PURE__*/ new TextEncoder()\n\ntype HeaderObject = { alg: string; typ?: string }\nfunction isHeaderObject(obj: unknown): obj is HeaderObject {\n return (\n isPlainObject(obj) &&\n typeof obj.alg === 'string' &&\n (obj.typ === undefined || typeof obj.typ === 'string')\n )\n}\n\ntype PayloadObject = {\n iss: DidString\n aud: DidString\n exp: number\n iat?: number\n nbf?: number\n lxm?: string\n nonce?: string\n}\nexport function isPayloadObject(obj: unknown): obj is PayloadObject {\n return (\n isPlainObject(obj) &&\n typeof obj.iss === 'string' &&\n typeof obj.aud === 'string' &&\n (obj.lxm === undefined || typeof obj.lxm === 'string') &&\n (obj.nonce === undefined || typeof obj.nonce === 'string') &&\n (obj.iat === undefined || isPositiveInt(obj.iat)) &&\n (obj.nbf === undefined || isPositiveInt(obj.nbf)) &&\n isPositiveInt(obj.exp) &&\n isDidString(obj.iss) &&\n isDidString(obj.aud)\n )\n}\n\nfunction timeDiff(t1: number, t2?: number): number {\n if (t2 === undefined) return 0\n return Math.abs(t1 - t2)\n}\n\nfunction isPositiveInt(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value > 0\n}\n\nfunction jsonFromBase64<T>(b64: string, isType: (obj: unknown) => obj is T): T {\n const obj = JSON.parse(utf8FromBase64(b64, 'base64url'))\n if (isType(obj)) return obj\n throw new Error('Invalid type')\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex-server",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "license": "MIT",
5
5
  "description": "Request router for Atproto Lexicon protocols and schemas",
6
6
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "browser": "./dist/index.js",
35
35
  "import": "./dist/index.js",
36
- "require": "./dist/index.js"
36
+ "default": "./dist/index.js"
37
37
  },
38
38
  "./nodejs": {
39
39
  "types": "./dist/nodejs.d.ts",
@@ -48,17 +48,17 @@
48
48
  "@atproto-labs/did-resolver": "^0.2.6",
49
49
  "@atproto/crypto": "^0.4.5",
50
50
  "@atproto/did": "^0.3.0",
51
- "@atproto/lex-cbor": "^0.0.10",
52
- "@atproto/lex-data": "^0.0.10",
53
- "@atproto/lex-json": "^0.0.10",
54
- "@atproto/lex-schema": "^0.0.11"
51
+ "@atproto/lex-cbor": "^0.0.12",
52
+ "@atproto/lex-data": "^0.0.12",
53
+ "@atproto/lex-json": "^0.0.12",
54
+ "@atproto/lex-schema": "^0.0.13"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/ws": "^8.18.1",
58
58
  "@vitest/coverage-v8": "4.0.16",
59
59
  "vitest": "^4.0.16",
60
- "@atproto/lex": "^0.0.14",
61
- "@atproto/lex-client": "^0.0.11"
60
+ "@atproto/lex": "^0.0.17",
61
+ "@atproto/lex-client": "^0.0.13"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "tsc --build tsconfig.build.json",
package/src/errors.ts CHANGED
@@ -6,11 +6,54 @@ import {
6
6
 
7
7
  export type { WWWAuthenticate }
8
8
 
9
+ /**
10
+ * Error class for authentication failures in XRPC server handlers.
11
+ *
12
+ * Extends {@link LexError} to include WWW-Authenticate header support,
13
+ * which is required by HTTP authentication standards (RFC 7235).
14
+ * The error automatically generates the appropriate 401 response with
15
+ * the WWW-Authenticate header when converted to a Response.
16
+ *
17
+ * @typeParam N - The Lexicon error code type
18
+ *
19
+ * @example Throwing an auth error
20
+ * ```typescript
21
+ * import { LexServerAuthError } from '@atproto/lex-server'
22
+ *
23
+ * throw new LexServerAuthError(
24
+ * 'AuthenticationRequired',
25
+ * 'Invalid or expired token',
26
+ * { Bearer: { error: 'InvalidToken', realm: 'api.example.com' } }
27
+ * )
28
+ * ```
29
+ *
30
+ * @example Converting from a LexError
31
+ * ```typescript
32
+ * try {
33
+ * await validateToken(token)
34
+ * } catch (error) {
35
+ * if (error instanceof LexError) {
36
+ * throw LexServerAuthError.from(error, {
37
+ * Bearer: { error: 'InvalidToken' }
38
+ * })
39
+ * }
40
+ * throw error
41
+ * }
42
+ * ```
43
+ */
9
44
  export class LexServerAuthError<
10
45
  N extends LexErrorCode = LexErrorCode,
11
46
  > extends LexError<N> {
12
47
  name = 'LexServerAuthError'
13
48
 
49
+ /**
50
+ * Creates a new authentication error.
51
+ *
52
+ * @param error - The Lexicon error code (e.g., 'AuthenticationRequired')
53
+ * @param message - Human-readable error message
54
+ * @param wwwAuthenticate - WWW-Authenticate header parameters
55
+ * @param options - Standard Error options including `cause`
56
+ */
14
57
  constructor(
15
58
  error: N,
16
59
  message: string,
@@ -20,15 +63,48 @@ export class LexServerAuthError<
20
63
  super(error, message, options)
21
64
  }
22
65
 
66
+ /**
67
+ * Gets the formatted WWW-Authenticate header value.
68
+ *
69
+ * @returns The formatted header string for the 401 response
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const error = new LexServerAuthError('AuthenticationRequired', 'Token required', {
74
+ * Bearer: { realm: 'api.example.com', error: 'MissingToken' }
75
+ * })
76
+ * console.log(error.wwwAuthenticateHeader)
77
+ * // Output: 'Bearer realm="api.example.com", error="MissingToken"'
78
+ * ```
79
+ */
23
80
  get wwwAuthenticateHeader(): string {
24
81
  return formatWWWAuthenticateHeader(this.wwwAuthenticate)
25
82
  }
26
83
 
84
+ /**
85
+ * Converts the error to a JSON representation suitable for response bodies.
86
+ *
87
+ * If the error was created from another LexError (via `from()`), returns
88
+ * the original error's JSON representation.
89
+ *
90
+ * @returns JSON object with error code and message
91
+ */
27
92
  toJSON() {
28
93
  const { cause } = this
29
94
  return cause instanceof LexError ? cause.toJSON() : super.toJSON()
30
95
  }
31
96
 
97
+ /**
98
+ * Converts the error to an HTTP 401 Response with WWW-Authenticate header.
99
+ *
100
+ * The response includes:
101
+ * - Status code 401 (Unauthorized)
102
+ * - WWW-Authenticate header (if parameters were provided)
103
+ * - Access-Control-Expose-Headers for CORS compatibility
104
+ * - JSON body with error details
105
+ *
106
+ * @returns HTTP Response object ready to be sent to the client
107
+ */
32
108
  toResponse(): Response {
33
109
  const { wwwAuthenticateHeader } = this
34
110
 
@@ -42,6 +118,24 @@ export class LexServerAuthError<
42
118
  return Response.json(this.toJSON(), { status: 401, headers })
43
119
  }
44
120
 
121
+ /**
122
+ * Creates a LexServerAuthError from an existing LexError.
123
+ *
124
+ * If the input is already a LexServerAuthError, returns it unchanged.
125
+ * Otherwise, wraps the error with the provided WWW-Authenticate parameters.
126
+ *
127
+ * @param cause - The original LexError to wrap
128
+ * @param wwwAuthenticate - WWW-Authenticate header parameters
129
+ * @returns A LexServerAuthError instance
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const lexError = new LexError('AuthenticationRequired', 'Token expired')
134
+ * const authError = LexServerAuthError.from(lexError, {
135
+ * Bearer: { error: 'ExpiredToken' }
136
+ * })
137
+ * ```
138
+ */
45
139
  static from(
46
140
  cause: LexError,
47
141
  wwwAuthenticate?: WWWAuthenticate,
@@ -1509,6 +1509,7 @@ describe('Subscription', () => {
1509
1509
 
1510
1510
  it('handles subscriptions with cleanup', async () => {
1511
1511
  let sentCount = 0
1512
+ const maxMessages = 10
1512
1513
 
1513
1514
  const { resolve, promise: finallyPromise } = timeoutDeferred(5000)
1514
1515
 
@@ -1516,7 +1517,7 @@ describe('Subscription', () => {
1516
1517
  io.example.subscribe,
1517
1518
  async function* ({ params: { message }, signal }) {
1518
1519
  try {
1519
- for (; sentCount < 10; ) {
1520
+ for (; sentCount < maxMessages; ) {
1520
1521
  await scheduler.wait(5, { signal })
1521
1522
  yield { message, count: ++sentCount }
1522
1523
  }
@@ -1558,7 +1559,7 @@ describe('Subscription', () => {
1558
1559
  ])
1559
1560
 
1560
1561
  expect(sentCount).toBeGreaterThanOrEqual(3)
1561
- expect(sentCount).toBeLessThan(5)
1562
+ expect(sentCount).toBeLessThan(maxMessages)
1562
1563
  })
1563
1564
 
1564
1565
  it('returns 405 for non-GET request', async () => {