toolcraft 0.0.313 → 0.0.315
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/composition.json +3 -3
- package/dist/composition.json +3 -3
- package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/README.md +25 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +1 -6
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +7 -18
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.d.ts +4 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.js +103 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +76 -50
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/scope.d.ts +2 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/scope.js +9 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-auth-method.d.ts +2 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-auth-method.js +7 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +3 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.js +23 -10
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +27 -4
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.d.ts +2 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.js +1 -0
- package/node_modules/tiny-http-mcp-server/package.json +1 -1
- package/node_modules/tiny-mcp-client/dist/index.d.ts +27 -4
- package/node_modules/tiny-mcp-client/dist/index.js +226 -73
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"name": "tiny-http-mcp-server",
|
|
101
|
-
"version": "0.1.
|
|
101
|
+
"version": "0.1.34",
|
|
102
102
|
"license": "MIT"
|
|
103
103
|
},
|
|
104
104
|
{
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
116
|
+
"version": "0.0.315",
|
|
117
117
|
"license": "MIT"
|
|
118
118
|
},
|
|
119
119
|
{
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"name": "toolcraft-schema",
|
|
126
|
-
"version": "0.0.
|
|
126
|
+
"version": "0.0.315",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
package/dist/composition.json
CHANGED
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"name": "tiny-http-mcp-server",
|
|
101
|
-
"version": "0.1.
|
|
101
|
+
"version": "0.1.34",
|
|
102
102
|
"license": "MIT"
|
|
103
103
|
},
|
|
104
104
|
{
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
116
|
+
"version": "0.0.315",
|
|
117
117
|
"license": "MIT"
|
|
118
118
|
},
|
|
119
119
|
{
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"name": "toolcraft-schema",
|
|
126
|
-
"version": "0.0.
|
|
126
|
+
"version": "0.0.315",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
@@ -96,7 +96,14 @@ on 401 even when the server omits `error="invalid_token"`. Invalid provenance
|
|
|
96
96
|
fails without quoting token values.
|
|
97
97
|
|
|
98
98
|
Configure `client.metadata.scope` to request a precise scope set; broader
|
|
99
|
-
discovery metadata does not override it.
|
|
99
|
+
discovery metadata does not override it. Explicit scopes must match the cached
|
|
100
|
+
or imported grant's scope set; ordering, repeated spaces and duplicates are
|
|
101
|
+
normalized. An imported grant must declare its scope when a scope is configured.
|
|
102
|
+
Authorization records the requested set when the endpoint omits scope, and
|
|
103
|
+
refresh retains the previous granted set. Mismatched responses never activate
|
|
104
|
+
credentials; an unusable refresh response retains the pending refresh record.
|
|
105
|
+
Select a separate persistence namespace for another scope profile. No scope is
|
|
106
|
+
invented when the client does not configure one.
|
|
100
107
|
|
|
101
108
|
Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
|
|
102
109
|
`tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
|
|
@@ -105,6 +112,23 @@ Discovery binds an expired or explicitly rejected grant before silent refresh,
|
|
|
105
112
|
using the original configured client. Persisted sessions take precedence,
|
|
106
113
|
including sessions whose tokens have been cleared; an import cannot revive them.
|
|
107
114
|
Input tokens are copied and invalid expiry values fail before authorization.
|
|
115
|
+
Pass a complete DCR response as `client.registration` (or validate untrusted JSON
|
|
116
|
+
with `parseOAuthClientRegistration`). Dynamic clients infer their original ID
|
|
117
|
+
and secret from that response and reuse it without registering another app.
|
|
118
|
+
Explicit ID/secret values must agree with the imported response. Sessions and
|
|
119
|
+
native registration stores retain arrays, issuance/expiry timestamps and JSON
|
|
120
|
+
provider metadata. Registration input is copied, bounded to 64 KiB and 64
|
|
121
|
+
levels, and rejects invalid standard field types and non-JSON extensions.
|
|
122
|
+
Set `client.tokenEndpointAuthMethod` to `none`, `client_secret_post` or
|
|
123
|
+
`client_secret_basic`; a full registration can supply the same field as
|
|
124
|
+
`token_endpoint_auth_method`. Public clients never transmit a stored secret.
|
|
125
|
+
Basic credentials are individually form-encoded before Base64 encoding and
|
|
126
|
+
are omitted from the form body. Cached grants retain their registered method;
|
|
127
|
+
an explicitly different configured method requires separate persistence or a
|
|
128
|
+
reset. Native DCR chooses a supported method, preferring public PKCE when
|
|
129
|
+
advertised. Unsupported methods and missing confidential secrets fail before
|
|
130
|
+
token requests. Existing clients without a method keep the previous default:
|
|
131
|
+
body authentication when a secret is present, public authentication otherwise.
|
|
108
132
|
Static clients and dynamic initial-grant imports require cached grants to match
|
|
109
133
|
the original normalized client ID and secret. A different client configuration
|
|
110
134
|
fails before attaching or refreshing credentials and retains the stored record;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { type CreateSecretStoreInput } from "auth-store";
|
|
2
|
-
import type { OAuthSessionStore } from "./types.js";
|
|
3
|
-
interface StoredOAuthClient {
|
|
4
|
-
clientId: string;
|
|
5
|
-
clientSecret?: string;
|
|
6
|
-
}
|
|
2
|
+
import type { OAuthSessionStore, StoredOAuthClient } from "./types.js";
|
|
7
3
|
export interface OAuthClientStore {
|
|
8
4
|
load(issuer: string): Promise<StoredOAuthClient | null>;
|
|
9
5
|
save(issuer: string, client: StoredOAuthClient): Promise<void>;
|
|
@@ -12,4 +8,3 @@ export interface OAuthClientStore {
|
|
|
12
8
|
export declare function createAuthStoreSessionStore(options?: CreateSecretStoreInput, namespace?: string): OAuthSessionStore;
|
|
13
9
|
export declare function createAuthStoreClientStore(options: CreateSecretStoreInput, namespace?: string): OAuthClientStore;
|
|
14
10
|
export declare function assertPersistenceNamespace(namespace: string | undefined): void;
|
|
15
|
-
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeStoredOAuthClient } from "./client-registration.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { createSecretStore } from "auth-store";
|
|
@@ -62,15 +63,10 @@ export function createAuthStoreClientStore(options, namespace) {
|
|
|
62
63
|
catch {
|
|
63
64
|
throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Object.prototype.hasOwnProperty.call(parsed, "clientSecret")) {
|
|
70
|
-
client.clientSecret = getOwnEntry(parsed, "clientSecret");
|
|
71
|
-
}
|
|
72
|
-
return client;
|
|
73
|
-
}
|
|
66
|
+
// Preserve all registration metadata; provider normalization validates its
|
|
67
|
+
// identity and schema before it can authorize a request.
|
|
68
|
+
if (isObjectRecord(parsed) && typeof getOwnEntry(parsed, "clientId") === "string")
|
|
69
|
+
return parsed;
|
|
74
70
|
throw new Error("Stored OAuth client must be a JSON object with clientId");
|
|
75
71
|
},
|
|
76
72
|
async save(issuer, client) {
|
|
@@ -142,20 +138,13 @@ function isStoredOAuthSession(value) {
|
|
|
142
138
|
}
|
|
143
139
|
return (isNonBlankOwnString(value, "resource") &&
|
|
144
140
|
isNonBlankOwnString(value, "authorizationServer") &&
|
|
145
|
-
|
|
141
|
+
normalizeStoredOAuthClient(getOwnEntry(value, "client")) !== null &&
|
|
146
142
|
isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
|
|
143
|
+
(getOwnEntry(value, "requestedScope") === undefined || isNonBlankOwnString(value, "requestedScope")) &&
|
|
147
144
|
(getOwnEntry(value, "refreshState") === undefined ||
|
|
148
145
|
(getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
|
|
149
146
|
isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
|
|
150
147
|
}
|
|
151
|
-
function isStoredOAuthClient(value) {
|
|
152
|
-
if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
const clientSecret = getOwnEntry(value, "clientSecret");
|
|
156
|
-
return (clientSecret === undefined ||
|
|
157
|
-
(typeof clientSecret === "string" && clientSecret.trim().length > 0));
|
|
158
|
-
}
|
|
159
148
|
function isStoredOAuthDiscovery(value) {
|
|
160
149
|
if (!isObjectRecord(value)) {
|
|
161
150
|
return false;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OAuthClientRegistration, StoredOAuthClient } from "./types.js";
|
|
2
|
+
/** Validate and copy a bounded JSON DCR response without quoting credential input. */
|
|
3
|
+
export declare function parseOAuthClientRegistration(value: unknown): OAuthClientRegistration;
|
|
4
|
+
export declare function normalizeStoredOAuthClient(value: unknown): StoredOAuthClient | null;
|
package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { normalizeOAuthScope } from "./scope.js";
|
|
2
|
+
import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
|
|
3
|
+
/** Validate and copy a bounded JSON DCR response without quoting credential input. */
|
|
4
|
+
export function parseOAuthClientRegistration(value) {
|
|
5
|
+
const invalid = () => new Error("Invalid OAuth client registration metadata");
|
|
6
|
+
let nodes = 0;
|
|
7
|
+
function copy(input, depth) {
|
|
8
|
+
if (++nodes > 20_000 || depth > 64)
|
|
9
|
+
throw invalid();
|
|
10
|
+
if (input === null || typeof input === "boolean" || typeof input === "string")
|
|
11
|
+
return input;
|
|
12
|
+
if (typeof input === "number" && Number.isFinite(input))
|
|
13
|
+
return input;
|
|
14
|
+
if (typeof input !== "object" || input === null)
|
|
15
|
+
throw invalid();
|
|
16
|
+
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
17
|
+
if (Array.isArray(input)) {
|
|
18
|
+
const length = descriptors.length?.value;
|
|
19
|
+
if (length > 20_000)
|
|
20
|
+
throw invalid();
|
|
21
|
+
const result = [];
|
|
22
|
+
for (let index = 0; index < length; index++) {
|
|
23
|
+
const descriptor = descriptors[String(index)];
|
|
24
|
+
if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
|
|
25
|
+
throw invalid();
|
|
26
|
+
result.push(copy(descriptor.value, depth + 1));
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
|
|
31
|
+
throw invalid();
|
|
32
|
+
return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key, descriptor]) => {
|
|
33
|
+
if (!Object.hasOwn(descriptor, "value"))
|
|
34
|
+
throw invalid();
|
|
35
|
+
return [key, copy(descriptor.value, depth + 1)];
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = copy(value, 0);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw invalid();
|
|
44
|
+
}
|
|
45
|
+
if (typeof result !== "object" || result === null || Array.isArray(result))
|
|
46
|
+
throw invalid();
|
|
47
|
+
const record = result;
|
|
48
|
+
if (!Object.hasOwn(record, "client_id") || typeof record.client_id !== "string" || record.client_id.trim() === "")
|
|
49
|
+
throw new Error("OAuth client registration response missing client_id");
|
|
50
|
+
for (const key of ["client_id", "client_secret", "token_endpoint_auth_method", "application_type", "client_name", "client_uri", "logo_uri", "scope",
|
|
51
|
+
"tos_uri", "policy_uri", "jwks_uri", "software_id", "software_version", "software_statement", "registration_access_token", "registration_client_uri", "issuer"]) {
|
|
52
|
+
if (Object.hasOwn(record, key) && record[key] !== null && typeof record[key] !== "string")
|
|
53
|
+
throw invalid();
|
|
54
|
+
}
|
|
55
|
+
if (typeof record.client_secret === "string" && record.client_secret.trim() === "")
|
|
56
|
+
throw invalid();
|
|
57
|
+
for (const key of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
|
|
58
|
+
const entry = record[key];
|
|
59
|
+
if (Object.hasOwn(record, key) && entry !== null && (!Array.isArray(entry) || entry.some(item => typeof item !== "string")))
|
|
60
|
+
throw invalid();
|
|
61
|
+
}
|
|
62
|
+
for (const key of ["client_id_issued_at", "client_secret_expires_at"]) {
|
|
63
|
+
const entry = record[key];
|
|
64
|
+
if (Object.hasOwn(record, key) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
|
|
65
|
+
throw invalid();
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
normalizeOAuthScope(Object.hasOwn(record, "scope") && record.scope !== null ? record.scope : undefined);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw invalid();
|
|
72
|
+
}
|
|
73
|
+
if (Buffer.byteLength(JSON.stringify(record), "utf8") > 64 * 1024)
|
|
74
|
+
throw invalid();
|
|
75
|
+
return record;
|
|
76
|
+
}
|
|
77
|
+
export function normalizeStoredOAuthClient(value) {
|
|
78
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
79
|
+
return null;
|
|
80
|
+
const record = value;
|
|
81
|
+
const clientId = Object.hasOwn(record, "clientId") ? record.clientId : undefined;
|
|
82
|
+
const clientSecret = Object.hasOwn(record, "clientSecret") ? record.clientSecret : undefined;
|
|
83
|
+
if (typeof clientId !== "string" || clientId.trim() === "" ||
|
|
84
|
+
(clientSecret !== undefined && (typeof clientSecret !== "string" || clientSecret.trim() === "")))
|
|
85
|
+
return null;
|
|
86
|
+
const client = { clientId: clientId.trim(), ...(clientSecret === undefined ? {} : { clientSecret: clientSecret.trim() }) };
|
|
87
|
+
const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record, "tokenEndpointAuthMethod") ? record.tokenEndpointAuthMethod : undefined);
|
|
88
|
+
if (Object.hasOwn(record, "registration") && record.registration !== undefined) {
|
|
89
|
+
const registration = parseOAuthClientRegistration(record.registration);
|
|
90
|
+
const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : undefined;
|
|
91
|
+
if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
|
|
92
|
+
throw new Error("OAuth client registration does not match the client identity");
|
|
93
|
+
client.registration = registration;
|
|
94
|
+
const registrationMethod = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(registration, "token_endpoint_auth_method") ? registration.token_endpoint_auth_method : undefined);
|
|
95
|
+
if (method !== undefined && registrationMethod !== undefined && method !== registrationMethod)
|
|
96
|
+
throw new Error("OAuth token endpoint authentication conflicts with the client registration");
|
|
97
|
+
if (registrationMethod !== undefined)
|
|
98
|
+
client.tokenEndpointAuthMethod = registrationMethod;
|
|
99
|
+
}
|
|
100
|
+
if (method !== undefined)
|
|
101
|
+
client.tokenEndpointAuthMethod = method;
|
|
102
|
+
return client;
|
|
103
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { normalizeStoredOAuthClient, parseOAuthClientRegistration } from "./client-registration.js";
|
|
2
|
+
import { normalizeOAuthScope } from "./scope.js";
|
|
3
|
+
import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
|
|
1
4
|
import { isIP } from "node:net";
|
|
2
5
|
import { fetchMcpResponse } from "../http-fetch.js";
|
|
3
6
|
import { URL } from "node:url";
|
|
@@ -18,6 +21,11 @@ export function createOAuthClientProvider(options) {
|
|
|
18
21
|
export function createDefaultOAuthClientProvider(options) {
|
|
19
22
|
loopbackTarget(options.browser);
|
|
20
23
|
assertPersistenceNamespace(options.persistenceNamespace);
|
|
24
|
+
const clientMetadata = getClientMetadata(options.client);
|
|
25
|
+
const requestedScope = clientMetadata?.scope;
|
|
26
|
+
const configuredClient = normalizeConfiguredClient(options.client);
|
|
27
|
+
const requestedTokenMethod = normalizeOAuthTokenEndpointAuthMethod(options.client.tokenEndpointAuthMethod);
|
|
28
|
+
const configuredTokenMethod = requestedTokenMethod ?? configuredClient?.tokenEndpointAuthMethod;
|
|
21
29
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
|
|
22
30
|
const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
|
|
23
31
|
const now = options.now ?? Date.now;
|
|
@@ -36,11 +44,13 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
36
44
|
const initialGrant = options.initialGrant === undefined ? undefined : {
|
|
37
45
|
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
38
46
|
tokens: normalizeStoredTokens(options.initialGrant.tokens),
|
|
39
|
-
client:
|
|
47
|
+
client: configuredClient
|
|
40
48
|
};
|
|
41
49
|
if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
|
|
42
50
|
throw new Error("OAuth initial grant requires valid tokens and the original client ID");
|
|
43
51
|
if (initialGrant?.tokens !== undefined) {
|
|
52
|
+
if (requestedScope !== undefined && initialGrant.tokens.scope !== requestedScope)
|
|
53
|
+
throw new Error("OAuth initial grant does not match the requested OAuth scope");
|
|
44
54
|
try {
|
|
45
55
|
new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
|
|
46
56
|
}
|
|
@@ -133,7 +143,8 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
133
143
|
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
134
144
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
135
145
|
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
136
|
-
client: initialGrant.client, tokens: initialGrant.tokens,
|
|
146
|
+
client: initialGrant.client, tokens: initialGrant.tokens,
|
|
147
|
+
...(requestedScope === undefined ? {} : { requestedScope }), discovery: toStoredDiscovery(discovery) };
|
|
137
148
|
await saveSession(canonicalResource, session);
|
|
138
149
|
initialGrantConsumed = true;
|
|
139
150
|
signal?.throwIfAborted();
|
|
@@ -141,11 +152,16 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
141
152
|
if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
142
153
|
forceRefresh = false;
|
|
143
154
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
144
|
-
if ((options.client.mode === "static" || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
|
|
145
|
-
const configured =
|
|
155
|
+
if ((options.client.mode === "static" || configuredClient?.registration !== undefined || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
|
|
156
|
+
const configured = configuredClient;
|
|
146
157
|
if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
|
|
147
158
|
throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
|
|
148
159
|
}
|
|
160
|
+
if (requestedScope !== undefined && session?.tokens !== undefined && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
|
|
161
|
+
throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
|
|
162
|
+
if (configuredTokenMethod !== undefined && session !== null && (session.tokens !== undefined || session.refreshState === "pending") &&
|
|
163
|
+
(session.client.tokenEndpointAuthMethod ?? (session.client.clientSecret === undefined ? "none" : "client_secret_post")) !== configuredTokenMethod)
|
|
164
|
+
throw new Error("Stored session does not match the requested OAuth token endpoint authentication; select separate persistence or reset it");
|
|
149
165
|
if (session?.refreshState === "pending") {
|
|
150
166
|
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
|
|
151
167
|
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
@@ -180,6 +196,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
180
196
|
if (session.tokens?.refreshToken === undefined) {
|
|
181
197
|
return session;
|
|
182
198
|
}
|
|
199
|
+
assertTokenEndpointAuthentication(session.client, discovery.authorizationServerMetadata);
|
|
183
200
|
const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
|
|
184
201
|
await saveSession(resource, pendingSession);
|
|
185
202
|
signal?.throwIfAborted();
|
|
@@ -191,6 +208,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
191
208
|
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
192
209
|
clientId: session.client.clientId,
|
|
193
210
|
clientSecret: session.client.clientSecret,
|
|
211
|
+
tokenEndpointAuthMethod: session.client.tokenEndpointAuthMethod,
|
|
194
212
|
refreshToken: session.tokens.refreshToken,
|
|
195
213
|
resource,
|
|
196
214
|
fetch, signal,
|
|
@@ -226,10 +244,13 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
226
244
|
...session,
|
|
227
245
|
tokens: {
|
|
228
246
|
...refreshedTokens,
|
|
229
|
-
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
247
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
|
|
248
|
+
scope: refreshedTokens.scope ?? session.tokens.scope
|
|
230
249
|
},
|
|
231
250
|
discovery: toStoredDiscovery(discovery)
|
|
232
251
|
};
|
|
252
|
+
if (requestedScope !== undefined && normalizeOAuthScope(updatedSession.tokens?.scope ?? session.requestedScope) !== requestedScope)
|
|
253
|
+
throw new Error("OAuth refresh response does not match the requested OAuth scope; authorize again");
|
|
233
254
|
await saveSession(resource, updatedSession);
|
|
234
255
|
return updatedSession;
|
|
235
256
|
}
|
|
@@ -253,10 +274,12 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
253
274
|
let resolvedClient = null;
|
|
254
275
|
try {
|
|
255
276
|
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch, signal);
|
|
277
|
+
assertTokenEndpointAuthentication(resolvedClient.client, discovery.authorizationServerMetadata);
|
|
256
278
|
const sessionWithoutTokens = {
|
|
257
279
|
resource,
|
|
258
280
|
authorizationServer: discovery.authorizationServer,
|
|
259
281
|
client: resolvedClient.client,
|
|
282
|
+
...(requestedScope === undefined ? {} : { requestedScope }),
|
|
260
283
|
discovery: toStoredDiscovery(discovery)
|
|
261
284
|
};
|
|
262
285
|
await saveSession(resource, sessionWithoutTokens);
|
|
@@ -268,13 +291,14 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
268
291
|
clientId: resolvedClient.client.clientId,
|
|
269
292
|
redirectUri: loopback.redirectUri,
|
|
270
293
|
codeChallenge: challenge,
|
|
271
|
-
clientMetadata
|
|
294
|
+
clientMetadata
|
|
272
295
|
});
|
|
273
296
|
const code = await loopback.waitForCode(authorizationUrl);
|
|
274
297
|
const tokens = await exchangeAuthorizationCode({
|
|
275
298
|
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
276
299
|
clientId: resolvedClient.client.clientId,
|
|
277
300
|
clientSecret: resolvedClient.client.clientSecret,
|
|
301
|
+
tokenEndpointAuthMethod: resolvedClient.client.tokenEndpointAuthMethod,
|
|
278
302
|
code,
|
|
279
303
|
codeVerifier: verifier,
|
|
280
304
|
redirectUri: loopback.redirectUri,
|
|
@@ -282,6 +306,8 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
282
306
|
fetch, signal,
|
|
283
307
|
now
|
|
284
308
|
});
|
|
309
|
+
if (requestedScope !== undefined && tokens.scope !== undefined && normalizeOAuthScope(tokens.scope) !== requestedScope)
|
|
310
|
+
throw new Error("OAuth authorization response does not match the requested OAuth scope");
|
|
285
311
|
const session = {
|
|
286
312
|
...sessionWithoutTokens,
|
|
287
313
|
tokens
|
|
@@ -313,8 +339,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
313
339
|
}
|
|
314
340
|
async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
|
|
315
341
|
parentSignal?.throwIfAborted();
|
|
316
|
-
|
|
317
|
-
if (options.client.mode === "static") {
|
|
342
|
+
if (options.client.mode === "static" || configuredClient?.registration !== undefined) {
|
|
318
343
|
if (configuredClient === null) {
|
|
319
344
|
throw new Error("OAuth client_id must not be blank");
|
|
320
345
|
}
|
|
@@ -363,7 +388,12 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
363
388
|
};
|
|
364
389
|
}
|
|
365
390
|
}
|
|
366
|
-
const
|
|
391
|
+
const supported = getSupportedTokenAuthMethods(discovery.authorizationServerMetadata);
|
|
392
|
+
const registrationMethod = requestedTokenMethod ?? (supported === undefined ? "none" :
|
|
393
|
+
["none", "client_secret_basic", "client_secret_post"].find(method => supported.includes(method)));
|
|
394
|
+
if (registrationMethod === undefined || (supported !== undefined && !supported.includes(registrationMethod)))
|
|
395
|
+
throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
|
|
396
|
+
const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri, registrationMethod);
|
|
367
397
|
const deadline = AbortSignal.timeout(30_000);
|
|
368
398
|
const signal = parentSignal === undefined ? deadline : AbortSignal.any([parentSignal, deadline]);
|
|
369
399
|
const response = await fetchMcpResponse(fetch, registrationEndpoint, {
|
|
@@ -375,16 +405,15 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
375
405
|
signal
|
|
376
406
|
});
|
|
377
407
|
const payload = await readOAuthJsonObjectResponse(response, signal);
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const clientSecret = getOwnString(payload, "client_secret");
|
|
408
|
+
const registration = parseOAuthClientRegistration(payload);
|
|
409
|
+
const registeredSecret = getOwnString(registration, "client_secret");
|
|
410
|
+
const responseMethod = normalizeOAuthTokenEndpointAuthMethod(getOwnEntry(registration, "token_endpoint_auth_method")) ??
|
|
411
|
+
requestedTokenMethod ?? (supported === undefined ? undefined : normalizeOAuthTokenEndpointAuthMethod(registrationMethod));
|
|
383
412
|
const registeredClient = {
|
|
384
|
-
clientId:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
413
|
+
clientId: registration.client_id.trim(),
|
|
414
|
+
...(registeredSecret === undefined ? {} : { clientSecret: registeredSecret.trim() }),
|
|
415
|
+
...(responseMethod === undefined ? {} : { tokenEndpointAuthMethod: responseMethod }),
|
|
416
|
+
registration
|
|
388
417
|
};
|
|
389
418
|
await saveRegisteredClient(discovery.authorizationServer, registeredClient);
|
|
390
419
|
return {
|
|
@@ -397,7 +426,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
397
426
|
return normalizeLoadedSession(await sessionStore.load(resource));
|
|
398
427
|
}
|
|
399
428
|
async function saveSession(resource, session) {
|
|
400
|
-
await sessionStore.save(resource, session);
|
|
429
|
+
await sessionStore.save(resource, structuredClone(session));
|
|
401
430
|
}
|
|
402
431
|
async function clearSession(resource) {
|
|
403
432
|
await sessionStore.clear(resource);
|
|
@@ -410,7 +439,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
410
439
|
return null;
|
|
411
440
|
}
|
|
412
441
|
const client = await clientStore.load(issuer);
|
|
413
|
-
const normalizedClient = client === null ? null :
|
|
442
|
+
const normalizedClient = client === null ? null : normalizeStoredOAuthClient(client);
|
|
414
443
|
if (client !== null && normalizedClient === null) {
|
|
415
444
|
await clientStore.clear(issuer);
|
|
416
445
|
return null;
|
|
@@ -489,7 +518,7 @@ function normalizeLoadedSession(session) {
|
|
|
489
518
|
const refreshState = getOwnEntry(session, "refreshState");
|
|
490
519
|
if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
|
|
491
520
|
throw new Error("Stored OAuth refresh state is invalid");
|
|
492
|
-
const client =
|
|
521
|
+
const client = normalizeStoredOAuthClient(getOwnEntry(session, "client"));
|
|
493
522
|
if (client === null) {
|
|
494
523
|
return { ...session, client: { clientId: "" }, tokens: undefined };
|
|
495
524
|
}
|
|
@@ -499,25 +528,6 @@ function normalizeLoadedSession(session) {
|
|
|
499
528
|
tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
|
|
500
529
|
};
|
|
501
530
|
}
|
|
502
|
-
function normalizeStoredClient(value) {
|
|
503
|
-
if (!isObjectRecord(value)) {
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
const clientId = getOwnString(value, "clientId");
|
|
507
|
-
if (clientId === undefined || clientId.trim().length === 0) {
|
|
508
|
-
return null;
|
|
509
|
-
}
|
|
510
|
-
const normalizedClientId = clientId.trim();
|
|
511
|
-
const clientSecret = getOwnEntry(value, "clientSecret");
|
|
512
|
-
if (clientSecret === undefined) {
|
|
513
|
-
return { clientId: normalizedClientId };
|
|
514
|
-
}
|
|
515
|
-
if (typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
const normalizedClientSecret = clientSecret.trim();
|
|
519
|
-
return { clientId: normalizedClientId, clientSecret: normalizedClientSecret };
|
|
520
|
-
}
|
|
521
531
|
function normalizeStoredTokens(value) {
|
|
522
532
|
if (value === undefined || !isObjectRecord(value)) {
|
|
523
533
|
return undefined;
|
|
@@ -526,10 +536,10 @@ function normalizeStoredTokens(value) {
|
|
|
526
536
|
const tokenType = getOwnString(value, "tokenType");
|
|
527
537
|
const expiresAt = getOwnEntry(value, "expiresAt");
|
|
528
538
|
const refreshToken = getOwnEntry(value, "refreshToken");
|
|
529
|
-
const scope =
|
|
539
|
+
const scope = getOwnEntry(value, "scope");
|
|
530
540
|
const normalizedAccessToken = accessToken?.trim();
|
|
531
541
|
const normalizedRefreshToken = typeof refreshToken === "string" ? refreshToken.trim() : undefined;
|
|
532
|
-
const normalizedScope = scope
|
|
542
|
+
const normalizedScope = normalizeOAuthScope(scope);
|
|
533
543
|
if (accessToken === undefined ||
|
|
534
544
|
normalizedAccessToken === undefined ||
|
|
535
545
|
normalizedAccessToken.length === 0 ||
|
|
@@ -561,18 +571,18 @@ function getClientMetadata(client) {
|
|
|
561
571
|
}
|
|
562
572
|
return {
|
|
563
573
|
clientName: normalizeOptionalOAuthString(client.metadata.clientName),
|
|
564
|
-
scope:
|
|
574
|
+
scope: normalizeOAuthScope(client.metadata.scope),
|
|
565
575
|
softwareId: normalizeOptionalOAuthString(client.metadata.softwareId),
|
|
566
576
|
softwareVersion: normalizeOptionalOAuthString(client.metadata.softwareVersion)
|
|
567
577
|
};
|
|
568
578
|
}
|
|
569
579
|
function normalizeConfiguredClient(client) {
|
|
570
|
-
const
|
|
571
|
-
|
|
580
|
+
const registration = client.registration === undefined ? undefined : parseOAuthClientRegistration(client.registration);
|
|
581
|
+
const clientId = normalizeOptionalOAuthString(client.clientId) ?? registration?.client_id.trim();
|
|
582
|
+
if (clientId === undefined)
|
|
572
583
|
return null;
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return clientSecret === undefined ? { clientId } : { clientId, clientSecret };
|
|
584
|
+
const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === undefined ? undefined : getOwnString(registration, "client_secret")?.trim());
|
|
585
|
+
return normalizeStoredOAuthClient({ clientId, clientSecret, registration, tokenEndpointAuthMethod: client.tokenEndpointAuthMethod });
|
|
576
586
|
}
|
|
577
587
|
function normalizeOptionalOAuthString(value) {
|
|
578
588
|
if (value === undefined) {
|
|
@@ -679,12 +689,28 @@ function assertRequestMatchesResource(requestUrl, resource) {
|
|
|
679
689
|
throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
|
|
680
690
|
}
|
|
681
691
|
}
|
|
682
|
-
function
|
|
692
|
+
function getSupportedTokenAuthMethods(metadata) {
|
|
693
|
+
const value = getOwnEntry(metadata, "token_endpoint_auth_methods_supported");
|
|
694
|
+
if (value === undefined)
|
|
695
|
+
return undefined;
|
|
696
|
+
if (!Array.isArray(value) || value.length > 128 || value.some(method => typeof method !== "string"))
|
|
697
|
+
throw new Error("Invalid OAuth token endpoint authentication metadata");
|
|
698
|
+
return value;
|
|
699
|
+
}
|
|
700
|
+
function assertTokenEndpointAuthentication(client, metadata) {
|
|
701
|
+
const method = client.tokenEndpointAuthMethod ?? (client.clientSecret === undefined ? "none" : "client_secret_post");
|
|
702
|
+
if (method !== "none" && client.clientSecret === undefined)
|
|
703
|
+
throw new Error("OAuth token endpoint authentication requires a client secret");
|
|
704
|
+
const supported = getSupportedTokenAuthMethods(metadata);
|
|
705
|
+
if (supported !== undefined && !supported.includes(method))
|
|
706
|
+
throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
|
|
707
|
+
}
|
|
708
|
+
function buildClientRegistrationBody(metadata, redirectUri, tokenEndpointAuthMethod) {
|
|
683
709
|
const body = {
|
|
684
710
|
redirect_uris: [redirectUri],
|
|
685
711
|
grant_types: ["authorization_code", "refresh_token"],
|
|
686
712
|
response_types: ["code"],
|
|
687
|
-
token_endpoint_auth_method:
|
|
713
|
+
token_endpoint_auth_method: tokenEndpointAuthMethod
|
|
688
714
|
};
|
|
689
715
|
const clientName = metadata === undefined ? undefined : getOwnString(metadata, "clientName");
|
|
690
716
|
const scope = metadata === undefined ? undefined : getOwnString(metadata, "scope");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Compare OAuth scope sets without accepting controls or changing token case. */
|
|
2
|
+
export function normalizeOAuthScope(scope) {
|
|
3
|
+
if (scope === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
if (typeof scope !== "string" || [...scope].some(char => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
|
|
6
|
+
throw new Error("Invalid OAuth scope syntax");
|
|
7
|
+
const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
|
|
8
|
+
return normalized || undefined;
|
|
9
|
+
}
|
package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-auth-method.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function normalizeOAuthTokenEndpointAuthMethod(value) {
|
|
2
|
+
if (value === undefined || value === null)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (value !== "none" && value !== "client_secret_post" && value !== "client_secret_basic")
|
|
5
|
+
throw new Error("Unsupported OAuth token endpoint authentication method");
|
|
6
|
+
return value;
|
|
7
|
+
}
|
package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OAuthMetadataFetch, StoredOAuthTokens } from "./types.js";
|
|
1
|
+
import type { OAuthMetadataFetch, StoredOAuthTokens, OAuthTokenEndpointAuthMethod } from "./types.js";
|
|
2
2
|
interface OAuthErrorShape {
|
|
3
3
|
error: string;
|
|
4
4
|
error_description?: string;
|
|
@@ -22,6 +22,7 @@ export declare function exchangeAuthorizationCode(input: {
|
|
|
22
22
|
tokenEndpoint: string;
|
|
23
23
|
clientId: string;
|
|
24
24
|
clientSecret?: string;
|
|
25
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
25
26
|
code: string;
|
|
26
27
|
codeVerifier: string;
|
|
27
28
|
redirectUri: string;
|
|
@@ -34,6 +35,7 @@ export declare function refreshAccessToken(input: {
|
|
|
34
35
|
tokenEndpoint: string;
|
|
35
36
|
clientId: string;
|
|
36
37
|
clientSecret?: string;
|
|
38
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
37
39
|
refreshToken: string;
|
|
38
40
|
resource: string;
|
|
39
41
|
fetch: OAuthMetadataFetch;
|