@atxp/common 0.2.22 → 0.3.0

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 (55) hide show
  1. package/dist/commonTestHelpers.d.ts +16 -14
  2. package/dist/commonTestHelpers.js +16 -13
  3. package/dist/commonTestHelpers.js.map +1 -1
  4. package/dist/index.cjs +930 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.ts +242 -12
  7. package/dist/index.js +887 -12
  8. package/dist/index.js.map +1 -1
  9. package/dist/jwt.js +5 -2
  10. package/dist/jwt.js.map +1 -1
  11. package/dist/logger.js +6 -3
  12. package/dist/logger.js.map +1 -1
  13. package/dist/mcpJson.js +6 -3
  14. package/dist/mcpJson.js.map +1 -1
  15. package/dist/memoryOAuthDb.js +5 -2
  16. package/dist/memoryOAuthDb.js.map +1 -1
  17. package/dist/oAuthResource.js +6 -3
  18. package/dist/oAuthResource.js.map +1 -1
  19. package/dist/paymentRequiredError.js +8 -5
  20. package/dist/paymentRequiredError.js.map +1 -1
  21. package/dist/platform/index.js +10 -8
  22. package/dist/platform/index.js.map +1 -1
  23. package/dist/servers.js +4 -2
  24. package/dist/servers.js.map +1 -1
  25. package/dist/sseParser.js +6 -4
  26. package/dist/sseParser.js.map +1 -1
  27. package/dist/types.js +5 -3
  28. package/dist/types.js.map +1 -1
  29. package/dist/utils.js +5 -3
  30. package/dist/utils.js.map +1 -1
  31. package/package.json +25 -4
  32. package/dist/commonTestHelpers.d.ts.map +0 -1
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/jwt.d.ts +0 -9
  35. package/dist/jwt.d.ts.map +0 -1
  36. package/dist/logger.d.ts +0 -18
  37. package/dist/logger.d.ts.map +0 -1
  38. package/dist/mcpJson.d.ts +0 -9
  39. package/dist/mcpJson.d.ts.map +0 -1
  40. package/dist/memoryOAuthDb.d.ts +0 -25
  41. package/dist/memoryOAuthDb.d.ts.map +0 -1
  42. package/dist/oAuthResource.d.ts +0 -35
  43. package/dist/oAuthResource.d.ts.map +0 -1
  44. package/dist/paymentRequiredError.d.ts +0 -6
  45. package/dist/paymentRequiredError.d.ts.map +0 -1
  46. package/dist/platform/index.d.ts +0 -14
  47. package/dist/platform/index.d.ts.map +0 -1
  48. package/dist/servers.d.ts +0 -14
  49. package/dist/servers.d.ts.map +0 -1
  50. package/dist/sseParser.d.ts +0 -27
  51. package/dist/sseParser.d.ts.map +0 -1
  52. package/dist/types.d.ts +0 -72
  53. package/dist/types.d.ts.map +0 -1
  54. package/dist/utils.d.ts +0 -19
  55. package/dist/utils.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,12 +1,887 @@
