tiny-http-mcp-server 0.1.34 → 0.1.35

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.34",
21
+ "version": "0.1.35",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -106,8 +106,13 @@ Select a separate persistence namespace for another scope profile. No scope is
106
106
  invented when the client does not configure one.
107
107
 
108
108
  Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
109
- `tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
110
- and optional `scope`. A fresh imported token is used only for its resource.
109
+ `tokenType: "Bearer"`, optional `expiresAt` (Unix epoch milliseconds or `null`
110
+ if unknown), and optional `scope`. `expiresIn` is a lifetime in seconds and is
111
+ anchored once at import. For a delayed import, provide the original `issuedAt`
112
+ in epoch milliseconds or its real absolute `expiresAt`; a numeric absolute
113
+ expiry takes precedence. Without either, the relative value means remaining
114
+ lifetime at import. Omitted expiry stays unknown. A fresh imported token is
115
+ used only for its resource.
111
116
  Discovery binds an expired or explicitly rejected grant before silent refresh,
112
117
  using the original configured client. Persisted sessions take precedence,
113
118
  including sessions whose tokens have been cleared; an import cannot revive them.
@@ -119,6 +124,14 @@ Explicit ID/secret values must agree with the imported response. Sessions and
119
124
  native registration stores retain arrays, issuance/expiry timestamps and JSON
120
125
  provider metadata. Registration input is copied, bounded to 64 KiB and 64
121
126
  levels, and rejects invalid standard field types and non-JSON extensions.
127
+ An optional registration `issuer` must match discovery and the persisted
128
+ authorization server exactly. Contradictory metadata fails without activating
129
+ or redeeming the grant. `client_secret_expires_at` uses Unix epoch seconds;
130
+ zero means no expiry. Live access tokens remain usable after secret expiry,
131
+ but an expired secret is never submitted for refresh. Native DCR can replace
132
+ an expired registration during explicit authorization; caller-owned imports
133
+ must be updated. Headless requests retain the old record and report recovery
134
+ is required without creating a pending refresh marker.
122
135
  Set `client.tokenEndpointAuthMethod` to `none`, `client_secret_post` or
123
136
  `client_secret_basic`; a full registration can supply the same field as
124
137
  `token_endpoint_auth_method`. Public clients never transmit a stored secret.
@@ -43,7 +43,7 @@ export function createDefaultOAuthClientProvider(options) {
43
43
  }
44
44
  const initialGrant = options.initialGrant === undefined ? undefined : {
45
45
  resource: canonicalizeResourceIndicator(options.initialGrant.resource),
46
- tokens: normalizeStoredTokens(options.initialGrant.tokens),
46
+ tokens: normalizeImportedTokens(options.initialGrant.tokens, now),
47
47
  client: configuredClient
48
48
  };
49
49
  if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
@@ -127,6 +127,10 @@ export function createDefaultOAuthClientProvider(options) {
127
127
  const canonicalResource = canonicalizeResourceIndicator(resource);
128
128
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
129
129
  let session = await loadSession(canonicalResource);
130
+ if (session !== null)
131
+ assertRegistrationIssuer(session.client, session.authorizationServer);
132
+ if (configuredClient !== null && discovery !== undefined)
133
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
130
134
  if (session !== null && initialGrant?.resource === canonicalResource)
131
135
  initialGrantConsumed = true;
132
136
  signal?.throwIfAborted();
@@ -173,6 +177,11 @@ export function createDefaultOAuthClientProvider(options) {
173
177
  if (session?.tokens?.refreshToken !== undefined &&
174
178
  sessionDiscovery !== undefined &&
175
179
  (forceRefresh || isExpired(session.tokens, now))) {
180
+ if (hasExpiredClientSecret(session.client, now)) {
181
+ if (!allowInteractive || options.allowInteractive === false || options.client.mode === "static" || configuredClient?.registration !== undefined)
182
+ throw new Error("OAuth client secret has expired; authorize again or update the imported registration");
183
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
184
+ }
176
185
  session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch, signal);
177
186
  if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
178
187
  return session;
@@ -343,6 +352,9 @@ export function createDefaultOAuthClientProvider(options) {
343
352
  if (configuredClient === null) {
344
353
  throw new Error("OAuth client_id must not be blank");
345
354
  }
355
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
356
+ if (hasExpiredClientSecret(configuredClient, now))
357
+ throw new Error("OAuth client secret has expired; update the imported registration");
346
358
  return {
347
359
  kind: "static",
348
360
  fromStoredRegistration: false,
@@ -357,7 +369,14 @@ export function createDefaultOAuthClientProvider(options) {
357
369
  client: configuredClient
358
370
  };
359
371
  }
360
- const storedClient = await loadRegisteredClient(discovery.authorizationServer);
372
+ let storedClient = await loadRegisteredClient(discovery.authorizationServer);
373
+ if (storedClient !== null) {
374
+ assertRegistrationIssuer(storedClient, discovery.authorizationServer);
375
+ if (hasExpiredClientSecret(storedClient, now)) {
376
+ await clearRegisteredClient(discovery.authorizationServer);
377
+ storedClient = null;
378
+ }
379
+ }
361
380
  if (storedClient !== null) {
362
381
  return {
363
382
  kind: "dynamic",
@@ -366,7 +385,7 @@ export function createDefaultOAuthClientProvider(options) {
366
385
  };
367
386
  }
368
387
  if (registrationEndpoint === undefined) {
369
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
388
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
370
389
  return {
371
390
  kind: "dynamic",
372
391
  fromStoredRegistration: true,
@@ -375,7 +394,7 @@ export function createDefaultOAuthClientProvider(options) {
375
394
  }
376
395
  throw new Error("Authorization server metadata is missing registration_endpoint");
377
396
  }
378
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
397
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
379
398
  const isConfiguredStaticFallback = configuredClient !== null &&
380
399
  existingSession.client.clientId === configuredClient.clientId &&
381
400
  existingSession.client.clientSecret === configuredClient.clientSecret;
@@ -415,6 +434,9 @@ export function createDefaultOAuthClientProvider(options) {
415
434
  ...(responseMethod === undefined ? {} : { tokenEndpointAuthMethod: responseMethod }),
416
435
  registration
417
436
  };
437
+ assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
438
+ if (hasExpiredClientSecret(registeredClient, now))
439
+ throw new Error("OAuth client secret has expired in the registration response");
418
440
  await saveRegisteredClient(discovery.authorizationServer, registeredClient);
419
441
  return {
420
442
  kind: "dynamic",
@@ -528,6 +550,21 @@ function normalizeLoadedSession(session) {
528
550
  tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
529
551
  };
530
552
  }
553
+ function normalizeImportedTokens(value, now) {
554
+ if (!isObjectRecord(value))
555
+ return undefined;
556
+ const absolute = getOwnEntry(value, "expiresAt");
557
+ const lifetime = getOwnEntry(value, "expiresIn");
558
+ const issuedAt = getOwnEntry(value, "issuedAt");
559
+ if (lifetime !== undefined && (typeof lifetime !== "number" || !Number.isSafeInteger(lifetime) || lifetime < 0))
560
+ throw new Error("OAuth initial grant has invalid relative expiry");
561
+ if (issuedAt !== undefined && (typeof issuedAt !== "number" || !Number.isSafeInteger(issuedAt) ||
562
+ Math.abs(issuedAt) > MAX_JS_DATE_MS))
563
+ throw new Error("OAuth initial grant has invalid issuance time");
564
+ const expiresAt = absolute !== undefined && absolute !== null ? absolute :
565
+ lifetime === undefined ? null : (issuedAt === undefined ? now() : issuedAt) + lifetime * 1000;
566
+ return normalizeStoredTokens({ ...value, expiresAt });
567
+ }
531
568
  function normalizeStoredTokens(value) {
532
569
  if (value === undefined || !isObjectRecord(value)) {
533
570
  return undefined;
@@ -689,6 +726,17 @@ function assertRequestMatchesResource(requestUrl, resource) {
689
726
  throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
690
727
  }
691
728
  }
729
+ function assertRegistrationIssuer(client, issuer) {
730
+ const registrationIssuer = client.registration === undefined ? undefined : getOwnString(client.registration, "issuer");
731
+ if (registrationIssuer !== undefined && registrationIssuer !== issuer)
732
+ throw new Error("OAuth client registration issuer does not match the authorization server");
733
+ }
734
+ function hasExpiredClientSecret(client, now) {
735
+ if (client.clientSecret === undefined || client.tokenEndpointAuthMethod === "none" || client.registration === undefined)
736
+ return false;
737
+ const expiry = getOwnEntry(client.registration, "client_secret_expires_at");
738
+ return typeof expiry === "number" && expiry !== 0 && expiry <= now() / 1000;
739
+ }
692
740
  function getSupportedTokenAuthMethods(metadata) {
693
741
  const value = getOwnEntry(metadata, "token_endpoint_auth_methods_supported");
694
742
  if (value === undefined)
@@ -70,6 +70,14 @@ export interface StoredOAuthTokens {
70
70
  expiresAt: number | null;
71
71
  scope?: string;
72
72
  }
73
+ /** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
74
+ export interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
75
+ expiresAt?: number | null;
76
+ /** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
77
+ expiresIn?: number;
78
+ /** Original issuance time in Unix epoch milliseconds. */
79
+ issuedAt?: number;
80
+ }
73
81
  /** Full RFC 7591 response, including JSON provider extensions. */
74
82
  export interface OAuthClientRegistration extends Record<string, unknown> {
75
83
  client_id: string;
@@ -140,7 +148,7 @@ export interface DefaultOAuthClientProviderOptions {
140
148
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
141
149
  initialGrant?: {
142
150
  resource: string;
143
- tokens: StoredOAuthTokens;
151
+ tokens: ImportedOAuthTokens;
144
152
  };
145
153
  browser: {
146
154
  openBrowser?(url: string): Promise<void>;
@@ -6,7 +6,7 @@ export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
6
6
  export { OAuthError, } from "./client/token-endpoint.js";
7
7
  export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
8
8
  export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
9
- export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, OAuthTokenEndpointAuthMethod, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, } from "./client/types.js";
9
+ export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, OAuthTokenEndpointAuthMethod, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, ImportedOAuthTokens, } from "./client/types.js";
10
10
  export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessToken, } from "./server/jwks-token-verifier.js";
11
11
  export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
12
12
  export { readBoundedResponseText } from "./http-response.js";
@@ -173,6 +173,14 @@ interface StoredOAuthTokens {
173
173
  expiresAt: number | null;
174
174
  scope?: string;
175
175
  }
176
+ /** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
177
+ interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
178
+ expiresAt?: number | null;
179
+ /** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
180
+ expiresIn?: number;
181
+ /** Original issuance time in Unix epoch milliseconds. */
182
+ issuedAt?: number;
183
+ }
176
184
  /** Full RFC 7591 response, including JSON provider extensions. */
177
185
  interface OAuthClientRegistration extends Record<string, unknown> {
178
186
  client_id: string;
@@ -243,7 +251,7 @@ interface DefaultOAuthClientProviderOptions {
243
251
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
244
252
  initialGrant?: {
245
253
  resource: string;
246
- tokens: StoredOAuthTokens;
254
+ tokens: ImportedOAuthTokens;
247
255
  };
248
256
  browser: {
249
257
  openBrowser?(url: string): Promise<void>;
@@ -4999,7 +4999,7 @@ function createDefaultOAuthClientProvider(options) {
4999
4999
  }
5000
5000
  const initialGrant = options.initialGrant === void 0 ? void 0 : {
5001
5001
  resource: canonicalizeResourceIndicator(options.initialGrant.resource),
5002
- tokens: normalizeStoredTokens(options.initialGrant.tokens),
5002
+ tokens: normalizeImportedTokens(options.initialGrant.tokens, now),
5003
5003
  client: configuredClient
5004
5004
  };
5005
5005
  if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
@@ -5077,6 +5077,10 @@ function createDefaultOAuthClientProvider(options) {
5077
5077
  const canonicalResource = canonicalizeResourceIndicator(resource);
5078
5078
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
5079
5079
  let session = await loadSession(canonicalResource);
5080
+ if (session !== null)
5081
+ assertRegistrationIssuer(session.client, session.authorizationServer);
5082
+ if (configuredClient !== null && discovery !== void 0)
5083
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
5080
5084
  if (session !== null && initialGrant?.resource === canonicalResource)
5081
5085
  initialGrantConsumed = true;
5082
5086
  signal?.throwIfAborted();
@@ -5122,6 +5126,11 @@ function createDefaultOAuthClientProvider(options) {
5122
5126
  return session;
5123
5127
  }
5124
5128
  if (session?.tokens?.refreshToken !== void 0 && sessionDiscovery !== void 0 && (forceRefresh || isExpired(session.tokens, now))) {
5129
+ if (hasExpiredClientSecret(session.client, now)) {
5130
+ if (!allowInteractive || options.allowInteractive === false || options.client.mode === "static" || configuredClient?.registration !== void 0)
5131
+ throw new Error("OAuth client secret has expired; authorize again or update the imported registration");
5132
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch2, signal);
5133
+ }
5125
5134
  session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
5126
5135
  if (session?.tokens !== void 0 && !isExpired(session.tokens, now)) {
5127
5136
  return session;
@@ -5289,6 +5298,9 @@ function createDefaultOAuthClientProvider(options) {
5289
5298
  if (configuredClient === null) {
5290
5299
  throw new Error("OAuth client_id must not be blank");
5291
5300
  }
5301
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
5302
+ if (hasExpiredClientSecret(configuredClient, now))
5303
+ throw new Error("OAuth client secret has expired; update the imported registration");
5292
5304
  return {
5293
5305
  kind: "static",
5294
5306
  fromStoredRegistration: false,
@@ -5303,7 +5315,14 @@ function createDefaultOAuthClientProvider(options) {
5303
5315
  client: configuredClient
5304
5316
  };
5305
5317
  }
5306
- const storedClient = await loadRegisteredClient(discovery.authorizationServer);
5318
+ let storedClient = await loadRegisteredClient(discovery.authorizationServer);
5319
+ if (storedClient !== null) {
5320
+ assertRegistrationIssuer(storedClient, discovery.authorizationServer);
5321
+ if (hasExpiredClientSecret(storedClient, now)) {
5322
+ await clearRegisteredClient(discovery.authorizationServer);
5323
+ storedClient = null;
5324
+ }
5325
+ }
5307
5326
  if (storedClient !== null) {
5308
5327
  return {
5309
5328
  kind: "dynamic",
@@ -5312,7 +5331,7 @@ function createDefaultOAuthClientProvider(options) {
5312
5331
  };
5313
5332
  }
5314
5333
  if (registrationEndpoint === void 0) {
5315
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
5334
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
5316
5335
  return {
5317
5336
  kind: "dynamic",
5318
5337
  fromStoredRegistration: true,
@@ -5321,7 +5340,7 @@ function createDefaultOAuthClientProvider(options) {
5321
5340
  }
5322
5341
  throw new Error("Authorization server metadata is missing registration_endpoint");
5323
5342
  }
5324
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
5343
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
5325
5344
  const isConfiguredStaticFallback = configuredClient !== null && existingSession.client.clientId === configuredClient.clientId && existingSession.client.clientSecret === configuredClient.clientSecret;
5326
5345
  if (!isConfiguredStaticFallback) {
5327
5346
  await saveRegisteredClient(discovery.authorizationServer, existingSession.client);
@@ -5357,6 +5376,9 @@ function createDefaultOAuthClientProvider(options) {
5357
5376
  ...responseMethod === void 0 ? {} : { tokenEndpointAuthMethod: responseMethod },
5358
5377
  registration
5359
5378
  };
5379
+ assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
5380
+ if (hasExpiredClientSecret(registeredClient, now))
5381
+ throw new Error("OAuth client secret has expired in the registration response");
5360
5382
  await saveRegisteredClient(discovery.authorizationServer, registeredClient);
5361
5383
  return {
5362
5384
  kind: "dynamic",
@@ -5464,6 +5486,19 @@ function normalizeLoadedSession(session) {
5464
5486
  tokens: normalizeStoredTokens(getOwnEntry6(session, "tokens"))
5465
5487
  };
5466
5488
  }
5489
+ function normalizeImportedTokens(value, now) {
5490
+ if (!isObjectRecord3(value))
5491
+ return void 0;
5492
+ const absolute = getOwnEntry6(value, "expiresAt");
5493
+ const lifetime = getOwnEntry6(value, "expiresIn");
5494
+ const issuedAt = getOwnEntry6(value, "issuedAt");
5495
+ if (lifetime !== void 0 && (typeof lifetime !== "number" || !Number.isSafeInteger(lifetime) || lifetime < 0))
5496
+ throw new Error("OAuth initial grant has invalid relative expiry");
5497
+ if (issuedAt !== void 0 && (typeof issuedAt !== "number" || !Number.isSafeInteger(issuedAt) || Math.abs(issuedAt) > MAX_JS_DATE_MS3))
5498
+ throw new Error("OAuth initial grant has invalid issuance time");
5499
+ const expiresAt = absolute !== void 0 && absolute !== null ? absolute : lifetime === void 0 ? null : (issuedAt === void 0 ? now() : issuedAt) + lifetime * 1e3;
5500
+ return normalizeStoredTokens({ ...value, expiresAt });
5501
+ }
5467
5502
  function normalizeStoredTokens(value) {
5468
5503
  if (value === void 0 || !isObjectRecord3(value)) {
5469
5504
  return void 0;
@@ -5604,6 +5639,17 @@ function assertRequestMatchesResource(requestUrl, resource) {
5604
5639
  throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
5605
5640
  }
5606
5641
  }
5642
+ function assertRegistrationIssuer(client, issuer) {
5643
+ const registrationIssuer = client.registration === void 0 ? void 0 : getOwnString2(client.registration, "issuer");
5644
+ if (registrationIssuer !== void 0 && registrationIssuer !== issuer)
5645
+ throw new Error("OAuth client registration issuer does not match the authorization server");
5646
+ }
5647
+ function hasExpiredClientSecret(client, now) {
5648
+ if (client.clientSecret === void 0 || client.tokenEndpointAuthMethod === "none" || client.registration === void 0)
5649
+ return false;
5650
+ const expiry = getOwnEntry6(client.registration, "client_secret_expires_at");
5651
+ return typeof expiry === "number" && expiry !== 0 && expiry <= now() / 1e3;
5652
+ }
5607
5653
  function getSupportedTokenAuthMethods(metadata) {
5608
5654
  const value = getOwnEntry6(metadata, "token_endpoint_auth_methods_supported");
5609
5655
  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.34",
3
+ "version": "0.1.35",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",