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.
Files changed (24) hide show
  1. package/composition.json +3 -3
  2. package/dist/composition.json +3 -3
  3. package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
  4. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/README.md +25 -1
  5. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +1 -6
  6. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +7 -18
  7. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.d.ts +4 -0
  8. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.js +103 -0
  9. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +76 -50
  10. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/scope.d.ts +2 -0
  11. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/scope.js +9 -0
  12. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-auth-method.d.ts +2 -0
  13. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-auth-method.js +7 -0
  14. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +3 -1
  15. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-endpoint.js +23 -10
  16. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +27 -4
  17. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.d.ts +2 -1
  18. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.js +1 -0
  19. package/node_modules/tiny-http-mcp-server/package.json +1 -1
  20. package/node_modules/tiny-mcp-client/dist/index.d.ts +27 -4
  21. package/node_modules/tiny-mcp-client/dist/index.js +226 -73
  22. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  23. package/node_modules/toolcraft-schema/package.json +1 -1
  24. 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.31",
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.313",
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.313",
126
+ "version": "0.0.315",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.31",
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.313",
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.313",
126
+ "version": "0.0.315",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.31",
21
+ "version": "0.1.34",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -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
- const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : undefined;
66
- if (clientId !== undefined) {
67
- const client = { clientId };
68
- if (isObjectRecord(parsed) &&
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
- isStoredOAuthClient(getOwnEntry(value, "client")) &&
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;
@@ -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: normalizeConfiguredClient(options.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, discovery: toStoredDiscovery(discovery) };
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 = normalizeConfiguredClient(options.client);
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: getClientMetadata(options.client)
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
- const configuredClient = normalizeConfiguredClient(options.client);
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 registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
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 clientId = getOwnString(payload, "client_id");
379
- if (clientId === undefined || clientId.trim().length === 0) {
380
- throw new Error("OAuth client registration response missing client_id");
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: clientId.trim(),
385
- clientSecret: clientSecret !== undefined && clientSecret.trim().length > 0
386
- ? clientSecret.trim()
387
- : undefined
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 : normalizeStoredClient(client);
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 = normalizeStoredClient(getOwnEntry(session, "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 = getOwnString(value, "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?.trim();
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: normalizeOptionalOAuthString(client.metadata.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 clientId = normalizeOptionalOAuthString(client.clientId);
571
- if (clientId === undefined) {
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
- const clientSecret = normalizeOptionalOAuthString(client.clientSecret);
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 buildClientRegistrationBody(metadata, redirectUri) {
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: "none"
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,2 @@
1
+ /** Compare OAuth scope sets without accepting controls or changing token case. */
2
+ export declare function normalizeOAuthScope(scope: unknown): string | undefined;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { OAuthTokenEndpointAuthMethod } from "./types.js";
2
+ export declare function normalizeOAuthTokenEndpointAuthMethod(value: unknown): OAuthTokenEndpointAuthMethod | undefined;
@@ -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
+ }
@@ -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;