1
- export * from './jwt.js';
2
- export * from './logger.js';
3
- export * from './memoryOAuthDb.js';
4
- export * from './oAuthResource.js';
5
- export * from './paymentRequiredError.js';
6
- export * from './servers.js';
7
- export * from './types.js';
8
- export * from './utils.js';
9
- export * from './mcpJson.js';
10
- export * from './sseParser.js';
11
- export * from './platform/index.js';
12
- //# sourceMappingURL=index.js.map
1
+ import { SignJWT } from 'jose';
2
+ import * as oauth from 'oauth4webapi';
3
+ import { McpError, isJSONRPCError, isJSONRPCResponse, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { ZodError } from 'zod';
5
+
6
+ // TODO: revisit this
7
+ const ISSUER = 'atxp.ai';
8
+ const AUDIENCE = 'https://auth.atxp.ai';
9
+ /**
10
+ * Generate a JWT using the jose library and EdDSA (Ed25519) private key.
11
+ * @param walletId - The subject (public key, wallet address, etc.)
12
+ * @param privateKey - Ed25519 private key as a CryptoKey or Uint8Array
13
+ * @param paymentIds - Optional array of payment IDs to include in the payload
14
+ * @returns JWT string
15
+ */
16
+ const generateJWT = async (walletId, privateKey, paymentRequestId, codeChallenge) => {
17
+ const payload = {
18
+ code_challenge: codeChallenge,
19
+ };
20
+ if (paymentRequestId)
21
+ payload.payment_request_id = paymentRequestId;
22
+ if (codeChallenge)
23
+ payload.code_challenge = codeChallenge;
24
+ return await new SignJWT(payload)
25
+ .setProtectedHeader({ alg: 'EdDSA', typ: 'JWT' })
26
+ .setIssuedAt()
27
+ .setIssuer(ISSUER)
28
+ .setAudience(AUDIENCE)
29
+ .setSubject(walletId)
30
+ .setExpirationTime('2m')
31
+ .sign(privateKey);
32
+ };
33
+
34
+ /**
35
+ * Exhaustiveness check for switch statements.
36
+ * This function should never be called at runtime.
37
+ * It's used to ensure all cases of a union type or enum are handled.
38
+ *
39
+ * @param value - The value that should have been handled by all cases
40
+ * @param message - Optional error message
41
+ * @throws {Error} Always throws an error indicating unhandled case
42
+ */
43
+ function assertNever(value, message) {
44
+ const errorMessage = message || `Unhandled case: ${JSON.stringify(value)}`;
45
+ throw new Error(errorMessage);
46
+ }
47
+ /**
48
+ * Type-safe way to check if a value is one of the enum values.
49
+ *
50
+ * @param enumObj - The enum object
51
+ * @param value - The value to check
52
+ * @returns True if the value is a valid enum value
53
+ */
54
+ function isEnumValue(enumObj, value) {
55
+ return Object.values(enumObj).includes(value);
56
+ }
57
+
58
+ const DEFAULT_AUTHORIZATION_SERVER = 'https://auth.atxp.ai';
59
+ var LogLevel;
60
+ (function (LogLevel) {
61
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
62
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
63
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
64
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
65
+ })(LogLevel || (LogLevel = {}));
66
+
67
+ /* eslint-disable no-console */
68
+ class ConsoleLogger {
69
+ constructor({ prefix = '[atxp]', level = LogLevel.INFO } = {}) {
70
+ this.debug = (message) => {
71
+ this.log(LogLevel.DEBUG, message);
72
+ };
73
+ this.info = (message) => {
74
+ this.log(LogLevel.INFO, message);
75
+ };
76
+ this.warn = (message) => {
77
+ this.log(LogLevel.WARN, message);
78
+ };
79
+ this.error = (message) => {
80
+ this.log(LogLevel.ERROR, message);
81
+ };
82
+ this.prefix = prefix;
83
+ this._level = level;
84
+ }
85
+ get level() {
86
+ return this._level;
87
+ }
88
+ set level(level) {
89
+ this._level = level;
90
+ }
91
+ log(level, message) {
92
+ if (level >= this._level) {
93
+ const consoleMethod = this.getConsoleMethod(level);
94
+ consoleMethod(`${this.prefix} ${message}`);
95
+ }
96
+ }
97
+ getConsoleMethod(level) {
98
+ switch (level) {
99
+ case LogLevel.DEBUG: return console.debug.bind(console);
100
+ case LogLevel.INFO: return console.info.bind(console);
101
+ case LogLevel.WARN: return console.warn.bind(console);
102
+ case LogLevel.ERROR: return console.error.bind(console);
103
+ default: return assertNever(level, `Unknown log level: ${level}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ class MemoryOAuthDb {
109
+ constructor(config = {}) {
110
+ this.clientCredentials = new Map();
111
+ this.pkceValues = new Map(); // key: `${userId}:${state}`
112
+ this.accessTokens = new Map(); // key: `${userId}:${url}`
113
+ this.logger = config.logger || new ConsoleLogger({ prefix: '[memory-oauth-db]', level: LogLevel.INFO });
114
+ this.logger.info(`Initialized in-memory OAuth database (instance: ${Math.random().toString(36).substr(2, 9)})`);
115
+ }
116
+ // OAuthResourceDb methods
117
+ async getClientCredentials(serverUrl) {
118
+ const credentials = this.clientCredentials.get(serverUrl) || null;
119
+ if (credentials) {
120
+ this.logger.debug(`Getting client credentials for server: ${serverUrl} (cached)`);
121
+ }
122
+ else {
123
+ this.logger.info(`Getting client credentials for server: ${serverUrl} (not cached)`);
124
+ this.logger.debug(`Available keys in cache: ${Array.from(this.clientCredentials.keys()).join(', ')}`);
125
+ }
126
+ return credentials;
127
+ }
128
+ async saveClientCredentials(serverUrl, credentials) {
129
+ this.logger.info(`Saving client credentials for server: ${serverUrl}`);
130
+ this.logger.debug(`Client credentials: clientId=${credentials.clientId}`);
131
+ this.clientCredentials.set(serverUrl, credentials);
132
+ }
133
+ // OAuthDb methods
134
+ async getPKCEValues(userId, state) {
135
+ const key = `${userId}:${state}`;
136
+ this.logger.info(`Getting PKCE values for user: ${userId}, state: ${state}`);
137
+ return this.pkceValues.get(key) || null;
138
+ }
139
+ async savePKCEValues(userId, state, values) {
140
+ const key = `${userId}:${state}`;
141
+ this.logger.info(`Saving PKCE values for user: ${userId}, state: ${state}`);
142
+ this.pkceValues.set(key, values);
143
+ }
144
+ async getAccessToken(userId, url) {
145
+ const key = `${userId}:${url}`;
146
+ this.logger.info(`Getting access token for user: ${userId}, url: ${url}`);
147
+ const token = this.accessTokens.get(key);
148
+ if (!token) {
149
+ this.logger.debug(`No cached token found for key: ${key}`);
150
+ return null;
151
+ }
152
+ // Check if token has expired
153
+ if (token.expiresAt && token.expiresAt < Date.now()) {
154
+ this.logger.info(`Access token expired for user: ${userId}, url: ${url}`);
155
+ this.accessTokens.delete(key);
156
+ return null;
157
+ }
158
+ this.logger.debug(`Found valid cached token for user: ${userId}, url: ${url}`);
159
+ return token;
160
+ }
161
+ async saveAccessToken(userId, url, token) {
162
+ const key = `${userId}:${url}`;
163
+ const existingToken = this.accessTokens.get(key);
164
+ if (existingToken) {
165
+ this.logger.debug(`Updating access token for user: ${userId}, url: ${url}`);
166
+ }
167
+ else {
168
+ this.logger.info(`Saving new access token for user: ${userId}, url: ${url}`);
169
+ }
170
+ this.accessTokens.set(key, token);
171
+ }
172
+ async close() {
173
+ this.logger.info('Closing in-memory OAuth database');
174
+ this.clientCredentials.clear();
175
+ this.pkceValues.clear();
176
+ this.accessTokens.clear();
177
+ }
178
+ // Utility methods for debugging/monitoring
179
+ getStats() {
180
+ return {
181
+ clientCredentials: this.clientCredentials.size,
182
+ pkceValues: this.pkceValues.size,
183
+ accessTokens: this.accessTokens.size
184
+ };
185
+ }
186
+ // Clean up expired tokens periodically
187
+ cleanupExpiredTokens() {
188
+ const now = Date.now();
189
+ let cleaned = 0;
190
+ for (const [key, token] of this.accessTokens.entries()) {
191
+ if (token.expiresAt && token.expiresAt < now) {
192
+ this.accessTokens.delete(key);
193
+ cleaned++;
194
+ }
195
+ }
196
+ if (cleaned > 0) {
197
+ this.logger.info(`Cleaned up ${cleaned} expired access tokens`);
198
+ }
199
+ return cleaned;
200
+ }
201
+ }
202
+
203
+ /* eslint-disable @typescript-eslint/no-explicit-any */
204
+ class OAuthResourceClient {
205
+ constructor({ db, callbackUrl = 'http://localhost:3000/unused-dummy-global-callback', isPublic = false, sideChannelFetch = fetch, strict = false, allowInsecureRequests = process.env.NODE_ENV === 'development', clientName = 'Token Introspection Client', logger = new ConsoleLogger() }) {
206
+ // In-memory lock to prevent concurrent client registrations
207
+ this.registrationLocks = new Map();
208
+ this.introspectToken = async (authorizationServerUrl, token, additionalParameters) => {
209
+ // Don't use getAuthorizationServer here, because we're not using the resource server url
210
+ const authorizationServer = await this.authorizationServerFromUrl(new URL(authorizationServerUrl));
211
+ // When introspecting a token, the "resource" server that we want credentials for is the auth server
212
+ let clientCredentials = await this.getClientCredentials(authorizationServer);
213
+ // Create a client for token introspection
214
+ let client = {
215
+ client_id: clientCredentials.clientId,
216
+ token_endpoint_auth_method: 'client_secret_basic'
217
+ };
218
+ // Create client authentication method
219
+ let clientAuth = oauth.ClientSecretBasic(clientCredentials.clientSecret);
220
+ // Use oauth4webapi's built-in token introspection
221
+ let introspectionResponse = await oauth.introspectionRequest(authorizationServer, client, clientAuth, token, {
222
+ additionalParameters,
223
+ [oauth.customFetch]: this.sideChannelFetch,
224
+ [oauth.allowInsecureRequests]: this.allowInsecureRequests
225
+ });
226
+ if (introspectionResponse.status === 403 || introspectionResponse.status === 401) {
227
+ this.logger.info(`Bad response status doing token introspection: ${introspectionResponse.statusText}. Could be due to bad client credentials - trying to re-register`);
228
+ clientCredentials = await this.registerClient(authorizationServer);
229
+ client = {
230
+ client_id: clientCredentials.clientId,
231
+ token_endpoint_auth_method: 'client_secret_basic'
232
+ };
233
+ clientAuth = oauth.ClientSecretBasic(clientCredentials.clientSecret);
234
+ introspectionResponse = await oauth.introspectionRequest(authorizationServer, client, clientAuth, token, {
235
+ additionalParameters,
236
+ [oauth.customFetch]: this.sideChannelFetch,
237
+ [oauth.allowInsecureRequests]: this.allowInsecureRequests
238
+ });
239
+ }
240
+ if (introspectionResponse.status !== 200) {
241
+ throw new Error(`Token introspection failed with status ${introspectionResponse.status}: ${introspectionResponse.statusText}`);
242
+ }
243
+ // Process the introspection response
244
+ const tokenData = await oauth.processIntrospectionResponse(authorizationServer, client, introspectionResponse);
245
+ return {
246
+ active: tokenData.active,
247
+ scope: tokenData.scope,
248
+ sub: tokenData.sub,
249
+ aud: tokenData.aud
250
+ };
251
+ };
252
+ this.getAuthorizationServer = async (resourceServerUrl) => {
253
+ resourceServerUrl = this.normalizeResourceServerUrl(resourceServerUrl);
254
+ try {
255
+ const resourceUrl = new URL(resourceServerUrl);
256
+ const prmResponse = await oauth.resourceDiscoveryRequest(resourceUrl, {
257
+ [oauth.customFetch]: this.sideChannelFetch,
258
+ [oauth.allowInsecureRequests]: this.allowInsecureRequests
259
+ });
260
+ const fallbackToRsAs = !this.strict && prmResponse.status === 404;
261
+ let authServer = undefined;
262
+ if (!fallbackToRsAs) {
263
+ const resourceServer = await oauth.processResourceDiscoveryResponse(resourceUrl, prmResponse);
264
+ authServer = resourceServer.authorization_servers?.[0];
265
+ }
266
+ else {
267
+ // Some older servers serve OAuth metadata from the MCP server instead of PRM data,
268
+ // so if the PRM data isn't found, we'll try to get the AS metadata from the MCP server
269
+ this.logger.info('Protected Resource Metadata document not found, looking for OAuth metadata on resource server');
270
+ // Trim off the path - OAuth metadata is also singular for a server and served from the root
271
+ const rsUrl = new URL(resourceServerUrl);
272
+ const rsAsUrl = rsUrl.protocol + '//' + rsUrl.host + '/.well-known/oauth-authorization-server';
273
+ // Don't use oauth4webapi for this, because these servers might be specifiying an issuer that is not
274
+ // themselves (in order to use a separate AS by just hosting the OAuth metadata on the MCP server)
275
+ // This is against the OAuth spec, but some servers do it anyway
276
+ const rsAsResponse = await this.sideChannelFetch(rsAsUrl);
277
+ if (rsAsResponse.status === 200) {
278
+ const rsAsBody = await rsAsResponse.json();
279
+ authServer = rsAsBody.issuer;
280
+ }
281
+ }
282
+ if (!authServer) {
283
+ throw new Error('No authorization_servers found in protected resource metadata');
284
+ }
285
+ const authServerUrl = new URL(authServer);
286
+ const res = await this.authorizationServerFromUrl(authServerUrl);
287
+ return res;
288
+ }
289
+ catch (error) {
290
+ this.logger.warn(`Error fetching authorization server configuration: ${error}`);
291
+ this.logger.warn(error.stack || '');
292
+ throw error;
293
+ }
294
+ };
295
+ this.authorizationServerFromUrl = async (authServerUrl) => {
296
+ try {
297
+ // Explicitly throw for a tricky edge case to trigger tests
298
+ if (authServerUrl.toString().includes('/.well-known/oauth-protected-resource')) {
299
+ throw new Error('Authorization server URL is a PRM URL, which is not supported. It must be an AS URL.');
300
+ }
301
+ // Now, get the authorization server metadata
302
+ const response = await oauth.discoveryRequest(authServerUrl, {
303
+ algorithm: 'oauth2',
304
+ [oauth.customFetch]: this.sideChannelFetch,
305
+ [oauth.allowInsecureRequests]: this.allowInsecureRequests
306
+ });
307
+ const authorizationServer = await oauth.processDiscoveryResponse(authServerUrl, response);
308
+ return authorizationServer;
309
+ }
310
+ catch (error) {
311
+ this.logger.warn(`Error fetching authorization server configuration: ${error}`);
312
+ throw error;
313
+ }
314
+ };
315
+ this.normalizeResourceServerUrl = (resourceServerUrl) => {
316
+ // the url might be EITHER:
317
+ // 1. the PRM URL (when it's received from the www-authenticate header or a PRM response conforming to RFC 9728)
318
+ // 2. the resource url itself (when we're using the resource url itself)
319
+ // We standardize on the resource url itself, so that we can store it in the DB and all the rest of the plumbing
320
+ // doesn't have to worry about the difference between the two.
321
+ const res = resourceServerUrl.replace('/.well-known/oauth-protected-resource', '');
322
+ return res;
323
+ };
324
+ this.getRegistrationMetadata = async () => {
325
+ // Create client metadata for registration
326
+ const clientMetadata = {
327
+ redirect_uris: [this.callbackUrl],
328
+ // We shouldn't actually need any response_types for this client either, but
329
+ // the OAuth spec requires us to provide a response_type
330
+ response_types: ['code'],
331
+ grant_types: ['authorization_code', 'client_credentials'],
332
+ token_endpoint_auth_method: 'client_secret_basic',
333
+ client_name: this.clientName,
334
+ };
335
+ return clientMetadata;
336
+ };
337
+ this.registerClient = async (authorizationServer) => {
338
+ this.logger.info(`Registering client with authorization server for ${this.callbackUrl}`);
339
+ if (!authorizationServer.registration_endpoint) {
340
+ throw new Error('Authorization server does not support dynamic client registration');
341
+ }
342
+ const clientMetadata = await this.getRegistrationMetadata();
343
+ let registeredClient;
344
+ try {
345
+ // Make the registration request
346
+ const response = await oauth.dynamicClientRegistrationRequest(authorizationServer, clientMetadata, {
347
+ [oauth.customFetch]: this.sideChannelFetch,
348
+ [oauth.allowInsecureRequests]: this.allowInsecureRequests
349
+ });
350
+ // Process the registration response
351
+ registeredClient = await oauth.processDynamicClientRegistrationResponse(response);
352
+ }
353
+ catch (error) {
354
+ this.logger.warn(`Client registration failure error_details: ${JSON.stringify(error.cause?.error_details)}`);
355
+ throw error;
356
+ }
357
+ this.logger.info(`Successfully registered client with ID: ${registeredClient.client_id}`);
358
+ // Create client credentials from the registration response
359
+ const credentials = {
360
+ clientId: registeredClient.client_id,
361
+ clientSecret: registeredClient.client_secret?.toString() || '', // Public client has no secret
362
+ redirectUri: this.callbackUrl
363
+ };
364
+ // Save the credentials in the database
365
+ await this.db.saveClientCredentials(authorizationServer.issuer, credentials);
366
+ return credentials;
367
+ };
368
+ this.getClientCredentials = async (authorizationServer) => {
369
+ let credentials = await this.db.getClientCredentials(authorizationServer.issuer);
370
+ // If no credentials found, register a new client
371
+ if (!credentials) {
372
+ // Check if there's already a registration in progress for this issuer
373
+ const lockKey = authorizationServer.issuer;
374
+ const existingLock = this.registrationLocks.get(lockKey);
375
+ if (existingLock) {
376
+ this.logger.debug(`Waiting for existing client registration for issuer: ${lockKey}`);
377
+ return await existingLock;
378
+ }
379
+ // Create a new registration promise and store it as a lock
380
+ try {
381
+ const registrationPromise = this.registerClient(authorizationServer);
382
+ this.registrationLocks.set(lockKey, registrationPromise);
383
+ credentials = await registrationPromise;
384
+ return credentials;
385
+ }
386
+ finally {
387
+ // Always clean up the lock when done
388
+ this.registrationLocks.delete(lockKey);
389
+ }
390
+ }
391
+ return credentials;
392
+ };
393
+ this.makeOAuthClientAndAuth = (credentials) => {
394
+ // Create the client configuration
395
+ const client = {
396
+ client_id: credentials.clientId,
397
+ token_endpoint_auth_method: 'none'
398
+ };
399
+ let clientAuth = oauth.None();
400
+ // If the client has a secret, that means it was registered as a confidential client
401
+ // In that case, we should auth to the token endpoint using the client secret as well.
402
+ // In either case (public or confidential), we're also using PKCE
403
+ if (credentials.clientSecret) {
404
+ client.token_endpoint_auth_method = 'client_secret_post';
405
+ // Create the client authentication method
406
+ clientAuth = oauth.ClientSecretPost(credentials.clientSecret);
407
+ }
408
+ return [client, clientAuth];
409
+ };
410
+ // Default values above are appropriate for a global client used directly. Subclasses should override these,
411
+ // because things like the callbackUrl will actually be important for them
412
+ this.db = db;
413
+ this.callbackUrl = callbackUrl;
414
+ this.isPublic = isPublic;
415
+ this.sideChannelFetch = sideChannelFetch;
416
+ this.strict = strict;
417
+ this.allowInsecureRequests = allowInsecureRequests;
418
+ this.clientName = clientName;
419
+ this.logger = logger;
420
+ }
421
+ }
422
+ OAuthResourceClient.trimToPath = (url) => {
423
+ try {
424
+ const urlObj = new URL(url);
425
+ return `${urlObj.origin}${urlObj.pathname}`;
426
+ }
427
+ catch (error) {
428
+ // If the URL is invalid, try to construct a valid one
429
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
430
+ return `https://${url}`;
431
+ }
432
+ throw error;
433
+ }
434
+ };
435
+ OAuthResourceClient.getParentPath = (url) => {
436
+ const urlObj = new URL(url);
437
+ urlObj.pathname = urlObj.pathname.replace(/\/[^/]+$/, '');
438
+ const res = urlObj.toString();
439
+ return res === url ? null : res;
440
+ };
441
+
442
+ const PAYMENT_REQUIRED_ERROR_CODE = -30402; // Payment required
443
+ // Do NOT modify this message. It is used by clients to identify an ATXP payment required error
444
+ // in an MCP response. Changing it will break back-compatability.
445
+ const PAYMENT_REQUIRED_PREAMBLE = 'Payment via ATXP is required. ';
446
+ function paymentRequiredError(server, paymentRequestId, chargeAmount) {
447
+ const serverUrl = new URL(server);
448
+ server = serverUrl.origin;
449
+ const paymentRequestUrl = `${server}/payment-request/${paymentRequestId}`;
450
+ const data = { paymentRequestId, paymentRequestUrl, chargeAmount };
451
+ const amountText = chargeAmount ? ` You will be charged ${chargeAmount.toString()}.` : '';
452
+ return new McpError(PAYMENT_REQUIRED_ERROR_CODE, `${PAYMENT_REQUIRED_PREAMBLE}${amountText} Please pay at: ${paymentRequestUrl} and then try again.`, data);
453
+ }
454
+
455
+ const Servers = {
456
+ browse: 'https://browse.mcp.novellum.ai',
457
+ filestore: 'https://filestore.mcp.novellum.ai',
458
+ database: 'https://database.mcp.novellum.ai',
459
+ code: 'https://code.mcp.novellum.ai',
460
+ search: 'https://search.mcp.novellum.ai',
461
+ crawl: 'https://crawl.mcp.novellum.ai',
462
+ video: 'https://video.mcp.novellum.ai',
463
+ image: 'https://image.mcp.novellum.ai',
464
+ music: 'https://music.mcp.novellum.ai',
465
+ research: 'https://research.mcp.novellum.ai',
466
+ shop: 'https://shop.mcp.novellum.ai'
467
+ };
468
+
469
+ /**
470
+ * Parses SSE (Server-Sent Events) formatted text into individual messages
471
+ * @param sseText - The raw SSE text to parse
472
+ * @returns Array of parsed SSE messages
473
+ */
474
+ function parseSSEMessages(sseText) {
475
+ const messages = [];
476
+ const lines = sseText.split('\n');
477
+ let currentMessage = {};
478
+ for (const line of lines) {
479
+ const trimmedLine = line.trim();
480
+ // Empty line indicates end of message
481
+ if (trimmedLine === '') {
482
+ if (currentMessage.data !== undefined) {
483
+ messages.push(currentMessage);
484
+ currentMessage = {};
485
+ }
486
+ continue;
487
+ }
488
+ // Parse field: value format
489
+ const colonIndex = trimmedLine.indexOf(':');
490
+ if (colonIndex === -1) {
491
+ continue; // Skip malformed lines
492
+ }
493
+ const field = trimmedLine.substring(0, colonIndex);
494
+ const value = trimmedLine.substring(colonIndex + 1);
495
+ let retryValue;
496
+ switch (field) {
497
+ case 'event':
498
+ currentMessage.event = value;
499
+ break;
500
+ case 'data':
501
+ // SSE spec allows multiple data fields to be concatenated
502
+ currentMessage.data = currentMessage.data ? currentMessage.data + '\n' + value : value;
503
+ break;
504
+ case 'id':
505
+ currentMessage.id = value;
506
+ break;
507
+ case 'retry':
508
+ retryValue = parseInt(value, 10);
509
+ if (!isNaN(retryValue)) {
510
+ currentMessage.retry = retryValue;
511
+ }
512
+ break;
513
+ }
514
+ }
515
+ // Don't forget the last message if it doesn't end with an empty line
516
+ if (currentMessage.data !== undefined) {
517
+ messages.push(currentMessage);
518
+ }
519
+ return messages;
520
+ }
521
+ /**
522
+ * Extracts JSON-RPC messages from SSE data fields
523
+ * @param sseMessages - Array of SSE messages
524
+ * @param logger - Optional logger for debugging
525
+ * @returns Array of parsed JSON objects from SSE data fields
526
+ */
527
+ function extractJSONFromSSE(sseMessages, logger) {
528
+ const jsonMessages = [];
529
+ for (const sseMessage of sseMessages) {
530
+ try {
531
+ if (sseMessage.data) {
532
+ const parsed = JSON.parse(sseMessage.data);
533
+ jsonMessages.push(parsed);
534
+ }
535
+ }
536
+ catch (error) {
537
+ logger?.warn(`Failed to parse SSE data as JSON: ${sseMessage.data}`);
538
+ logger?.debug(`Parse error: ${error}`);
539
+ }
540
+ }
541
+ return jsonMessages;
542
+ }
543
+ /**
544
+ * Determines if a response body appears to be SSE formatted
545
+ * @param body - The response body to check
546
+ * @returns true if the body appears to be SSE formatted
547
+ */
548
+ function isSSEResponse(body) {
549
+ if (typeof body !== 'string') {
550
+ return false;
551
+ }
552
+ const lines = body.split('\n');
553
+ for (const line of lines) {
554
+ const trimmedLine = line.trim();
555
+ if (trimmedLine === '')
556
+ continue;
557
+ // Check for SSE field format (field: value)
558
+ const colonIndex = trimmedLine.indexOf(':');
559
+ if (colonIndex === -1)
560
+ continue;
561
+ const field = trimmedLine.substring(0, colonIndex);
562
+ if (['event', 'data', 'id', 'retry'].includes(field)) {
563
+ return true;
564
+ }
565
+ }
566
+ return false;
567
+ }
568
+
569
+ function parsePaymentRequests(message) {
570
+ const res = [];
571
+ // Handle MCP protocol-level errors. These have an explicit error code that we can check for
572
+ if (isJSONRPCError(message)) {
573
+ // Explicitly throw payment required errors that result in MCP protocol-level errors
574
+ const rpcError = message;
575
+ if (rpcError.error.code === PAYMENT_REQUIRED_ERROR_CODE) {
576
+ const paymentRequestUrl = rpcError.error.data?.paymentRequestUrl;
577
+ const dataPr = _parsePaymentRequestFromString(paymentRequestUrl);
578
+ if (dataPr) {
579
+ res.push(dataPr);
580
+ }
581
+ else {
582
+ const pr = _parsePaymentRequestFromString(rpcError.error.message);
583
+ if (pr) {
584
+ res.push(pr);
585
+ }
586
+ }
587
+ }
588
+ // Elicitation - required errors
589
+ // Current draft of elicitation-required error code as per
590
+ // https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887
591
+ if (rpcError.error.code === -32604) {
592
+ const elicitations = rpcError.error.data?.elicitations || [];
593
+ for (const elicitation of elicitations) {
594
+ if (elicitation?.mode === 'url') {
595
+ const pr = _parsePaymentRequestFromString(elicitation?.url);
596
+ if (pr) {
597
+ res.push(pr);
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+ // TODO: Ensure that ATXP errors only come back as MCP protocol-level errors.
604
+ // Handle MCP tool application-level errors. For these, the error message is serialized into a normal
605
+ // tool response with the isError flag set
606
+ if (isJSONRPCResponse(message)) {
607
+ const toolResult = message.result;
608
+ if (toolResult.isError) {
609
+ for (const content of toolResult.content) {
610
+ if (content.type === 'text') {
611
+ const text = content.text;
612
+ if (text.includes(PAYMENT_REQUIRED_PREAMBLE) && text.includes(PAYMENT_REQUIRED_ERROR_CODE.toString())) {
613
+ const pr = _parsePaymentRequestFromString(text);
614
+ if (pr) {
615
+ res.push(pr);
616
+ }
617
+ }
618
+ }
619
+ }
620
+ }
621
+ }
622
+ return res;
623
+ }
624
+ function _parsePaymentRequestFromString(text) {
625
+ if (!text) {
626
+ return null;
627
+ }
628
+ const paymentRequestUrl = /(http[^ ]+)\/payment-request\/([^ ]+)/.exec(text);
629
+ if (paymentRequestUrl) {
630
+ const id = paymentRequestUrl[2];
631
+ const url = paymentRequestUrl[0];
632
+ return { url, id };
633
+ }
634
+ return null;
635
+ }
636
+ async function parseMcpMessages(json, logger) {
637
+ let messages = [];
638
+ try {
639
+ // Check if the response is SSE formatted
640
+ if (typeof json === 'string' && isSSEResponse(json)) {
641
+ logger?.debug('Detected SSE-formatted response, parsing SSE messages');
642
+ const sseMessages = parseSSEMessages(json);
643
+ const jsonMessages = extractJSONFromSSE(sseMessages, logger);
644
+ // Process each JSON message from SSE
645
+ for (const jsonMsg of jsonMessages) {
646
+ try {
647
+ if (Array.isArray(jsonMsg)) {
648
+ // Handle batch messages from SSE
649
+ const batchMessages = jsonMsg.map(msg => JSONRPCMessageSchema.parse(msg));
650
+ messages.push(...batchMessages);
651
+ }
652
+ else {
653
+ // Handle single message from SSE
654
+ const message = JSONRPCMessageSchema.parse(jsonMsg);
655
+ messages.push(message);
656
+ }
657
+ }
658
+ catch (parseError) {
659
+ if (parseError instanceof ZodError) {
660
+ logger?.warn(`Invalid JSON-RPC message format in SSE data`);
661
+ logger?.debug(parseError.message);
662
+ }
663
+ else {
664
+ logger?.error(`Unexpected error parsing JSON-RPC message from SSE: ${parseError}`);
665
+ }
666
+ }
667
+ }
668
+ }
669
+ else {
670
+ // Handle regular JSON responses
671
+ if (Array.isArray(json)) {
672
+ messages = json.map(msg => JSONRPCMessageSchema.parse(msg));
673
+ }
674
+ else {
675
+ messages = [JSONRPCMessageSchema.parse(json)];
676
+ }
677
+ }
678
+ }
679
+ catch (error) {
680
+ // If Zod validation fails, log the error and return empty array
681
+ if (error instanceof ZodError) {
682
+ logger?.warn(`Invalid JSON-RPC message format`);
683
+ logger?.debug(error.message);
684
+ }
685
+ else {
686
+ logger?.error(`Unexpected error parsing JSON-RPC messages: ${error}`);
687
+ }
688
+ }
689
+ return messages;
690
+ }
691
+
692
+ /* eslint-disable @typescript-eslint/no-explicit-any */
693
+ // Platform detection - supports both Expo and bare React Native
694
+ function getIsReactNative() {
695
+ const nav = (typeof navigator !== 'undefined' ? navigator : (typeof global !== 'undefined' ? global.navigator : undefined));
696
+ return !!nav && nav.product === 'ReactNative';
697
+ }
698
+ const isNode = typeof process !== 'undefined' && !!process.versions?.node;
699
+ const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
700
+ const isNextJS = typeof process !== 'undefined' && process.env.NEXT_RUNTIME !== undefined;
701
+ const isWebEnvironment = isBrowser || isNextJS;
702
+ // Helper to load modules in both CommonJS and ESM environments
703
+ function loadModule(moduleId) {
704
+ try {
705
+ // First try standard require if available (CommonJS environment)
706
+ if (typeof require !== 'undefined') {
707
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
708
+ return require(moduleId);
709
+ }
710
+ // Check if we're in a Node.js environment where createRequire might be available
711
+ if (typeof process !== 'undefined' && process.versions && process.versions.node) {
712
+ try {
713
+ const { createRequire } = eval('require')('module');
714
+ const require = createRequire(import.meta.url || 'file:///dummy');
715
+ return require(moduleId);
716
+ }
717
+ catch {
718
+ // Fall through to eval require
719
+ }
720
+ }
721
+ // Fall back to eval('require') to prevent bundler static analysis
722
+ const requireFunc = (0, eval)('require');
723
+ return requireFunc(moduleId);
724
+ }
725
+ catch {
726
+ throw new Error(`Failed to load module "${moduleId}" synchronously. In ESM environments, please ensure the module is pre-loaded or use MemoryOAuthDb instead.`);
727
+ }
728
+ }
729
+ // Async version for cases where we can fall back to dynamic import
730
+ async function loadModuleAsync(moduleId) {
731
+ try {
732
+ // Try synchronous loading first
733
+ return loadModule(moduleId);
734
+ }
735
+ catch {
736
+ // Fall back to dynamic import for ESM
737
+ try {
738
+ return await import(moduleId);
739
+ }
740
+ catch (e) {
741
+ throw new Error(`Failed to load module "${moduleId}": ${e instanceof Error ? e.message : 'Module loading not available in this environment'}`);
742
+ }
743
+ }
744
+ }
745
+ // Apply URL polyfill for React Native/Expo
746
+ if (getIsReactNative()) {
747
+ loadModule('react-native-url-polyfill/auto');
748
+ }
749
+ // React Native safe fetch that prevents body consumption issues
750
+ const createReactNativeSafeFetch = (originalFetch) => {
751
+ return async (url, init) => {
752
+ const response = await originalFetch(url, init);
753
+ // For non-2xx responses or responses we know won't have JSON bodies, return as-is
754
+ if (!response.ok || response.status === 204) {
755
+ return response;
756
+ }
757
+ // Pre-read the body to avoid consumption issues
758
+ const contentType = response.headers.get('content-type');
759
+ if (contentType && contentType.includes('application/json')) {
760
+ try {
761
+ const bodyText = await response.text();
762
+ // Create a new Response with the pre-read body
763
+ return new Response(bodyText, {
764
+ status: response.status,
765
+ statusText: response.statusText,
766
+ headers: response.headers
767
+ });
768
+ }
769
+ catch {
770
+ // If reading fails, return original response
771
+ return response;
772
+ }
773
+ }
774
+ return response;
775
+ };
776
+ };
777
+ // Platform factory functions
778
+ function createReactNativeCrypto() {
779
+ let expoCrypto;
780
+ try {
781
+ expoCrypto = loadModule('expo-crypto');
782
+ }
783
+ catch {
784
+ throw new Error('React Native detected but expo-crypto package is required. ' +
785
+ 'Please install it: npm install expo-crypto');
786
+ }
787
+ return {
788
+ digest: async (data) => {
789
+ const hash = await expoCrypto.digestStringAsync(expoCrypto.CryptoDigestAlgorithm.SHA256, new TextDecoder().decode(data));
790
+ return new Uint8Array(Buffer.from(hash, 'hex'));
791
+ },
792
+ randomUUID: () => expoCrypto.randomUUID(),
793
+ toHex: (data) => Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(''),
794
+ };
795
+ }
796
+ function createBrowserCrypto() {
797
+ return {
798
+ digest: async (data) => {
799
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) {
800
+ const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
801
+ return new Uint8Array(hashBuffer);
802
+ }
803
+ throw new Error('Web Crypto API not available in this browser environment');
804
+ },
805
+ randomUUID: () => {
806
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.randomUUID) {
807
+ return globalThis.crypto.randomUUID();
808
+ }
809
+ // Fallback UUID generation for older browsers
810
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
811
+ const r = Math.random() * 16 | 0;
812
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
813
+ return v.toString(16);
814
+ });
815
+ },
816
+ toHex: (data) => Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(''),
817
+ };
818
+ }
819
+ function createNodeCrypto() {
820
+ let cryptoModule = null;
821
+ return {
822
+ digest: async (data) => {
823
+ // Prefer Web Crypto API if available (works in Node.js 16+ and browsers)
824
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) {
825
+ try {
826
+ const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
827
+ return new Uint8Array(hashBuffer);
828
+ }
829
+ catch {
830
+ // Fall through to Node.js crypto
831
+ }
832
+ }
833
+ // Fall back to Node.js crypto module
834
+ if (!cryptoModule) {
835
+ // Try node:crypto first, then fallback to crypto
836
+ try {
837
+ cryptoModule = await loadModuleAsync('node:crypto');
838
+ }
839
+ catch {
840
+ cryptoModule = await loadModuleAsync('crypto');
841
+ }
842
+ }
843
+ return new Uint8Array(cryptoModule.createHash('sha256').update(data).digest());
844
+ },
845
+ randomUUID: () => {
846
+ // Prefer Web Crypto API if available (works in Node.js 16+ and browsers)
847
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.randomUUID) {
848
+ return globalThis.crypto.randomUUID();
849
+ }
850
+ // Try Node.js crypto module if available (CommonJS environments)
851
+ try {
852
+ // Try node:crypto first, then fallback to crypto
853
+ let crypto;
854
+ try {
855
+ crypto = loadModule('node:crypto');
856
+ }
857
+ catch {
858
+ crypto = loadModule('crypto');
859
+ }
860
+ return crypto.randomUUID();
861
+ }
862
+ catch {
863
+ // Fallback to Math.random() based UUID generation (RFC 4122 compliant)
864
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
865
+ const r = Math.random() * 16 | 0;
866
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
867
+ return v.toString(16);
868
+ });
869
+ }
870
+ },
871
+ toHex: (data) => Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(''),
872
+ };
873
+ }
874
+ // Export platform-specific implementations
875
+ let crypto;
876
+ if (getIsReactNative()) {
877
+ crypto = createReactNativeCrypto();
878
+ }
879
+ else if (isWebEnvironment) {
880
+ crypto = createBrowserCrypto();
881
+ }
882
+ else {
883
+ crypto = createNodeCrypto();
884
+ }
885
+
886
+ export { ConsoleLogger, DEFAULT_AUTHORIZATION_SERVER, LogLevel, MemoryOAuthDb, OAuthResourceClient, PAYMENT_REQUIRED_ERROR_CODE, PAYMENT_REQUIRED_PREAMBLE, Servers, assertNever, createReactNativeSafeFetch, crypto, extractJSONFromSSE, generateJWT, getIsReactNative, isBrowser, isEnumValue, isNextJS, isNode, isSSEResponse, isWebEnvironment, parseMcpMessages, parsePaymentRequests, parseSSEMessages, paymentRequiredError };
887
+ //# sourceMappingURL=index.js.map