tiny-http-mcp-server 0.1.31 → 0.1.33

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.
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.31",
21
+ "version": "0.1.33",
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,13 @@ 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.
108
122
  Static clients and dynamic initial-grant imports require cached grants to match
109
123
  the original normalized client ID and secret. A different client configuration
110
124
  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,94 @@
1
+ import { normalizeOAuthScope } from "./scope.js";
2
+ /** Validate and copy a bounded JSON DCR response without quoting credential input. */
3
+ export function parseOAuthClientRegistration(value) {
4
+ const invalid = () => new Error("Invalid OAuth client registration metadata");
5
+ let nodes = 0;
6
+ function copy(input, depth) {
7
+ if (++nodes > 20_000 || depth > 64)
8
+ throw invalid();
9
+ if (input === null || typeof input === "boolean" || typeof input === "string")
10
+ return input;
11
+ if (typeof input === "number" && Number.isFinite(input))
12
+ return input;
13
+ if (typeof input !== "object" || input === null)
14
+ throw invalid();
15
+ const descriptors = Object.getOwnPropertyDescriptors(input);
16
+ if (Array.isArray(input)) {
17
+ const length = descriptors.length?.value;
18
+ if (length > 20_000)
19
+ throw invalid();
20
+ const result = [];
21
+ for (let index = 0; index < length; index++) {
22
+ const descriptor = descriptors[String(index)];
23
+ if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
24
+ throw invalid();
25
+ result.push(copy(descriptor.value, depth + 1));
26
+ }
27
+ return result;
28
+ }
29
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
30
+ throw invalid();
31
+ return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key, descriptor]) => {
32
+ if (!Object.hasOwn(descriptor, "value"))
33
+ throw invalid();
34
+ return [key, copy(descriptor.value, depth + 1)];
35
+ }));
36
+ }
37
+ let result;
38
+ try {
39
+ result = copy(value, 0);
40
+ }
41
+ catch {
42
+ throw invalid();
43
+ }
44
+ if (typeof result !== "object" || result === null || Array.isArray(result))
45
+ throw invalid();
46
+ const record = result;
47
+ if (!Object.hasOwn(record, "client_id") || typeof record.client_id !== "string" || record.client_id.trim() === "")
48
+ throw new Error("OAuth client registration response missing client_id");
49
+ for (const key of ["client_id", "client_secret", "token_endpoint_auth_method", "application_type", "client_name", "client_uri", "logo_uri", "scope",
50
+ "tos_uri", "policy_uri", "jwks_uri", "software_id", "software_version", "software_statement", "registration_access_token", "registration_client_uri", "issuer"]) {
51
+ if (Object.hasOwn(record, key) && record[key] !== null && typeof record[key] !== "string")
52
+ throw invalid();
53
+ }
54
+ if (typeof record.client_secret === "string" && record.client_secret.trim() === "")
55
+ throw invalid();
56
+ for (const key of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
57
+ const entry = record[key];
58
+ if (Object.hasOwn(record, key) && entry !== null && (!Array.isArray(entry) || entry.some(item => typeof item !== "string")))
59
+ throw invalid();
60
+ }
61
+ for (const key of ["client_id_issued_at", "client_secret_expires_at"]) {
62
+ const entry = record[key];
63
+ if (Object.hasOwn(record, key) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
64
+ throw invalid();
65
+ }
66
+ try {
67
+ normalizeOAuthScope(Object.hasOwn(record, "scope") && record.scope !== null ? record.scope : undefined);
68
+ }
69
+ catch {
70
+ throw invalid();
71
+ }
72
+ if (Buffer.byteLength(JSON.stringify(record), "utf8") > 64 * 1024)
73
+ throw invalid();
74
+ return record;
75
+ }
76
+ export function normalizeStoredOAuthClient(value) {
77
+ if (typeof value !== "object" || value === null || Array.isArray(value))
78
+ return null;
79
+ const record = value;
80
+ const clientId = Object.hasOwn(record, "clientId") ? record.clientId : undefined;
81
+ const clientSecret = Object.hasOwn(record, "clientSecret") ? record.clientSecret : undefined;
82
+ if (typeof clientId !== "string" || clientId.trim() === "" ||
83
+ (clientSecret !== undefined && (typeof clientSecret !== "string" || clientSecret.trim() === "")))
84
+ return null;
85
+ const client = { clientId: clientId.trim(), ...(clientSecret === undefined ? {} : { clientSecret: clientSecret.trim() }) };
86
+ if (Object.hasOwn(record, "registration") && record.registration !== undefined) {
87
+ const registration = parseOAuthClientRegistration(record.registration);
88
+ const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : undefined;
89
+ if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
90
+ throw new Error("OAuth client registration does not match the client identity");
91
+ client.registration = registration;
92
+ }
93
+ return client;
94
+ }
@@ -1,3 +1,5 @@
1
+ import { normalizeStoredOAuthClient, parseOAuthClientRegistration } from "./client-registration.js";
2
+ import { normalizeOAuthScope } from "./scope.js";
1
3
  import { isIP } from "node:net";
2
4
  import { fetchMcpResponse } from "../http-fetch.js";
3
5
  import { URL } from "node:url";
