tiny-http-mcp-server 0.1.35 → 0.1.36
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/composition.json +1 -1
- package/node_modules/mcp-oauth/README.md +8 -0
- package/node_modules/mcp-oauth/dist/client/client-registration.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/client/client-registration.js +45 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +9 -6
- package/node_modules/mcp-oauth/dist/client/types.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/index.js +1202 -1159
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -132,6 +132,14 @@ but an expired secret is never submitted for refresh. Native DCR can replace
|
|
|
132
132
|
an expired registration during explicit authorization; caller-owned imports
|
|
133
133
|
must be updated. Headless requests retain the old record and report recovery
|
|
134
134
|
is required without creating a pending refresh marker.
|
|
135
|
+
Native registrations retain `requestedRedirectUri`, the actual listener URI
|
|
136
|
+
submitted to DCR, separately from the full response metadata. Fresh responses
|
|
137
|
+
may normalize a loopback port or represent IPv4 loopback as portless localhost;
|
|
138
|
+
host, path, scheme, query and fragment differences outside that boundary fail.
|
|
139
|
+
Authorization and code exchange always use the actual listener URI. Silent
|
|
140
|
+
refresh keeps its original client regardless of callback changes. At interactive
|
|
141
|
+
authorization, a native registration with an obsolete captured callback is
|
|
142
|
+
replaced; caller-owned full registration imports retain their original identity.
|
|
135
143
|
Set `client.tokenEndpointAuthMethod` to `none`, `client_secret_post` or
|
|
136
144
|
`client_secret_basic`; a full registration can supply the same field as
|
|
137
145
|
`token_endpoint_auth_method`. Public clients never transmit a stored secret.
|
|
@@ -2,3 +2,5 @@ import type { OAuthClientRegistration, StoredOAuthClient } from "./types.js";
|
|
|
2
2
|
/** Validate and copy a bounded JSON DCR response without quoting credential input. */
|
|
3
3
|
export declare function parseOAuthClientRegistration(value: unknown): OAuthClientRegistration;
|
|
4
4
|
export declare function normalizeStoredOAuthClient(value: unknown): StoredOAuthClient | null;
|
|
5
|
+
/** Fresh DCR may describe a normalized loopback port; saved identity stays exact. */
|
|
6
|
+
export declare function registrationMatchesRedirect(client: StoredOAuthClient, requestedUri: string, fresh?: boolean): boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loopbackTarget } from "./loopback-authorization.js";
|
|
1
2
|
import { normalizeOAuthScope } from "./scope.js";
|
|
2
3
|
import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
|
|
3
4
|
/** Validate and copy a bounded JSON DCR response without quoting credential input. */
|
|
@@ -84,6 +85,18 @@ export function normalizeStoredOAuthClient(value) {
|
|
|
84
85
|
(clientSecret !== undefined && (typeof clientSecret !== "string" || clientSecret.trim() === "")))
|
|
85
86
|
return null;
|
|
86
87
|
const client = { clientId: clientId.trim(), ...(clientSecret === undefined ? {} : { clientSecret: clientSecret.trim() }) };
|
|
88
|
+
const requestedRedirectUri = Object.hasOwn(record, "requestedRedirectUri") ? record.requestedRedirectUri : undefined;
|
|
89
|
+
if (requestedRedirectUri !== undefined) {
|
|
90
|
+
try {
|
|
91
|
+
if (typeof requestedRedirectUri !== "string")
|
|
92
|
+
throw new Error("Invalid redirect identity");
|
|
93
|
+
loopbackTarget({ redirectUri: requestedRedirectUri });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new Error("Invalid stored OAuth registration redirect identity");
|
|
97
|
+
}
|
|
98
|
+
client.requestedRedirectUri = requestedRedirectUri;
|
|
99
|
+
}
|
|
87
100
|
const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record, "tokenEndpointAuthMethod") ? record.tokenEndpointAuthMethod : undefined);
|
|
88
101
|
if (Object.hasOwn(record, "registration") && record.registration !== undefined) {
|
|
89
102
|
const registration = parseOAuthClientRegistration(record.registration);
|
|
@@ -101,3 +114,35 @@ export function normalizeStoredOAuthClient(value) {
|
|
|
101
114
|
client.tokenEndpointAuthMethod = method;
|
|
102
115
|
return client;
|
|
103
116
|
}
|
|
117
|
+
/** Fresh DCR may describe a normalized loopback port; saved identity stays exact. */
|
|
118
|
+
export function registrationMatchesRedirect(client, requestedUri, fresh = false) {
|
|
119
|
+
if (client.requestedRedirectUri !== undefined)
|
|
120
|
+
return client.requestedRedirectUri === requestedUri;
|
|
121
|
+
const redirects = client.registration?.redirect_uris;
|
|
122
|
+
if (redirects === undefined || redirects === null || redirects.length === 0)
|
|
123
|
+
return true;
|
|
124
|
+
return redirects.some(returnedUri => {
|
|
125
|
+
if (returnedUri === requestedUri)
|
|
126
|
+
return true;
|
|
127
|
+
if (!fresh)
|
|
128
|
+
return false;
|
|
129
|
+
let returned, requested;
|
|
130
|
+
try {
|
|
131
|
+
returned = new URL(returnedUri);
|
|
132
|
+
requested = new URL(requestedUri);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (requested.protocol !== "http:" || returned.protocol !== "http:" ||
|
|
138
|
+
!["127.0.0.1", "[::1]", "localhost"].includes(requested.hostname))
|
|
139
|
+
return false;
|
|
140
|
+
const sameHost = returned.hostname === requested.hostname;
|
|
141
|
+
const normalizedIpv4 = requested.hostname === "127.0.0.1" && returned.hostname === "localhost" && returned.port === "";
|
|
142
|
+
if (!sameHost && !normalizedIpv4)
|
|
143
|
+
return false;
|
|
144
|
+
returned.hostname = requested.hostname;
|
|
145
|
+
returned.port = requested.port;
|
|
146
|
+
return returned.href === requested.href;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeStoredOAuthClient, parseOAuthClientRegistration } from "./client-registration.js";
|
|
1
|
+
import { normalizeStoredOAuthClient, parseOAuthClientRegistration, registrationMatchesRedirect } from "./client-registration.js";
|
|
2
2
|
import { normalizeOAuthScope } from "./scope.js";
|
|
3
3
|
import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
|
|
4
4
|
import { isIP } from "node:net";
|
|
@@ -372,7 +372,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
372
372
|
let storedClient = await loadRegisteredClient(discovery.authorizationServer);
|
|
373
373
|
if (storedClient !== null) {
|
|
374
374
|
assertRegistrationIssuer(storedClient, discovery.authorizationServer);
|
|
375
|
-
if (hasExpiredClientSecret(storedClient, now)) {
|
|
375
|
+
if (hasExpiredClientSecret(storedClient, now) || !registrationMatchesRedirect(storedClient, redirectUri)) {
|
|
376
376
|
await clearRegisteredClient(discovery.authorizationServer);
|
|
377
377
|
storedClient = null;
|
|
378
378
|
}
|
|
@@ -385,7 +385,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
if (registrationEndpoint === undefined) {
|
|
388
|
-
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
|
|
388
|
+
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
|
|
389
389
|
return {
|
|
390
390
|
kind: "dynamic",
|
|
391
391
|
fromStoredRegistration: true,
|
|
@@ -394,7 +394,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
394
394
|
}
|
|
395
395
|
throw new Error("Authorization server metadata is missing registration_endpoint");
|
|
396
396
|
}
|
|
397
|
-
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
|
|
397
|
+
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
|
|
398
398
|
const isConfiguredStaticFallback = configuredClient !== null &&
|
|
399
399
|
existingSession.client.clientId === configuredClient.clientId &&
|
|
400
400
|
existingSession.client.clientSecret === configuredClient.clientSecret;
|
|
@@ -437,11 +437,14 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
437
437
|
assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
|
|
438
438
|
if (hasExpiredClientSecret(registeredClient, now))
|
|
439
439
|
throw new Error("OAuth client secret has expired in the registration response");
|
|
440
|
-
|
|
440
|
+
if (!registrationMatchesRedirect(registeredClient, redirectUri, true))
|
|
441
|
+
throw new Error("OAuth registration response does not match the requested redirect URI");
|
|
442
|
+
const clientWithRedirect = { ...registeredClient, requestedRedirectUri: redirectUri };
|
|
443
|
+
await saveRegisteredClient(discovery.authorizationServer, clientWithRedirect);
|
|
441
444
|
return {
|
|
442
445
|
kind: "dynamic",
|
|
443
446
|
fromStoredRegistration: false,
|
|
444
|
-
client:
|
|
447
|
+
client: clientWithRedirect
|
|
445
448
|
};
|
|
446
449
|
}
|
|
447
450
|
async function loadSession(resource) {
|
|
@@ -92,6 +92,8 @@ export interface OAuthClientRegistration extends Record<string, unknown> {
|
|
|
92
92
|
}
|
|
93
93
|
export interface StoredOAuthClient {
|
|
94
94
|
clientId: string;
|
|
95
|
+
/** Actual listener URI submitted when this native client was registered. */
|
|
96
|
+
requestedRedirectUri?: string;
|
|
95
97
|
clientSecret?: string;
|
|
96
98
|
registration?: OAuthClientRegistration;
|
|
97
99
|
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
@@ -195,6 +195,8 @@ interface OAuthClientRegistration extends Record<string, unknown> {
|
|
|
195
195
|
}
|
|
196
196
|
interface StoredOAuthClient {
|
|
197
197
|
clientId: string;
|
|
198
|
+
/** Actual listener URI submitted when this native client was registered. */
|
|
199
|
+
requestedRedirectUri?: string;
|
|
198
200
|
clientSecret?: string;
|
|
199
201
|
registration?: OAuthClientRegistration;
|
|
200
202
|
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|