tiny-http-mcp-server 0.1.30 → 0.1.32

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.30",
21
+ "version": "0.1.32",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -42,6 +42,7 @@ const verifier = createJwksTokenVerifier({
42
42
  - `mode: "static"` with `clientId`, optional `clientSecret`, optional `metadata`
43
43
  - `allowInteractive: false` prevents interactive login while retaining cached tokens and silent refresh
44
44
  - `sessionLockTimeoutMs` limits acquisition waits for a session transaction lock (default 30,000 ms; integer from 1 to 2147483647)
45
+ - `persistenceNamespace` isolates native sessions and registrations for a named profile (nonempty string, at most 1024 UTF-8 bytes)
45
46
  - `initialGrant: { resource, tokens }` optionally imports an existing Bearer grant for one HTTP resource; requires the original client ID
46
47
  - `browser.openBrowser(url)` optional
47
48
  - `browser.readLine()` optional
@@ -95,7 +96,14 @@ on 401 even when the server omits `error="invalid_token"`. Invalid provenance
95
96
  fails without quoting token values.
96
97
 
97
98
  Configure `client.metadata.scope` to request a precise scope set; broader
98
- 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.
99
107
 
100
108
  Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
101
109
  `tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
@@ -108,6 +116,13 @@ Static clients and dynamic initial-grant imports require cached grants to match
108
116
  the original normalized client ID and secret. A different client configuration
109
117
  fails before attaching or refreshing credentials and retains the stored record;
110
118
  select separate persistence or explicitly reset the session to change apps.
119
+ Use `persistenceNamespace: "personal"` or `"work"` to keep separate native
120
+ profiles for the same resource/issuer. `createAuthStoreSessionStore(options,
121
+ namespace)` addresses the same profile when seeding or inspecting credentials.
122
+ Namespaces are hashed into file/Keychain identities and transaction locks;
123
+ omitting one preserves the default storage keys. Switching namespaces selects a
124
+ different record and does not migrate or reset the previous profile. Host-owned
125
+ `sessionStore` implementations remain responsible for their own profile keys.
111
126
 
112
127
  `createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
113
128
 
@@ -9,6 +9,7 @@ export interface OAuthClientStore {
9
9
  save(issuer: string, client: StoredOAuthClient): Promise<void>;
10
10
  clear(issuer: string): Promise<void>;
11
11
  }
12
- export declare function createAuthStoreSessionStore(options?: CreateSecretStoreInput): OAuthSessionStore;
13
- export declare function createAuthStoreClientStore(options: CreateSecretStoreInput): OAuthClientStore;
12
+ export declare function createAuthStoreSessionStore(options?: CreateSecretStoreInput, namespace?: string): OAuthSessionStore;
13
+ export declare function createAuthStoreClientStore(options: CreateSecretStoreInput, namespace?: string): OAuthClientStore;
14
+ export declare function assertPersistenceNamespace(namespace: string | undefined): void;
14
15
  export {};
@@ -9,16 +9,17 @@ const DEFAULT_CLIENT_FILE_SALT = "poe-code:mcp-oauth:clients:v1";
9
9
  const DEFAULT_CLIENT_FILE_DIRECTORY = ".poe-code/mcp-oauth/clients";
10
10
  const DEFAULT_CLIENT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth-clients";
11
11
  const MAX_JS_DATE_MS = 8_640_000_000_000_000;
12
- export function createAuthStoreSessionStore(options = {}) {
12
+ export function createAuthStoreSessionStore(options = {}, namespace) {
13
+ assertPersistenceNamespace(namespace);
13
14
  return {
14
15
  async withLock(resource, operation, lockOptions) {
15
- const store = createResourceSecretStore(resource, options);
16
+ const store = createResourceSecretStore(resource, options, namespace);
16
17
  if (store.withLock === undefined)
17
18
  throw new Error("OAuth secret-store backend does not support transaction locks");
18
19
  return store.withLock(operation, lockOptions);
19
20
  },
20
21
  async load(resource) {
21
- const store = createResourceSecretStore(resource, options);
22
+ const store = createResourceSecretStore(resource, options, namespace);
22
23
  const value = await store.get();
23
24
  if (value === null) {
24
25
  return null;
@@ -36,19 +37,20 @@ export function createAuthStoreSessionStore(options = {}) {
36
37
  throw new Error("Stored OAuth session must match the expected shape");
37
38
  },
38
39
  async save(resource, session) {
39
- const store = createResourceSecretStore(resource, options);
40
+ const store = createResourceSecretStore(resource, options, namespace);
40
41
  await store.set(JSON.stringify(session));
41
42
  },
42
43
  async clear(resource) {
43
- const store = createResourceSecretStore(resource, options);
44
+ const store = createResourceSecretStore(resource, options, namespace);
44
45
  await store.delete();
45
46
  }
46
47
  };
47
48
  }
48
- export function createAuthStoreClientStore(options) {
49
+ export function createAuthStoreClientStore(options, namespace) {
50
+ assertPersistenceNamespace(namespace);
49
51
  return {
50
52
  async load(issuer) {
51
- const store = createIssuerSecretStore(issuer, options);
53
+ const store = createIssuerSecretStore(issuer, options, namespace);
52
54
  const value = await store.get();
53
55
  if (value === null) {
54
56
  return null;
@@ -72,17 +74,17 @@ export function createAuthStoreClientStore(options) {
72
74
  throw new Error("Stored OAuth client must be a JSON object with clientId");
73
75
  },
74
76
  async save(issuer, client) {
75
- const store = createIssuerSecretStore(issuer, options);
77
+ const store = createIssuerSecretStore(issuer, options, namespace);
76
78
  await store.set(JSON.stringify(client));
77
79
  },
78
80
  async clear(issuer) {
79
- const store = createIssuerSecretStore(issuer, options);
81
+ const store = createIssuerSecretStore(issuer, options, namespace);
80
82
  await store.delete();
81
83
  }
82
84
  };
83
85
  }
84
- function createNamedSecretStore(key, options, defaults) {
85
- const hash = crypto.createHash("sha256").update(key).digest("hex");
86
+ function createNamedSecretStore(key, options, defaults, namespace) {
87
+ const hash = crypto.createHash("sha256").update(namespace === undefined ? key : JSON.stringify([namespace, key])).digest("hex");
86
88
  const configuredFilePath = options.fileStore?.filePath;
87
89
  const parsedFilePath = configuredFilePath === undefined ? null : path.parse(configuredFilePath);
88
90
  const fileStore = {
@@ -104,21 +106,25 @@ function createNamedSecretStore(key, options, defaults) {
104
106
  };
105
107
  return createSecretStore({ ...options, fileStore, keychainStore }).store;
106
108
  }
107
- function createResourceSecretStore(resource, options) {
109
+ function createResourceSecretStore(resource, options, namespace) {
108
110
  return createNamedSecretStore(canonicalizeResourceIndicator(resource), options, {
109
111
  salt: DEFAULT_FILE_SALT,
110
112
  directory: DEFAULT_FILE_DIRECTORY,
111
113
  service: DEFAULT_KEYCHAIN_SERVICE,
112
114
  accountPrefix: "provider"
113
- });
115
+ }, namespace);
114
116
  }
115
- function createIssuerSecretStore(issuer, options) {
117
+ function createIssuerSecretStore(issuer, options, namespace) {
116
118
  return createNamedSecretStore(issuer, options, {
117
119
  salt: DEFAULT_CLIENT_FILE_SALT,
118
120
  directory: DEFAULT_CLIENT_FILE_DIRECTORY,
119
121
  service: DEFAULT_CLIENT_KEYCHAIN_SERVICE,
120
122
  accountPrefix: "issuer"
121
- });
123
+ }, namespace);
124
+ }
125
+ export function assertPersistenceNamespace(namespace) {
126
+ if (namespace !== undefined && (typeof namespace !== "string" || namespace.trim() === "" || Buffer.byteLength(namespace, "utf8") > 1024))
127
+ throw new Error("OAuth persistence namespace must be a nonempty string within 1024 bytes");
122
128
  }
123
129
  function isObjectRecord(value) {
124
130
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -138,6 +144,7 @@ function isStoredOAuthSession(value) {
138
144
  isNonBlankOwnString(value, "authorizationServer") &&
139
145
  isStoredOAuthClient(getOwnEntry(value, "client")) &&
140
146
  isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
147
+ (getOwnEntry(value, "requestedScope") === undefined || isNonBlankOwnString(value, "requestedScope")) &&
141
148
  (getOwnEntry(value, "refreshState") === undefined ||
142
149
  (getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
143
150
  isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
@@ -1,7 +1,8 @@
1
+ import { normalizeOAuthScope } from "./scope.js";
1
2
  import { isIP } from "node:net";
2
3
  import { fetchMcpResponse } from "../http-fetch.js";
3
4
  import { URL } from "node:url";
4
- import { createAuthStoreClientStore, createAuthStoreSessionStore } from "./auth-store-session-store.js";
5
+ import { createAuthStoreClientStore, createAuthStoreSessionStore, assertPersistenceNamespace } from "./auth-store-session-store.js";
5
6
  import { createLoopbackAuthorizationSession, loopbackTarget } from "./loopback-authorization.js";
6
7
  import { createAuthorizationState } from "./authorization-state.js";
7
8
  import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
@@ -17,8 +18,11 @@ export function createOAuthClientProvider(options) {
17
18
  }
18
19
  export function createDefaultOAuthClientProvider(options) {
19
20
  loopbackTarget(options.browser);
20
- const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
21
- const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
21
+ assertPersistenceNamespace(options.persistenceNamespace);
22
+ const clientMetadata = getClientMetadata(options.client);
23
+ const requestedScope = clientMetadata?.scope;
24
+ const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
25
+ const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
22
26
  const now = options.now ?? Date.now;
23
27
  const registeredClients = new Map();
24
28
  if (options.initialGrant !== undefined) {
@@ -40,6 +44,8 @@ export function createDefaultOAuthClientProvider(options) {
40
44
  if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
41
45
  throw new Error("OAuth initial grant requires valid tokens and the original client ID");
42
46
  if (initialGrant?.tokens !== undefined) {
47
+ if (requestedScope !== undefined && initialGrant.tokens.scope !== requestedScope)
48
+ throw new Error("OAuth initial grant does not match the requested OAuth scope");
43
49
  try {
44
50
  new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
45
51
  }
@@ -132,7 +138,8 @@ export function createDefaultOAuthClientProvider(options) {
132
138
  initialGrant.tokens !== undefined && initialGrant.client !== null) {
133
139
  assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
134
140
  session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
135
- client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
141
+ client: initialGrant.client, tokens: initialGrant.tokens,
142
+ ...(requestedScope === undefined ? {} : { requestedScope }), discovery: toStoredDiscovery(discovery) };
136
143
  await saveSession(canonicalResource, session);
137
144
  initialGrantConsumed = true;
138
145
  signal?.throwIfAborted();
@@ -145,6 +152,8 @@ export function createDefaultOAuthClientProvider(options) {
145
152
  if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
146
153
  throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
147
154
  }
155
+ if (requestedScope !== undefined && session?.tokens !== undefined && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
156
+ throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
148
157
  if (session?.refreshState === "pending") {
149
158
  if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
150
159
  throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
@@ -225,10 +234,13 @@ export function createDefaultOAuthClientProvider(options) {
225
234
  ...session,
226
235
  tokens: {
227
236
  ...refreshedTokens,
228
- refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
237
+ refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
238
+ scope: refreshedTokens.scope ?? session.tokens.scope
229
239
  },
230
240
  discovery: toStoredDiscovery(discovery)
231
241
  };
242
+ if (requestedScope !== undefined && normalizeOAuthScope(updatedSession.tokens?.scope ?? session.requestedScope) !== requestedScope)
243
+ throw new Error("OAuth refresh response does not match the requested OAuth scope; authorize again");
232
244
  await saveSession(resource, updatedSession);
233
245
  return updatedSession;
234
246
  }
@@ -256,6 +268,7 @@ export function createDefaultOAuthClientProvider(options) {
256
268
  resource,
257
269
  authorizationServer: discovery.authorizationServer,
258
270
  client: resolvedClient.client,
271
+ ...(requestedScope === undefined ? {} : { requestedScope }),
259
272
  discovery: toStoredDiscovery(discovery)
260
273
  };
261
274
  await saveSession(resource, sessionWithoutTokens);
@@ -267,7 +280,7 @@ export function createDefaultOAuthClientProvider(options) {
267
280
  clientId: resolvedClient.client.clientId,
268
281
  redirectUri: loopback.redirectUri,
269
282
  codeChallenge: challenge,
270
- clientMetadata: getClientMetadata(options.client)
283
+ clientMetadata
271
284
  });
272
285
  const code = await loopback.waitForCode(authorizationUrl);
273
286
  const tokens = await exchangeAuthorizationCode({
@@ -281,6 +294,8 @@ export function createDefaultOAuthClientProvider(options) {
281
294
  fetch, signal,
282
295
  now
283
296
  });
297
+ if (requestedScope !== undefined && tokens.scope !== undefined && normalizeOAuthScope(tokens.scope) !== requestedScope)
298
+ throw new Error("OAuth authorization response does not match the requested OAuth scope");
284
299
  const session = {
285
300
  ...sessionWithoutTokens,
286
301
  tokens
@@ -362,7 +377,7 @@ export function createDefaultOAuthClientProvider(options) {
362
377
  };
363
378
  }
364
379
  }
365
- const registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
380
+ const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
366
381
  const deadline = AbortSignal.timeout(30_000);
367
382
  const signal = parentSignal === undefined ? deadline : AbortSignal.any([parentSignal, deadline]);
368
383
  const response = await fetchMcpResponse(fetch, registrationEndpoint, {
@@ -525,10 +540,10 @@ function normalizeStoredTokens(value) {
525
540
  const tokenType = getOwnString(value, "tokenType");
526
541
  const expiresAt = getOwnEntry(value, "expiresAt");
527
542
  const refreshToken = getOwnEntry(value, "refreshToken");
528
- const scope = getOwnString(value, "scope");
543
+ const scope = getOwnEntry(value, "scope");
529
544
  const normalizedAccessToken = accessToken?.trim();
530
545
  const normalizedRefreshToken = typeof refreshToken === "string" ? refreshToken.trim() : undefined;
531
- const normalizedScope = scope?.trim();
546
+ const normalizedScope = normalizeOAuthScope(scope);
532
547
  if (accessToken === undefined ||
533
548
  normalizedAccessToken === undefined ||
534
549
  normalizedAccessToken.length === 0 ||
@@ -560,7 +575,7 @@ function getClientMetadata(client) {
560
575
  }
561
576
  return {
562
577
  clientName: normalizeOptionalOAuthString(client.metadata.clientName),
563
- scope: normalizeOptionalOAuthString(client.metadata.scope),
578
+ scope: normalizeOAuthScope(client.metadata.scope),
564
579
  softwareId: normalizeOptionalOAuthString(client.metadata.softwareId),
565
580
  softwareVersion: normalizeOptionalOAuthString(client.metadata.softwareVersion)
566
581
  };
@@ -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,
@@ -80,6 +80,8 @@ export interface StoredOAuthSession {
80
80
  tokens?: StoredOAuthTokens;
81
81
  /** A refresh was begun; its winning response may not have been persisted. */
82
82
  refreshState?: "pending";
83
+ /** Canonical explicitly requested scope set when the server omits token scope. */
84
+ requestedScope?: string;
83
85
  discovery: {
84
86
  resourceMetadataUrl: string;
85
87
  resourceMetadata: Record<string, unknown>;
@@ -112,6 +114,8 @@ export interface DefaultOAuthClientProviderOptions {
112
114
  allowInteractive?: boolean;
113
115
  /** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
114
116
  sessionLockTimeoutMs?: number;
117
+ /** Isolate native persisted sessions and registrations for a named profile. */
118
+ persistenceNamespace?: string;
115
119
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
116
120
  initialGrant?: {
117
121
  resource: string;
@@ -183,6 +183,8 @@ interface StoredOAuthSession {
183
183
  tokens?: StoredOAuthTokens;
184
184
  /** A refresh was begun; its winning response may not have been persisted. */
185
185
  refreshState?: "pending";
186
+ /** Canonical explicitly requested scope set when the server omits token scope. */
187
+ requestedScope?: string;
186
188
  discovery: {
187
189
  resourceMetadataUrl: string;
188
190
  resourceMetadata: Record<string, unknown>;
@@ -215,6 +217,8 @@ interface DefaultOAuthClientProviderOptions {
215
217
  allowInteractive?: boolean;
216
218
  /** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
217
219
  sessionLockTimeoutMs?: number;
220
+ /** Isolate native persisted sessions and registrations for a named profile. */
221
+ persistenceNamespace?: string;
218
222
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
219
223
  initialGrant?: {
220
224
  resource: string;
@@ -4077,16 +4077,17 @@ var DEFAULT_CLIENT_FILE_SALT = "poe-code:mcp-oauth:clients:v1";
4077
4077
  var DEFAULT_CLIENT_FILE_DIRECTORY = ".poe-code/mcp-oauth/clients";
4078
4078
  var DEFAULT_CLIENT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth-clients";
4079
4079
  var MAX_JS_DATE_MS = 864e13;
4080
- function createAuthStoreSessionStore(options = {}) {
4080
+ function createAuthStoreSessionStore(options = {}, namespace) {
4081
+ assertPersistenceNamespace(namespace);
4081
4082
  return {
4082
4083
  async withLock(resource, operation, lockOptions) {
4083
- const store = createResourceSecretStore(resource, options);
4084
+ const store = createResourceSecretStore(resource, options, namespace);
4084
4085
  if (store.withLock === void 0)
4085
4086
  throw new Error("OAuth secret-store backend does not support transaction locks");
4086
4087
  return store.withLock(operation, lockOptions);
4087
4088
  },
4088
4089
  async load(resource) {
4089
- const store = createResourceSecretStore(resource, options);
4090
+ const store = createResourceSecretStore(resource, options, namespace);
4090
4091
  const value = await store.get();
4091
4092
  if (value === null) {
4092
4093
  return null;
@@ -4103,19 +4104,20 @@ function createAuthStoreSessionStore(options = {}) {
4103
4104
  throw new Error("Stored OAuth session must match the expected shape");
4104
4105
  },
4105
4106
  async save(resource, session) {
4106
- const store = createResourceSecretStore(resource, options);
4107
+ const store = createResourceSecretStore(resource, options, namespace);
4107
4108
  await store.set(JSON.stringify(session));
4108
4109
  },
4109
4110
  async clear(resource) {
4110
- const store = createResourceSecretStore(resource, options);
4111
+ const store = createResourceSecretStore(resource, options, namespace);
4111
4112
  await store.delete();
4112
4113
  }
4113
4114
  };
4114
4115
  }
4115
- function createAuthStoreClientStore(options) {
4116
+ function createAuthStoreClientStore(options, namespace) {
4117
+ assertPersistenceNamespace(namespace);
4116
4118
  return {
4117
4119
  async load(issuer) {
4118
- const store = createIssuerSecretStore(issuer, options);
4120
+ const store = createIssuerSecretStore(issuer, options, namespace);
4119
4121
  const value = await store.get();
4120
4122
  if (value === null) {
4121
4123
  return null;
@@ -4137,17 +4139,17 @@ function createAuthStoreClientStore(options) {
4137
4139
  throw new Error("Stored OAuth client must be a JSON object with clientId");
4138
4140
  },
4139
4141
  async save(issuer, client) {
4140
- const store = createIssuerSecretStore(issuer, options);
4142
+ const store = createIssuerSecretStore(issuer, options, namespace);
4141
4143
  await store.set(JSON.stringify(client));
4142
4144
  },
4143
4145
  async clear(issuer) {
4144
- const store = createIssuerSecretStore(issuer, options);
4146
+ const store = createIssuerSecretStore(issuer, options, namespace);
4145
4147
  await store.delete();
4146
4148
  }
4147
4149
  };
4148
4150
  }
4149
- function createNamedSecretStore(key2, options, defaults) {
4150
- const hash = crypto.createHash("sha256").update(key2).digest("hex");
4151
+ function createNamedSecretStore(key2, options, defaults, namespace) {
4152
+ const hash = crypto.createHash("sha256").update(namespace === void 0 ? key2 : JSON.stringify([namespace, key2])).digest("hex");
4151
4153
  const configuredFilePath = options.fileStore?.filePath;
4152
4154
  const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
4153
4155
  const fileStore = {
@@ -4165,21 +4167,25 @@ function createNamedSecretStore(key2, options, defaults) {
4165
4167
  };
4166
4168
  return createSecretStore({ ...options, fileStore, keychainStore }).store;
4167
4169
  }
4168
- function createResourceSecretStore(resource, options) {
4170
+ function createResourceSecretStore(resource, options, namespace) {
4169
4171
  return createNamedSecretStore(canonicalizeResourceIndicator(resource), options, {
4170
4172
  salt: DEFAULT_FILE_SALT,
4171
4173
  directory: DEFAULT_FILE_DIRECTORY,
4172
4174
  service: DEFAULT_KEYCHAIN_SERVICE,
4173
4175
  accountPrefix: "provider"
4174
- });
4176
+ }, namespace);
4175
4177
  }
4176
- function createIssuerSecretStore(issuer, options) {
4178
+ function createIssuerSecretStore(issuer, options, namespace) {
4177
4179
  return createNamedSecretStore(issuer, options, {
4178
4180
  salt: DEFAULT_CLIENT_FILE_SALT,
4179
4181
  directory: DEFAULT_CLIENT_FILE_DIRECTORY,
4180
4182
  service: DEFAULT_CLIENT_KEYCHAIN_SERVICE,
4181
4183
  accountPrefix: "issuer"
4182
- });
4184
+ }, namespace);
4185
+ }
4186
+ function assertPersistenceNamespace(namespace) {
4187
+ if (namespace !== void 0 && (typeof namespace !== "string" || namespace.trim() === "" || Buffer.byteLength(namespace, "utf8") > 1024))
4188
+ throw new Error("OAuth persistence namespace must be a nonempty string within 1024 bytes");
4183
4189
  }
4184
4190
  function isObjectRecord(value) {
4185
4191
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -4195,7 +4201,7 @@ function isStoredOAuthSession(value) {
4195
4201
  if (!isObjectRecord(value)) {
4196
4202
  return false;
4197
4203
  }
4198
- 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"));
4204
+ return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && 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"));
4199
4205
  }
4200
4206
  function isStoredOAuthClient(value) {
4201
4207
  if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
@@ -4236,6 +4242,16 @@ function isNonBlankOwnString(record2, key2) {
4236
4242
  return value !== void 0 && value.trim().length > 0;
4237
4243
  }
4238
4244
 
4245
+ // ../mcp-oauth/dist/client/scope.js
4246
+ function normalizeOAuthScope(scope) {
4247
+ if (scope === void 0)
4248
+ return void 0;
4249
+ if (typeof scope !== "string" || [...scope].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
4250
+ throw new Error("Invalid OAuth scope syntax");
4251
+ const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
4252
+ return normalized || void 0;
4253
+ }
4254
+
4239
4255
  // ../mcp-oauth/dist/client/default-oauth-client-provider.js
4240
4256
  import { isIP } from "node:net";
4241
4257
 
@@ -4720,7 +4736,9 @@ async function requestTokens(input) {
4720
4736
  const refreshToken = getOwnEntry5(payload, "refresh_token");
4721
4737
  const scope = getOwnEntry5(payload, "scope");
4722
4738
  const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : void 0;
4723
- const normalizedScope = typeof scope === "string" && scope.trim().length > 0 ? scope.trim() : void 0;
4739
+ const normalizedScope = normalizeOAuthScope(scope);
4740
+ if (scope !== void 0 && normalizedScope === void 0)
4741
+ throw new Error("Invalid OAuth scope syntax in token response");
4724
4742
  return {
4725
4743
  accessToken: normalizedAccessToken,
4726
4744
  refreshToken: normalizedRefreshToken === void 0 ? void 0 : normalizedRefreshToken,
@@ -4840,8 +4858,11 @@ function createOAuthClientProvider(options) {
4840
4858
  }
4841
4859
  function createDefaultOAuthClientProvider(options) {
4842
4860
  loopbackTarget(options.browser);
4843
- const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
4844
- const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore);
4861
+ assertPersistenceNamespace(options.persistenceNamespace);
4862
+ const clientMetadata = getClientMetadata(options.client);
4863
+ const requestedScope = clientMetadata?.scope;
4864
+ const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
4865
+ const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
4845
4866
  const now = options.now ?? Date.now;
4846
4867
  const registeredClients = /* @__PURE__ */ new Map();
4847
4868
  if (options.initialGrant !== void 0) {
@@ -4862,6 +4883,8 @@ function createDefaultOAuthClientProvider(options) {
4862
4883
  if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
4863
4884
  throw new Error("OAuth initial grant requires valid tokens and the original client ID");
4864
4885
  if (initialGrant?.tokens !== void 0) {
4886
+ if (requestedScope !== void 0 && initialGrant.tokens.scope !== requestedScope)
4887
+ throw new Error("OAuth initial grant does not match the requested OAuth scope");
4865
4888
  try {
4866
4889
  new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
4867
4890
  } catch {
@@ -4949,6 +4972,7 @@ function createDefaultOAuthClientProvider(options) {
4949
4972
  authorizationServer: discovery.authorizationServer,
4950
4973
  client: initialGrant.client,
4951
4974
  tokens: initialGrant.tokens,
4975
+ ...requestedScope === void 0 ? {} : { requestedScope },
4952
4976
  discovery: toStoredDiscovery(discovery)
4953
4977
  };
4954
4978
  await saveSession(canonicalResource, session);
@@ -4963,6 +4987,8 @@ function createDefaultOAuthClientProvider(options) {
4963
4987
  if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
4964
4988
  throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
4965
4989
  }
4990
+ if (requestedScope !== void 0 && session?.tokens !== void 0 && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
4991
+ throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
4966
4992
  if (session?.refreshState === "pending") {
4967
4993
  if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
4968
4994
  throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
@@ -5039,10 +5065,13 @@ function createDefaultOAuthClientProvider(options) {
5039
5065
  ...session,
5040
5066
  tokens: {
5041
5067
  ...refreshedTokens,
5042
- refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
5068
+ refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
5069
+ scope: refreshedTokens.scope ?? session.tokens.scope
5043
5070
  },
5044
5071
  discovery: toStoredDiscovery(discovery)
5045
5072
  };
5073
+ if (requestedScope !== void 0 && normalizeOAuthScope(updatedSession.tokens?.scope ?? session.requestedScope) !== requestedScope)
5074
+ throw new Error("OAuth refresh response does not match the requested OAuth scope; authorize again");
5046
5075
  await saveSession(resource, updatedSession);
5047
5076
  return updatedSession;
5048
5077
  }
@@ -5070,6 +5099,7 @@ function createDefaultOAuthClientProvider(options) {
5070
5099
  resource,
5071
5100
  authorizationServer: discovery.authorizationServer,
5072
5101
  client: resolvedClient.client,
5102
+ ...requestedScope === void 0 ? {} : { requestedScope },
5073
5103
  discovery: toStoredDiscovery(discovery)
5074
5104
  };
5075
5105
  await saveSession(resource, sessionWithoutTokens);
@@ -5081,7 +5111,7 @@ function createDefaultOAuthClientProvider(options) {
5081
5111
  clientId: resolvedClient.client.clientId,
5082
5112
  redirectUri: loopback.redirectUri,
5083
5113
  codeChallenge: challenge,
5084
- clientMetadata: getClientMetadata(options.client)
5114
+ clientMetadata
5085
5115
  });
5086
5116
  const code = await loopback.waitForCode(authorizationUrl);
5087
5117
  const tokens = await exchangeAuthorizationCode({
@@ -5096,6 +5126,8 @@ function createDefaultOAuthClientProvider(options) {
5096
5126
  signal,
5097
5127
  now
5098
5128
  });
5129
+ if (requestedScope !== void 0 && tokens.scope !== void 0 && normalizeOAuthScope(tokens.scope) !== requestedScope)
5130
+ throw new Error("OAuth authorization response does not match the requested OAuth scope");
5099
5131
  const session = {
5100
5132
  ...sessionWithoutTokens,
5101
5133
  tokens
@@ -5173,7 +5205,7 @@ function createDefaultOAuthClientProvider(options) {
5173
5205
  };
5174
5206
  }
5175
5207
  }
5176
- const registrationBody = buildClientRegistrationBody(getClientMetadata(options.client), redirectUri);
5208
+ const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
5177
5209
  const deadline = AbortSignal.timeout(3e4);
5178
5210
  const signal = parentSignal === void 0 ? deadline : AbortSignal.any([parentSignal, deadline]);
5179
5211
  const response = await fetchMcpResponse(fetch2, registrationEndpoint, {
@@ -5328,10 +5360,10 @@ function normalizeStoredTokens(value) {
5328
5360
  const tokenType = getOwnString2(value, "tokenType");
5329
5361
  const expiresAt = getOwnEntry6(value, "expiresAt");
5330
5362
  const refreshToken = getOwnEntry6(value, "refreshToken");
5331
- const scope = getOwnString2(value, "scope");
5363
+ const scope = getOwnEntry6(value, "scope");
5332
5364
  const normalizedAccessToken = accessToken?.trim();
5333
5365
  const normalizedRefreshToken = typeof refreshToken === "string" ? refreshToken.trim() : void 0;
5334
- const normalizedScope = scope?.trim();
5366
+ const normalizedScope = normalizeOAuthScope(scope);
5335
5367
  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)) {
5336
5368
  return void 0;
5337
5369
  }
@@ -5349,7 +5381,7 @@ function getClientMetadata(client) {
5349
5381
  }
5350
5382
  return {
5351
5383
  clientName: normalizeOptionalOAuthString(client.metadata.clientName),
5352
- scope: normalizeOptionalOAuthString(client.metadata.scope),
5384
+ scope: normalizeOAuthScope(client.metadata.scope),
5353
5385
  softwareId: normalizeOptionalOAuthString(client.metadata.softwareId),
5354
5386
  softwareVersion: normalizeOptionalOAuthString(client.metadata.softwareVersion)
5355
5387
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",