@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.
- package/dist/commonTestHelpers.d.ts +16 -14
- package/dist/commonTestHelpers.js +16 -13
- package/dist/commonTestHelpers.js.map +1 -1
- package/dist/index.cjs +930 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +242 -12
- package/dist/index.js +887 -12
- package/dist/index.js.map +1 -1
- package/dist/jwt.js +5 -2
- package/dist/jwt.js.map +1 -1
- package/dist/logger.js +6 -3
- package/dist/logger.js.map +1 -1
- package/dist/mcpJson.js +6 -3
- package/dist/mcpJson.js.map +1 -1
- package/dist/memoryOAuthDb.js +5 -2
- package/dist/memoryOAuthDb.js.map +1 -1
- package/dist/oAuthResource.js +6 -3
- package/dist/oAuthResource.js.map +1 -1
- package/dist/paymentRequiredError.js +8 -5
- package/dist/paymentRequiredError.js.map +1 -1
- package/dist/platform/index.js +10 -8
- package/dist/platform/index.js.map +1 -1
- package/dist/servers.js +4 -2
- package/dist/servers.js.map +1 -1
- package/dist/sseParser.js +6 -4
- package/dist/sseParser.js.map +1 -1
- package/dist/types.js +5 -3
- package/dist/types.js.map +1 -1
- package/dist/utils.js +5 -3
- package/dist/utils.js.map +1 -1
- package/package.json +25 -4
- package/dist/commonTestHelpers.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jwt.d.ts +0 -9
- package/dist/jwt.d.ts.map +0 -1
- package/dist/logger.d.ts +0 -18
- package/dist/logger.d.ts.map +0 -1
- package/dist/mcpJson.d.ts +0 -9
- package/dist/mcpJson.d.ts.map +0 -1
- package/dist/memoryOAuthDb.d.ts +0 -25
- package/dist/memoryOAuthDb.d.ts.map +0 -1
- package/dist/oAuthResource.d.ts +0 -35
- package/dist/oAuthResource.d.ts.map +0 -1
- package/dist/paymentRequiredError.d.ts +0 -6
- package/dist/paymentRequiredError.d.ts.map +0 -1
- package/dist/platform/index.d.ts +0 -14
- package/dist/platform/index.d.ts.map +0 -1
- package/dist/servers.d.ts +0 -14
- package/dist/servers.d.ts.map +0 -1
- package/dist/sseParser.d.ts +0 -27
- package/dist/sseParser.d.ts.map +0 -1
- package/dist/types.d.ts +0 -72
- package/dist/types.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -19
- package/dist/utils.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,887 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|