@@ -18,6 +20,9 @@ export function createOAuthClientProvider(options) {
18
20
  export function createDefaultOAuthClientProvider(options) {
19
21
  loopbackTarget(options.browser);
20
22
  assertPersistenceNamespace(options.persistenceNamespace);
23
+ const clientMetadata = getClientMetadata(options.client);
24
+ const requestedScope = clientMetadata?.scope;
25
+ const configuredClient = normalizeConfiguredClient(options.client);
21
26
  const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
22
27
  const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
23
28
  const now = options.now ?? Date.now;
@@ -36,11 +41,13 @@ export function createDefaultOAuthClientProvider(options) {
36
41
  const initialGrant = options.initialGrant === undefined ? undefined : {
37
42
  resource: canonicalizeResourceIndicator(options.initialGrant.resource),
38
43
  tokens: normalizeStoredTokens(options.initialGrant.tokens),
39
- client: normalizeConfiguredClient(options.client)
44
+ client: configuredClient
40
45
  };
41
46
  if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
42
47
  throw new Error("OAuth initial grant requires valid tokens and the original client ID");
43
48
  if (initialGrant?.tokens !== undefined) {
49
+ if (requestedScope !== undefined && initialGrant.tokens.scope !== requestedScope)
50
+ throw new Error("OAuth initial grant does not match the requested OAuth scope");
44
51
  try {
45
52
  new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
46
53
  }
@@ -133,7 +140,8 @@ export function createDefaultOAuthClientProvider(options) {
133
140
  initialGrant.tokens !== undefined && initialGrant.client !== null) {
134
141
  assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
135
142
  session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
136
- client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
143
+ client: initialGrant.client, tokens: initialGrant.tokens,
144
+ ...(requestedScope === undefined ? {} : { requestedScope }), discovery: toStoredDiscovery(discovery) };
137
145
  await saveSession(canonicalResource, session);
138
146
  initialGrantConsumed = true;
139
147
  signal?.throwIfAborted();
@@ -141,11 +149,13 @@ export function createDefaultOAuthClientProvider(options) {
141
149
  if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
142
150
  forceRefresh = false;
143
151
  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);
152
+ if ((options.client.mode === "static" || configuredClient?.registration !== undefined || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
153
+ const configured = configuredClient;
146
154
  if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
147
155
  throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
148
156
  }
157
+ if (requestedScope !== undefined && session?.tokens !== undefined && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
158
+ throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
149
159
  if (session?.refreshState === "pending") {
150
160
  if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
151
161
  throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
@@ -226,10 +236,13 @@ export function createDefaultOAuthClientProvider(options) {
226
236
  ...session,
227
237
  tokens: {
228
238
  ...refreshedTokens,
229
- refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
239
+ refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
240
+ scope: refreshedTokens.scope ?? session.tokens.scope
230
241
  },
231
242
  discovery: toStoredDiscovery(discovery)
232
243
  };
244
+ if (requestedScope !== undefined && normalizeOAuthScope(updatedSession.tokens?.scope ?? session.requestedScope) !== requestedScope)
245
+ throw new Error("OAuth refresh response does not match the requested OAuth scope; authorize again");
233
246
  await saveSession(resource, updatedSession);
234
247
  return updatedSession;
235
248
  }
@@ -257,6 +270,7 @@ export function createDefaultOAuthClientProvider(options) {
257
270
  resource,
258
271
  authorizationServer: discovery.authorizationServer,
259
272
  client: resolvedClient.client,
273
+ ...(requestedScope === undefined ? {} : { requestedScope }),
260
274
  discovery: toStoredDiscovery(discovery)
261
275
  };
262
276
  await saveSession(resource, sessionWithoutTokens);
@@ -268,7 +282,7 @@ export function createDefaultOAuthClientProvider(options) {
268
282
  clientId: resolvedClient.client.clientId,
269
283
  redirectUri: loopback.redirectUri,
270
284
  codeChallenge: challenge,
271
- clientMetadata: getClientMetadata(options.client)
285
+ clientMetadata
272
286
  });
273
287
  const code = await loopback.waitForCode(authorizationUrl);
274
288
  const tokens = await exchangeAuthorizationCode({
@@ -282,6 +296,8 @@ export function createDefaultOAuthClientProvider(options) {
282
296
  fetch, signal,
283
297
  now
284
298
  });
299
+ if (requestedScope !== undefined && tokens.scope !== undefined && normalizeOAuthScope(tokens.scope) !== requestedScope)
300
+ throw new Error("OAuth authorization response does not match the requested OAuth scope");
285
301
  const session = {
286
302
  ...sessionWithoutTokens,
287
303
  tokens
@@ -313,8 +329,7 @@ export function createDefaultOAuthClientProvider(options) {
313
329
  }
314
330
  async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
315
331
  parentSignal?.throwIfAborted();
316
- const configuredClient = normalizeConfiguredClient(options.client);
317
- if (options.client.mode === "static") {
332
+ if (options.client.mode === "static" || configuredClient?.registration !== undefined) {
318
333
  if (configuredClient === null) {
319
334
  throw new Error("OAuth client_id must not be blank");
320
335
  }
@@ -363,7 +378,7 @@ export function createDefaultOAuthClientProvider(options) {
363
378
  };
364
379
  }
365
380
  }
366
- const registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
381
+ const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
367
382
  const deadline = AbortSignal.timeout(30_000);
368
383
  const signal = parentSignal === undefined ? deadline : AbortSignal.any([parentSignal, deadline]);
369
384
  const response = await fetchMcpResponse(fetch, registrationEndpoint, {
@@ -375,16 +390,12 @@ export function createDefaultOAuthClientProvider(options) {
375
390
  signal
376
391
  });
377
392
  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");
393
+ const registration = parseOAuthClientRegistration(payload);
394
+ const registeredSecret = getOwnString(registration, "client_secret");
383
395
  const registeredClient = {
384
- clientId: clientId.trim(),
385
- clientSecret: clientSecret !== undefined && clientSecret.trim().length > 0
386
- ? clientSecret.trim()
387
- : undefined
396
+ clientId: registration.client_id.trim(),
397
+ ...(registeredSecret === undefined ? {} : { clientSecret: registeredSecret.trim() }),
398
+ registration
388
399
  };
389
400
  await saveRegisteredClient(discovery.authorizationServer, registeredClient);
390
401
  return {
@@ -410,7 +421,7 @@ export function createDefaultOAuthClientProvider(options) {
410
421
  return null;
411
422
  }
412
423
  const client = await clientStore.load(issuer);
413
- const normalizedClient = client === null ? null : normalizeStoredClient(client);
424
+ const normalizedClient = client === null ? null : normalizeStoredOAuthClient(client);
414
425
  if (client !== null && normalizedClient === null) {
415
426
  await clientStore.clear(issuer);
416
427
  return null;
@@ -489,7 +500,7 @@ function normalizeLoadedSession(session) {
489
500
  const refreshState = getOwnEntry(session, "refreshState");
490
501
  if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
491
502
  throw new Error("Stored OAuth refresh state is invalid");
492
- const client = normalizeStoredClient(getOwnEntry(session, "client"));
503
+ const client = normalizeStoredOAuthClient(getOwnEntry(session, "client"));
493
504
  if (client === null) {
494
505
  return { ...session, client: { clientId: "" }, tokens: undefined };
495
506
  }
@@ -499,25 +510,6 @@ function normalizeLoadedSession(session) {
499
510
  tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
500
511
  };
501
512
  }
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
513
  function normalizeStoredTokens(value) {
522
514
  if (value === undefined || !isObjectRecord(value)) {
523
515
  return undefined;
@@ -526,10 +518,10 @@ function normalizeStoredTokens(value) {
526
518
  const tokenType = getOwnString(value, "tokenType");
527
519
  const expiresAt = getOwnEntry(value, "expiresAt");
528
520
  const refreshToken = getOwnEntry(value, "refreshToken");
529
- const scope = getOwnString(value, "scope");
521
+ const scope = getOwnEntry(value, "scope");
530
522
  const normalizedAccessToken = accessToken?.trim();
531
523
  const normalizedRefreshToken = typeof refreshToken === "string" ? refreshToken.trim() : undefined;
532
- const normalizedScope = scope?.trim();
524
+ const normalizedScope = normalizeOAuthScope(scope);
533
525
  if (accessToken === undefined ||
534
526
  normalizedAccessToken === undefined ||
535
527
  normalizedAccessToken.length === 0 ||
@@ -561,18 +553,18 @@ function getClientMetadata(client) {
561
553
  }
562
554
  return {
563
555
  clientName: normalizeOptionalOAuthString(client.metadata.clientName),
564
- scope: normalizeOptionalOAuthString(client.metadata.scope),
556
+ scope: normalizeOAuthScope(client.metadata.scope),
565
557
  softwareId: normalizeOptionalOAuthString(client.metadata.softwareId),
566
558
  softwareVersion: normalizeOptionalOAuthString(client.metadata.softwareVersion)
567
559
  };
568
560
  }
569
561
  function normalizeConfiguredClient(client) {
570
- const clientId = normalizeOptionalOAuthString(client.clientId);
571
- if (clientId === undefined) {
562
+ const registration = client.registration === undefined ? undefined : parseOAuthClientRegistration(client.registration);
563
+ const clientId = normalizeOptionalOAuthString(client.clientId) ?? registration?.client_id.trim();
564
+ if (clientId === undefined)
572
565
  return null;
573
- }
574
- const clientSecret = normalizeOptionalOAuthString(client.clientSecret);
575
- return clientSecret === undefined ? { clientId } : { clientId, clientSecret };
566
+ const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === undefined ? undefined : getOwnString(registration, "client_secret")?.trim());
567
+ return normalizeStoredOAuthClient({ clientId, clientSecret, registration });
576
568
  }
577
569
  function normalizeOptionalOAuthString(value) {
578
570
  if (value === undefined) {
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import { canonicalizeResourceIndicator } from "../resource-indicator.js";
2
2
  import { readBoundedResponseText } from "../http-response.js";
3
3
  import { fetchMcpResponse } from "../http-fetch.js";
4
+ import { normalizeOAuthScope } from "./scope.js";
4
5
  const MAX_JS_DATE_MS = 8_640_000_000_000_000;
5
6
  export class OAuthError extends Error {
6
7
  error;
@@ -117,7 +118,9 @@ async function requestTokens(input) {
117
118
  const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0
118
119
  ? refreshToken.trim()
119
120
  : undefined;
120
- const normalizedScope = typeof scope === "string" && scope.trim().length > 0 ? scope.trim() : undefined;
121
+ const normalizedScope = normalizeOAuthScope(scope);
122
+ if (scope !== undefined && normalizedScope === undefined)
123
+ throw new Error("Invalid OAuth scope syntax in token response");
121
124
  return {
122
125
  accessToken: normalizedAccessToken,
123
126
  refreshToken: normalizedRefreshToken === undefined ? undefined : normalizedRefreshToken,
@@ -70,16 +70,32 @@ export interface StoredOAuthTokens {
70
70
  expiresAt: number | null;
71
71
  scope?: string;
72
72
  }
73
+ /** Full RFC 7591 response, including JSON provider extensions. */
74
+ export interface OAuthClientRegistration extends Record<string, unknown> {
75
+ client_id: string;
76
+ client_secret?: string | null;
77
+ redirect_uris?: string[] | null;
78
+ grant_types?: string[] | null;
79
+ response_types?: string[] | null;
80
+ contacts?: string[] | null;
81
+ client_id_issued_at?: number | null;
82
+ client_secret_expires_at?: number | null;
83
+ token_endpoint_auth_method?: string | null;
84
+ }
85
+ export interface StoredOAuthClient {
86
+ clientId: string;
87
+ clientSecret?: string;
88
+ registration?: OAuthClientRegistration;
89
+ }
73
90
  export interface StoredOAuthSession {
74
91
  resource: string;
75
92
  authorizationServer: string;
76
- client: {
77
- clientId: string;
78
- clientSecret?: string;
79
- };
93
+ client: StoredOAuthClient;
80
94
  tokens?: StoredOAuthTokens;
81
95
  /** A refresh was begun; its winning response may not have been persisted. */
82
96
  refreshState?: "pending";
97
+ /** Canonical explicitly requested scope set when the server omits token scope. */
98
+ requestedScope?: string;
83
99
  discovery: {
84
100
  resourceMetadataUrl: string;
85
101
  resourceMetadata: Record<string, unknown>;
@@ -102,11 +118,14 @@ export interface DefaultOAuthClientProviderOptions {
102
118
  clientId?: string;
103
119
  clientSecret?: string;
104
120
  metadata?: OAuthClientMetadata;
121
+ /** Import a complete registration owned by the caller. */
122
+ registration?: OAuthClientRegistration;
105
123
  } | {
106
124
  mode: "static";
107
125
  clientId: string;
108
126
  clientSecret?: string;
109
127
  metadata?: OAuthClientMetadata;
128
+ registration?: OAuthClientRegistration;
110
129
  };
111
130
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
112
131
  allowInteractive?: boolean;
@@ -1,11 +1,12 @@
1
1
  export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
2
+ export { parseOAuthClientRegistration } from "./client/client-registration.js";
2
3
  export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
3
4
  export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
4
5
  export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
5
6
  export { OAuthError, } from "./client/token-endpoint.js";
6
7
  export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
7
8
  export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
8
- export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, StoredOAuthSession, StoredOAuthTokens, } from "./client/types.js";
9
+ export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, } from "./client/types.js";
9
10
  export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessToken, } from "./server/jwks-token-verifier.js";
10
11
  export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
11
12
  export { readBoundedResponseText } from "./http-response.js";
@@ -1,4 +1,5 @@
1
1
  export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
2
+ export { parseOAuthClientRegistration } from "./client/client-registration.js";
2
3
  export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
3
4
  export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
4
5
  export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
@@ -173,16 +173,32 @@ interface StoredOAuthTokens {
173
173
  expiresAt: number | null;
174
174
  scope?: string;
175
175
  }
176
+ /** Full RFC 7591 response, including JSON provider extensions. */
177
+ interface OAuthClientRegistration extends Record<string, unknown> {
178
+ client_id: string;
179
+ client_secret?: string | null;
180
+ redirect_uris?: string[] | null;
181
+ grant_types?: string[] | null;
182
+ response_types?: string[] | null;
183
+ contacts?: string[] | null;
184
+ client_id_issued_at?: number | null;
185
+ client_secret_expires_at?: number | null;
186
+ token_endpoint_auth_method?: string | null;
187
+ }
188
+ interface StoredOAuthClient {
189
+ clientId: string;
190
+ clientSecret?: string;
191
+ registration?: OAuthClientRegistration;
192
+ }
176
193
  interface StoredOAuthSession {
177
194
  resource: string;
178
195
  authorizationServer: string;
179
- client: {
180
- clientId: string;
181
- clientSecret?: string;
182
- };
196
+ client: StoredOAuthClient;
183
197
  tokens?: StoredOAuthTokens;
184
198
  /** A refresh was begun; its winning response may not have been persisted. */
185
199
  refreshState?: "pending";
200
+ /** Canonical explicitly requested scope set when the server omits token scope. */
201
+ requestedScope?: string;
186
202
  discovery: {
187
203
  resourceMetadataUrl: string;
188
204
  resourceMetadata: Record<string, unknown>;
@@ -205,11 +221,14 @@ interface DefaultOAuthClientProviderOptions {
205
221
  clientId?: string;
206
222
  clientSecret?: string;
207
223
  metadata?: OAuthClientMetadata;
224
+ /** Import a complete registration owned by the caller. */
225
+ registration?: OAuthClientRegistration;
208
226
  } | {
209
227
  mode: "static";
210
228
  clientId: string;
211
229
  clientSecret?: string;
212
230
  metadata?: OAuthClientMetadata;
231
+ registration?: OAuthClientRegistration;
213
232
  };
214
233
  /** Disable interactive authorization while allowing cached tokens and silent refresh. */
215
234
  allowInteractive?: boolean;
@@ -3395,6 +3395,124 @@ var SubscriptionManager = class {
3395
3395
  }
3396
3396
  };
3397
3397
 
3398
+ // ../mcp-oauth/dist/client/scope.js
3399
+ function normalizeOAuthScope(scope) {
3400
+ if (scope === void 0)
3401
+ return void 0;
3402
+ if (typeof scope !== "string" || [...scope].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
3403
+ throw new Error("Invalid OAuth scope syntax");
3404
+ const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
3405
+ return normalized || void 0;
3406
+ }
3407
+
3408
+ // ../mcp-oauth/dist/client/client-registration.js
3409
+ function parseOAuthClientRegistration(value) {
3410
+ const invalid = () => new Error("Invalid OAuth client registration metadata");
3411
+ let nodes = 0;
3412
+ function copy(input, depth) {
3413
+ if (++nodes > 2e4 || depth > 64)
3414
+ throw invalid();
3415
+ if (input === null || typeof input === "boolean" || typeof input === "string")
3416
+ return input;
3417
+ if (typeof input === "number" && Number.isFinite(input))
3418
+ return input;
3419
+ if (typeof input !== "object" || input === null)
3420
+ throw invalid();
3421
+ const descriptors = Object.getOwnPropertyDescriptors(input);
3422
+ if (Array.isArray(input)) {
3423
+ const length = descriptors.length?.value;
3424
+ if (length > 2e4)
3425
+ throw invalid();
3426
+ const result2 = [];
3427
+ for (let index = 0; index < length; index++) {
3428
+ const descriptor = descriptors[String(index)];
3429
+ if (descriptor === void 0 || !Object.hasOwn(descriptor, "value"))
3430
+ throw invalid();
3431
+ result2.push(copy(descriptor.value, depth + 1));
3432
+ }
3433
+ return result2;
3434
+ }
3435
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
3436
+ throw invalid();
3437
+ return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key2, descriptor]) => {
3438
+ if (!Object.hasOwn(descriptor, "value"))
3439
+ throw invalid();
3440
+ return [key2, copy(descriptor.value, depth + 1)];
3441
+ }));
3442
+ }
3443
+ let result;
3444
+ try {
3445
+ result = copy(value, 0);
3446
+ } catch {
3447
+ throw invalid();
3448
+ }
3449
+ if (typeof result !== "object" || result === null || Array.isArray(result))
3450
+ throw invalid();
3451
+ const record2 = result;
3452
+ if (!Object.hasOwn(record2, "client_id") || typeof record2.client_id !== "string" || record2.client_id.trim() === "")
3453
+ throw new Error("OAuth client registration response missing client_id");
3454
+ for (const key2 of [
3455
+ "client_id",
3456
+ "client_secret",
3457
+ "token_endpoint_auth_method",
3458
+ "application_type",
3459
+ "client_name",
3460
+ "client_uri",
3461
+ "logo_uri",
3462
+ "scope",
3463
+ "tos_uri",
3464
+ "policy_uri",
3465
+ "jwks_uri",
3466
+ "software_id",
3467
+ "software_version",
3468
+ "software_statement",
3469
+ "registration_access_token",
3470
+ "registration_client_uri",
3471
+ "issuer"
3472
+ ]) {
3473
+ if (Object.hasOwn(record2, key2) && record2[key2] !== null && typeof record2[key2] !== "string")
3474
+ throw invalid();
3475
+ }
3476
+ if (typeof record2.client_secret === "string" && record2.client_secret.trim() === "")
3477
+ throw invalid();
3478
+ for (const key2 of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
3479
+ const entry = record2[key2];
3480
+ if (Object.hasOwn(record2, key2) && entry !== null && (!Array.isArray(entry) || entry.some((item) => typeof item !== "string")))
3481
+ throw invalid();
3482
+ }
3483
+ for (const key2 of ["client_id_issued_at", "client_secret_expires_at"]) {
3484
+ const entry = record2[key2];
3485
+ if (Object.hasOwn(record2, key2) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
3486
+ throw invalid();
3487
+ }
3488
+ try {
3489
+ normalizeOAuthScope(Object.hasOwn(record2, "scope") && record2.scope !== null ? record2.scope : void 0);
3490
+ } catch {
3491
+ throw invalid();
3492
+ }
3493
+ if (Buffer.byteLength(JSON.stringify(record2), "utf8") > 64 * 1024)
3494
+ throw invalid();
3495
+ return record2;
3496
+ }
3497
+ function normalizeStoredOAuthClient(value) {
3498
+ if (typeof value !== "object" || value === null || Array.isArray(value))
3499
+ return null;
3500
+ const record2 = value;
3501
+ const clientId = Object.hasOwn(record2, "clientId") ? record2.clientId : void 0;
3502
+ const clientSecret = Object.hasOwn(record2, "clientSecret") ? record2.clientSecret : void 0;
3503
+ if (typeof clientId !== "string" || clientId.trim() === "" || clientSecret !== void 0 && (typeof clientSecret !== "string" || clientSecret.trim() === ""))
3504
+ return null;
3505
+ const client = { clientId: clientId.trim(), ...clientSecret === void 0 ? {} : { clientSecret: clientSecret.trim() } };
3506
+ if (Object.hasOwn(record2, "registration") && record2.registration !== void 0) {
3507
+ const registration = parseOAuthClientRegistration(record2.registration);
3508
+ const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : void 0;
3509
+ if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
3510
+ throw new Error("OAuth client registration does not match the client identity");
3511
+ client.registration = registration;
3512
+ }
3513
+ return client;
3514
+ }
3515
+
3398
3516
  // ../mcp-oauth/dist/client/auth-store-session-store.js
3399
3517
  import crypto from "node:crypto";
3400
3518
  import path4 from "node:path";
@@ -4128,14 +4246,8 @@ function createAuthStoreClientStore(options, namespace) {
4128
4246
  } catch {
4129
4247
  throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
4130
4248
  }
4131
- const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : void 0;
4132
- if (clientId !== void 0) {
4133
- const client = { clientId };
4134
- if (isObjectRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, "clientSecret")) {
4135
- client.clientSecret = getOwnEntry3(parsed, "clientSecret");
4136
- }
4137
- return client;
4138
- }
4249
+ if (isObjectRecord(parsed) && typeof getOwnEntry3(parsed, "clientId") === "string")
4250
+ return parsed;
4139
4251
  throw new Error("Stored OAuth client must be a JSON object with clientId");
4140
4252
  },
4141
4253
  async save(issuer, client) {
@@ -4201,14 +4313,7 @@ function isStoredOAuthSession(value) {
4201
4313
  if (!isObjectRecord(value)) {
4202
4314
  return false;
4203
4315
  }
4204
- return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && (getOwnEntry3(value, "refreshState") === void 0 || getOwnEntry3(value, "refreshState") === "pending" && getOwnEntry3(value, "tokens") === void 0) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
4205
- }
4206
- function isStoredOAuthClient(value) {
4207
- if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
4208
- return false;
4209
- }
4210
- const clientSecret = getOwnEntry3(value, "clientSecret");
4211
- return clientSecret === void 0 || typeof clientSecret === "string" && clientSecret.trim().length > 0;
4316
+ return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && normalizeStoredOAuthClient(getOwnEntry3(value, "client")) !== null && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && (getOwnEntry3(value, "requestedScope") === void 0 || isNonBlankOwnString(value, "requestedScope")) && (getOwnEntry3(value, "refreshState") === void 0 || getOwnEntry3(value, "refreshState") === "pending" && getOwnEntry3(value, "tokens") === void 0) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
4212
4317
  }
4213
4318
  function isStoredOAuthDiscovery(value) {
4214
4319
  if (!isObjectRecord(value)) {
@@ -4726,7 +4831,9 @@ async function requestTokens(input) {
4726
4831
  const refreshToken = getOwnEntry5(payload, "refresh_token");
4727
4832
  const scope = getOwnEntry5(payload, "scope");
4728
4833
  const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : void 0;
4729
- const normalizedScope = typeof scope === "string" && scope.trim().length > 0 ? scope.trim() : void 0;
4834
+ const normalizedScope = normalizeOAuthScope(scope);
4835
+ if (scope !== void 0 && normalizedScope === void 0)
4836
+ throw new Error("Invalid OAuth scope syntax in token response");
4730
4837
  return {
4731
4838
  accessToken: normalizedAccessToken,
4732
4839
  refreshToken: normalizedRefreshToken === void 0 ? void 0 : normalizedRefreshToken,
@@ -4847,6 +4954,9 @@ function createOAuthClientProvider(options) {
4847
4954
  function createDefaultOAuthClientProvider(options) {
4848
4955
  loopbackTarget(options.browser);
4849
4956
  assertPersistenceNamespace(options.persistenceNamespace);
4957
+ const clientMetadata = getClientMetadata(options.client);
4958
+ const requestedScope = clientMetadata?.scope;
4959
+ const configuredClient = normalizeConfiguredClient(options.client);
4850
4960
  const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
4851
4961
  const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
4852
4962
  const now = options.now ?? Date.now;
@@ -4864,11 +4974,13 @@ function createDefaultOAuthClientProvider(options) {
4864
4974
  const initialGrant = options.initialGrant === void 0 ? void 0 : {
4865
4975
  resource: canonicalizeResourceIndicator(options.initialGrant.resource),
4866
4976
  tokens: normalizeStoredTokens(options.initialGrant.tokens),
4867
- client: normalizeConfiguredClient(options.client)
4977
+ client: configuredClient
4868
4978
  };
4869
4979
  if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
4870
4980
  throw new Error("OAuth initial grant requires valid tokens and the original client ID");
4871
4981
  if (initialGrant?.tokens !== void 0) {
4982
+ if (requestedScope !== void 0 && initialGrant.tokens.scope !== requestedScope)
4983
+ throw new Error("OAuth initial grant does not match the requested OAuth scope");
4872
4984
  try {
4873
4985
  new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
4874
4986
  } catch {
@@ -4956,6 +5068,7 @@ function createDefaultOAuthClientProvider(options) {
4956
5068
  authorizationServer: discovery.authorizationServer,
4957
5069
  client: initialGrant.client,
4958
5070
  tokens: initialGrant.tokens,
5071
+ ...requestedScope === void 0 ? {} : { requestedScope },
4959
5072
  discovery: toStoredDiscovery(discovery)
4960
5073
  };
4961
5074
  await saveSession(canonicalResource, session);
@@ -4965,11 +5078,13 @@ function createDefaultOAuthClientProvider(options) {
4965
5078
  if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
4966
5079
  forceRefresh = false;
4967
5080
  const sessionDiscovery = resolveDiscovery(discovery, session);
4968
- if ((options.client.mode === "static" || initialGrant !== void 0) && session !== null && (session.tokens !== void 0 || session.refreshState === "pending")) {
4969
- const configured = normalizeConfiguredClient(options.client);
5081
+ if ((options.client.mode === "static" || configuredClient?.registration !== void 0 || initialGrant !== void 0) && session !== null && (session.tokens !== void 0 || session.refreshState === "pending")) {
5082
+ const configured = configuredClient;
4970
5083
  if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
4971
5084
  throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
4972
5085
  }
5086
+ if (requestedScope !== void 0 && session?.tokens !== void 0 && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
5087
+ throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
4973
5088
  if (session?.refreshState === "pending") {
4974
5089
  if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
4975
5090
  throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
@@ -5046,10 +5161,13 @@ function createDefaultOAuthClientProvider(options) {
5046
5161
  ...session,
5047
5162
  tokens: {
5048
5163
  ...refreshedTokens,
5049
- refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
5164
+ refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
5165
+ scope: refreshedTokens.scope ?? session.tokens.scope
5050
5166
  },
5051
5167
  discovery: toStoredDiscovery(discovery)
5052
5168
  };
5169
+ if (requestedScope !== void 0 && normalizeOAuthScope(updatedSession.tokens?.scope ?? session.requestedScope) !== requestedScope)
5170
+ throw new Error("OAuth refresh response does not match the requested OAuth scope; authorize again");
5053
5171
  await saveSession(resource, updatedSession);
5054
5172
  return updatedSession;
5055
5173
  }
@@ -5077,6 +5195,7 @@ function createDefaultOAuthClientProvider(options) {
5077
5195
  resource,
5078
5196
  authorizationServer: discovery.authorizationServer,
5079
5197
  client: resolvedClient.client,
5198
+ ...requestedScope === void 0 ? {} : { requestedScope },
5080
5199
  discovery: toStoredDiscovery(discovery)
5081
5200
  };
5082
5201
  await saveSession(resource, sessionWithoutTokens);
@@ -5088,7 +5207,7 @@ function createDefaultOAuthClientProvider(options) {
5088
5207
  clientId: resolvedClient.client.clientId,
5089
5208
  redirectUri: loopback.redirectUri,
5090
5209
  codeChallenge: challenge,
5091
- clientMetadata: getClientMetadata(options.client)
5210
+ clientMetadata
5092
5211
  });
5093
5212
  const code = await loopback.waitForCode(authorizationUrl);
5094
5213
  const tokens = await exchangeAuthorizationCode({
@@ -5103,6 +5222,8 @@ function createDefaultOAuthClientProvider(options) {
5103
5222
  signal,
5104
5223
  now
5105
5224
  });
5225
+ if (requestedScope !== void 0 && tokens.scope !== void 0 && normalizeOAuthScope(tokens.scope) !== requestedScope)
5226
+ throw new Error("OAuth authorization response does not match the requested OAuth scope");
5106
5227
  const session = {
5107
5228
  ...sessionWithoutTokens,
5108
5229
  tokens
@@ -5132,8 +5253,7 @@ function createDefaultOAuthClientProvider(options) {
5132
5253
  }
5133
5254
  async function resolveClient(existingSession, discovery, redirectUri, fetch2, parentSignal) {
5134
5255
  parentSignal?.throwIfAborted();
5135
- const configuredClient = normalizeConfiguredClient(options.client);
5136
- if (options.client.mode === "static") {
5256
+ if (options.client.mode === "static" || configuredClient?.registration !== void 0) {
5137
5257
  if (configuredClient === null) {
5138
5258
  throw new Error("OAuth client_id must not be blank");
5139
5259
  }
@@ -5180,7 +5300,7 @@ function createDefaultOAuthClientProvider(options) {
5180
5300
  };
5181
5301
  }
5182
5302
  }
5183
- const registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
5303
+ const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
5184
5304
  const deadline = AbortSignal.timeout(3e4);
5185
5305
  const signal = parentSignal === void 0 ? deadline : AbortSignal.any([parentSignal, deadline]);
5186
5306
  const response = await fetchMcpResponse(fetch2, registrationEndpoint, {
@@ -5192,14 +5312,12 @@ function createDefaultOAuthClientProvider(options) {
5192
5312
  signal
5193
5313
  });
5194
5314
  const payload = await readOAuthJsonObjectResponse(response, signal);
5195
- const clientId = getOwnString2(payload, "client_id");
5196
- if (clientId === void 0 || clientId.trim().length === 0) {
5197
- throw new Error("OAuth client registration response missing client_id");
5198
- }
5199
- const clientSecret = getOwnString2(payload, "client_secret");
5315
+ const registration = parseOAuthClientRegistration(payload);
5316
+ const registeredSecret = getOwnString2(registration, "client_secret");
5200
5317
  const registeredClient = {
5201
- clientId: clientId.trim(),
5202
- clientSecret: clientSecret !== void 0 && clientSecret.trim().length > 0 ? clientSecret.trim() : void 0
5318
+ clientId: registration.client_id.trim(),
5319
+ ...registeredSecret === void 0 ? {} : { clientSecret: registeredSecret.trim() },
5320
+ registration
5203
5321
  };
5204
5322
  await saveRegisteredClient(discovery.authorizationServer, registeredClient);
5205
5323
  return {
@@ -5225,7 +5343,7 @@ function createDefaultOAuthClientProvider(options) {
5225
5343
  return null;
5226
5344
  }
5227
5345
  const client = await clientStore.load(issuer);
5228
- const normalizedClient = client === null ? null : normalizeStoredClient(client);
5346
+ const normalizedClient = client === null ? null : normalizeStoredOAuthClient(client);
5229
5347
  if (client !== null && normalizedClient === null) {
5230
5348
  await clientStore.clear(issuer);
5231
5349
  return null;
@@ -5298,7 +5416,7 @@ function normalizeLoadedSession(session) {
5298
5416
  const refreshState = getOwnEntry6(session, "refreshState");
5299
5417
  if (refreshState !== void 0 && (refreshState !== "pending" || getOwnEntry6(session, "tokens") !== void 0))
5300
5418
  throw new Error("Stored OAuth refresh state is invalid");
5301
- const client = normalizeStoredClient(getOwnEntry6(session, "client"));
5419
+ const client = normalizeStoredOAuthClient(getOwnEntry6(session, "client"));
5302
5420
  if (client === null) {
5303
5421
  return { ...session, client: { clientId: "" }, tokens: void 0 };
5304
5422
  }
@@ -5308,25 +5426,6 @@ function normalizeLoadedSession(session) {
5308
5426
  tokens: normalizeStoredTokens(getOwnEntry6(session, "tokens"))
5309
5427
  };
5310
5428
  }
5311
- function normalizeStoredClient(value) {
5312
- if (!isObjectRecord3(value)) {
5313
- return null;
5314
- }
5315
- const clientId = getOwnString2(value, "clientId");
5316
- if (clientId === void 0 || clientId.trim().length === 0) {
5317
- return null;
5318
- }
5319
- const normalizedClientId = clientId.trim();
5320
- const clientSecret = getOwnEntry6(value, "clientSecret");
5321
- if (clientSecret === void 0) {
5322
- return { clientId: normalizedClientId };
5323
- }
5324
- if (typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
5325
- return null;
5326
- }
5327
- const normalizedClientSecret = clientSecret.trim();
5328
- return { clientId: normalizedClientId, clientSecret: normalizedClientSecret };
5329
- }
5330
5429
  function normalizeStoredTokens(value) {
5331
5430
  if (value === void 0 || !isObjectRecord3(value)) {
5332
5431
  return void 0;
@@ -5335,10 +5434,10 @@ function normalizeStoredTokens(value) {
5335
5434
  const tokenType = getOwnString2(value, "tokenType");
5336
5435
  const expiresAt = getOwnEntry6(value, "expiresAt");
5337
5436
  const refreshToken = getOwnEntry6(value, "refreshToken");
5338
- const scope = getOwnString2(value, "scope");
5437
+ const scope = getOwnEntry6(value, "scope");
5339
5438
  const normalizedAccessToken = accessToken?.trim();
5340
5439
  const normalizedRefreshToken = typeof refreshToken === "string" ? refreshToken.trim() : void 0;
5341
- const normalizedScope = scope?.trim();
5440
+ const normalizedScope = normalizeOAuthScope(scope);
5342
5441
  if (accessToken === void 0 || normalizedAccessToken === void 0 || normalizedAccessToken.length === 0 || tokenType !== "Bearer" || !(expiresAt === null || typeof expiresAt === "number" && Number.isSafeInteger(expiresAt) && expiresAt <= MAX_JS_DATE_MS3 && Number.isFinite(new Date(expiresAt).getTime())) || refreshToken !== void 0 && (typeof refreshToken !== "string" || normalizedRefreshToken === void 0 || normalizedRefreshToken.length === 0)) {
5343
5442
  return void 0;
5344
5443
  }
@@ -5356,18 +5455,18 @@ function getClientMetadata(client) {
5356
5455
  }
5357
5456
  return {
5358
5457
  clientName: normalizeOptionalOAuthString(client.metadata.clientName),
5359
- scope: normalizeOptionalOAuthString(client.metadata.scope),
5458
+ scope: normalizeOAuthScope(client.metadata.scope),
5360
5459
  softwareId: normalizeOptionalOAuthString(client.metadata.softwareId),
5361
5460
  softwareVersion: normalizeOptionalOAuthString(client.metadata.softwareVersion)
5362
5461
  };
5363
5462
  }
5364
5463
  function normalizeConfiguredClient(client) {
5365
- const clientId = normalizeOptionalOAuthString(client.clientId);
5366
- if (clientId === void 0) {
5464
+ const registration = client.registration === void 0 ? void 0 : parseOAuthClientRegistration(client.registration);
5465
+ const clientId = normalizeOptionalOAuthString(client.clientId) ?? registration?.client_id.trim();
5466
+ if (clientId === void 0)
5367
5467
  return null;
5368
- }
5369
- const clientSecret = normalizeOptionalOAuthString(client.clientSecret);
5370
- return clientSecret === void 0 ? { clientId } : { clientId, clientSecret };
5468
+ const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === void 0 ? void 0 : getOwnString2(registration, "client_secret")?.trim());
5469
+ return normalizeStoredOAuthClient({ clientId, clientSecret, registration });
5371
5470
  }
5372
5471
  function normalizeOptionalOAuthString(value) {
5373
5472
  if (value === void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",