tiny-http-mcp-server 0.1.27 → 0.1.29

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.27",
21
+ "version": "0.1.29",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -56,6 +56,10 @@ directory or filesystem adapter. Injected encrypted-file adapters need
56
56
  - Configurable salt, directory, and file name
57
57
  - File permissions: `0600`
58
58
  - Random IV per write
59
+ - `fileStore.throwOnInvalidDocument: true` makes malformed or unauthenticated
60
+ existing documents fail with a safe diagnostic; missing files still return
61
+ `null`. The default treats invalid documents as absent. Strict reads preserve
62
+ the existing file until an explicit reset or replacement.
59
63
 
60
64
  ### macOS Keychain
61
65
 
@@ -31,6 +31,8 @@ export interface EncryptedFileStoreInput {
31
31
  getMachineIdentity?: () => MachineIdentity | Promise<MachineIdentity>;
32
32
  getHomeDirectory?: () => string;
33
33
  getRandomBytes?: (size: number) => Buffer;
34
+ /** Fail closed instead of treating malformed or unauthenticated documents as absent. */
35
+ throwOnInvalidDocument?: boolean;
34
36
  }
35
37
  export declare class EncryptedFileStore implements SecretStore {
36
38
  private readonly fs;
@@ -40,6 +42,7 @@ export declare class EncryptedFileStore implements SecretStore {
40
42
  private readonly getMachineIdentity;
41
43
  private readonly getRandomBytes;
42
44
  private keyPromise;
45
+ private readonly throwOnInvalidDocument;
43
46
  constructor(input: EncryptedFileStoreInput);
44
47
  get(): Promise<string | null>;
45
48
  withLock<T>(operation: () => Promise<T>, options?: SecretStoreLockOptions): Promise<T>;
@@ -19,6 +19,7 @@ export class EncryptedFileStore {
19
19
  getMachineIdentity;
20
20
  getRandomBytes;
21
21
  keyPromise = null;
22
+ throwOnInvalidDocument;
22
23
  constructor(input) {
23
24
  this.fs = input.fs ?? fs;
24
25
  this.salt = input.salt;
@@ -37,6 +38,7 @@ export class EncryptedFileStore {
37
38
  }
38
39
  this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
39
40
  this.getRandomBytes = input.getRandomBytes ?? randomBytes;
41
+ this.throwOnInvalidDocument = input.throwOnInvalidDocument ?? false;
40
42
  }
41
43
  async get() {
42
44
  await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
@@ -52,6 +54,8 @@ export class EncryptedFileStore {
52
54
  }
53
55
  const document = parseEncryptedDocument(rawDocument);
54
56
  if (!document) {
57
+ if (this.throwOnInvalidDocument)
58
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
55
59
  return null;
56
60
  }
57
61
  const key = await this.getEncryptionKey();
@@ -61,6 +65,8 @@ export class EncryptedFileStore {
61
65
  const ciphertext = Buffer.from(document.ciphertext, "base64");
62
66
  if (iv.byteLength !== ENCRYPTION_IV_BYTES ||
63
67
  authTag.byteLength !== ENCRYPTION_AUTH_TAG_BYTES) {
68
+ if (this.throwOnInvalidDocument)
69
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
64
70
  return null;
65
71
  }
66
72
  const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
@@ -69,6 +75,8 @@ export class EncryptedFileStore {
69
75
  return plaintext.toString("utf8");
70
76
  }
71
77
  catch {
78
+ if (this.throwOnInvalidDocument)
79
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
72
80
  return null;
73
81
  }
74
82
  }
@@ -118,9 +118,23 @@ acquisition, while token and browser operations retain their own deadlines.
118
118
  The native `auth-store` session adapter implements this hook for both encrypted
119
119
  files and Keychain identities, including across independent processes. Locks
120
120
  cover the complete read, refresh/authorization and persisted winner. Dead-owner
121
- claims are recovered without stealing a live transaction. A process crash or
122
- cancellation after refresh redemption but before persistence is still an
123
- uncertain token outcome; recovery for that case is under development.
121
+ claims are recovered without stealing a live transaction.
122
+
123
+ Before sending a refresh request, the provider persists a tokenless session with
124
+ `refreshState: "pending"`, retaining the original client and discovery binding.
125
+ A successful response replaces it with the rotated grant. A crash, cancellation,
126
+ network disconnect or incomplete response leaves the marker, so another process
127
+ cannot replay a possibly consumed refresh token or revive the initial import.
128
+ Such a session requires fresh authorization. Interactive unauthorized handling
129
+ can recover it; headless requests fail with an explicit unknown-outcome error.
130
+ Only complete OAuth error responses establish a rejected request and allow a
131
+ transient retry or restoration of the original grant. Gateway error pages do not.
132
+
133
+ Native persisted OAuth reads always reject corrupt encrypted documents and
134
+ invalid stored JSON, with diagnostics that omit decrypted contents. They retain
135
+ the existing record for explicit reset rather than interpreting corruption as
136
+ an absent session and reviving an initial grant. Caller file-backend settings
137
+ cannot disable this policy.
124
138
 
125
139
  ## Environment Variables
126
140
 
@@ -23,7 +23,13 @@ export function createAuthStoreSessionStore(options = {}) {
23
23
  if (value === null) {
24
24
  return null;
25
25
  }
26
- const parsed = JSON.parse(value);
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(value);
29
+ }
30
+ catch {
31
+ throw new Error("Stored OAuth session must be valid JSON; reset the store explicitly to recover");
32
+ }
27
33
  if (isStoredOAuthSession(parsed)) {
28
34
  return parsed;
29
35
  }
@@ -47,7 +53,13 @@ export function createAuthStoreClientStore(options) {
47
53
  if (value === null) {
48
54
  return null;
49
55
  }
50
- const parsed = JSON.parse(value);
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(value);
59
+ }
60
+ catch {
61
+ throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
62
+ }
51
63
  const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : undefined;
52
64
  if (clientId !== undefined) {
53
65
  const client = { clientId };
@@ -75,6 +87,7 @@ function createNamedSecretStore(key, options, defaults) {
75
87
  const parsedFilePath = configuredFilePath === undefined ? null : path.parse(configuredFilePath);
76
88
  const fileStore = {
77
89
  ...options.fileStore,
90
+ throwOnInvalidDocument: true,
78
91
  filePath: parsedFilePath === null
79
92
  ? undefined
80
93
  : path.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
@@ -125,6 +138,8 @@ function isStoredOAuthSession(value) {
125
138
  isNonBlankOwnString(value, "authorizationServer") &&
126
139
  isStoredOAuthClient(getOwnEntry(value, "client")) &&
127
140
  isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
141
+ (getOwnEntry(value, "refreshState") === undefined ||
142
+ (getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
128
143
  isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
129
144
  }
130
145
  function isStoredOAuthClient(value) {
@@ -140,6 +140,11 @@ export function createDefaultOAuthClientProvider(options) {
140
140
  if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
141
141
  forceRefresh = false;
142
142
  const sessionDiscovery = resolveDiscovery(discovery, session);
143
+ if (session?.refreshState === "pending") {
144
+ if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
145
+ throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
146
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
147
+ }
143
148
  if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
144
149
  return session;
145
150
  }
@@ -169,6 +174,9 @@ export function createDefaultOAuthClientProvider(options) {
169
174
  if (session.tokens?.refreshToken === undefined) {
170
175
  return session;
171
176
  }
177
+ const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
178
+ await saveSession(resource, pendingSession);
179
+ signal?.throwIfAborted();
172
180
  let refreshAttempted = false;
173
181
  let refreshedTokens;
174
182
  while (true) {
@@ -186,7 +194,11 @@ export function createDefaultOAuthClientProvider(options) {
186
194
  }
187
195
  catch (error) {
188
196
  signal?.throwIfAborted();
189
- if (error instanceof OAuthError && error.error === "invalid_grant") {
197
+ // Network errors, lost/malformed bodies and gateway failures cannot
198
+ // establish whether a rotating refresh token was already consumed.
199
+ if (!(error instanceof OAuthError) || !error.outcomeKnown)
200
+ throw error;
201
+ if (error.error === "invalid_grant") {
190
202
  const clearedSession = clearSessionTokens(session);
191
203
  await saveSession(resource, clearedSession);
192
204
  return clearedSession;
@@ -200,6 +212,7 @@ export function createDefaultOAuthClientProvider(options) {
200
212
  refreshAttempted = true;
201
213
  continue;
202
214
  }
215
+ await saveSession(resource, session);
203
216
  throw error;
204
217
  }
205
218
  }
@@ -457,6 +470,7 @@ function sameTokenGrant(left, right) {
457
470
  function clearSessionTokens(session) {
458
471
  const nextSession = { ...session };
459
472
  delete nextSession.tokens;
473
+ delete nextSession.refreshState;
460
474
  return nextSession;
461
475
  }
462
476
  function hasCachedAccessToken(session) {
@@ -466,6 +480,9 @@ function normalizeLoadedSession(session) {
466
480
  if (session === null) {
467
481
  return null;
468
482
  }
483
+ const refreshState = getOwnEntry(session, "refreshState");
484
+ if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
485
+ throw new Error("Stored OAuth refresh state is invalid");
469
486
  const client = normalizeStoredClient(getOwnEntry(session, "client"));
470
487
  if (client === null) {
471
488
  return { ...session, client: { clientId: "" }, tokens: undefined };
@@ -13,7 +13,9 @@ export declare class OAuthError extends Error {
13
13
  readonly status: number;
14
14
  readonly retryable: boolean;
15
15
  readonly terminal: boolean;
16
- constructor(shape: OAuthErrorShape, status: number);
16
+ /** True only when a complete OAuth error response establishes rejection. */
17
+ readonly outcomeKnown: boolean;
18
+ constructor(shape: OAuthErrorShape, status: number, outcomeKnown?: boolean);
17
19
  }
18
20
  export declare function isRetryableOAuthError(error: unknown): error is OAuthError;
19
21
  export declare function exchangeAuthorizationCode(input: {
@@ -11,7 +11,9 @@ export class OAuthError extends Error {
11
11
  status;
12
12
  retryable;
13
13
  terminal;
14
- constructor(shape, status) {
14
+ /** True only when a complete OAuth error response establishes rejection. */
15
+ outcomeKnown;
16
+ constructor(shape, status, outcomeKnown = true) {
15
17
  super(shape.error_description ?? shape.error);
16
18
  this.name = "OAuthError";
17
19
  this.error = shape.error;
@@ -22,6 +24,7 @@ export class OAuthError extends Error {
22
24
  this.status = status;
23
25
  this.retryable = isRetryableOAuthError(this);
24
26
  this.terminal = !this.retryable;
27
+ this.outcomeKnown = outcomeKnown;
25
28
  }
26
29
  }
27
30
  export function isRetryableOAuthError(error) {
@@ -147,7 +150,8 @@ export async function readOAuthJsonObjectResponse(response, signal) {
147
150
  }
148
151
  const record = payload;
149
152
  if (!response.ok) {
150
- throw new OAuthError(readOAuthError(record, fallbackError.error), response.status);
153
+ const error = getOwnEntry(record, "error");
154
+ throw new OAuthError(readOAuthError(record, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
151
155
  }
152
156
  return record;
153
157
  }
@@ -166,7 +170,7 @@ function getOwnEntry(record, key) {
166
170
  }
167
171
  function createFallbackOAuthError(status) {
168
172
  const error = status === 503 ? "temporarily_unavailable" : "server_error";
169
- return new OAuthError({ error }, status);
173
+ return new OAuthError({ error }, status, false);
170
174
  }
171
175
  function normalizeBearerTokenType(value) {
172
176
  if (typeof value !== "string") {
@@ -78,6 +78,8 @@ export interface StoredOAuthSession {
78
78
  clientSecret?: string;
79
79
  };
80
80
  tokens?: StoredOAuthTokens;
81
+ /** A refresh was begun; its winning response may not have been persisted. */
82
+ refreshState?: "pending";
81
83
  discovery: {
82
84
  resourceMetadataUrl: string;
83
85
  resourceMetadata: Record<string, unknown>;
@@ -70,6 +70,8 @@ interface EncryptedFileStoreInput {
70
70
  getMachineIdentity?: () => MachineIdentity | Promise<MachineIdentity>;
71
71
  getHomeDirectory?: () => string;
72
72
  getRandomBytes?: (size: number) => Buffer;
73
+ /** Fail closed instead of treating malformed or unauthenticated documents as absent. */
74
+ throwOnInvalidDocument?: boolean;
73
75
  }
74
76
 
75
77
  interface KeychainCommandResult {
@@ -179,6 +181,8 @@ interface StoredOAuthSession {
179
181
  clientSecret?: string;
180
182
  };
181
183
  tokens?: StoredOAuthTokens;
184
+ /** A refresh was begun; its winning response may not have been persisted. */
185
+ refreshState?: "pending";
182
186
  discovery: {
183
187
  resourceMetadataUrl: string;
184
188
  resourceMetadata: Record<string, unknown>;
@@ -3575,6 +3575,7 @@ var EncryptedFileStore = class {
3575
3575
  getMachineIdentity;
3576
3576
  getRandomBytes;
3577
3577
  keyPromise = null;
3578
+ throwOnInvalidDocument;
3578
3579
  constructor(input) {
3579
3580
  this.fs = input.fs ?? fs;
3580
3581
  this.salt = input.salt;
@@ -3592,6 +3593,7 @@ var EncryptedFileStore = class {
3592
3593
  }
3593
3594
  this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
3594
3595
  this.getRandomBytes = input.getRandomBytes ?? randomBytes;
3596
+ this.throwOnInvalidDocument = input.throwOnInvalidDocument ?? false;
3595
3597
  }
3596
3598
  async get() {
3597
3599
  await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
@@ -3606,6 +3608,8 @@ var EncryptedFileStore = class {
3606
3608
  }
3607
3609
  const document = parseEncryptedDocument(rawDocument);
3608
3610
  if (!document) {
3611
+ if (this.throwOnInvalidDocument)
3612
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
3609
3613
  return null;
3610
3614
  }
3611
3615
  const key2 = await this.getEncryptionKey();
@@ -3614,6 +3618,8 @@ var EncryptedFileStore = class {
3614
3618
  const authTag = Buffer.from(document.authTag, "base64");
3615
3619
  const ciphertext = Buffer.from(document.ciphertext, "base64");
3616
3620
  if (iv.byteLength !== ENCRYPTION_IV_BYTES || authTag.byteLength !== ENCRYPTION_AUTH_TAG_BYTES) {
3621
+ if (this.throwOnInvalidDocument)
3622
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
3617
3623
  return null;
3618
3624
  }
3619
3625
  const decipher = createDecipheriv(ENCRYPTION_ALGORITHM, key2, iv);
@@ -3621,6 +3627,8 @@ var EncryptedFileStore = class {
3621
3627
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
3622
3628
  return plaintext.toString("utf8");
3623
3629
  } catch {
3630
+ if (this.throwOnInvalidDocument)
3631
+ throw new Error("Invalid encrypted credential document; reset the store explicitly to recover");
3624
3632
  return null;
3625
3633
  }
3626
3634
  }
@@ -4083,7 +4091,12 @@ function createAuthStoreSessionStore(options = {}) {
4083
4091
  if (value === null) {
4084
4092
  return null;
4085
4093
  }
4086
- const parsed = JSON.parse(value);
4094
+ let parsed;
4095
+ try {
4096
+ parsed = JSON.parse(value);
4097
+ } catch {
4098
+ throw new Error("Stored OAuth session must be valid JSON; reset the store explicitly to recover");
4099
+ }
4087
4100
  if (isStoredOAuthSession(parsed)) {
4088
4101
  return parsed;
4089
4102
  }
@@ -4107,7 +4120,12 @@ function createAuthStoreClientStore(options) {
4107
4120
  if (value === null) {
4108
4121
  return null;
4109
4122
  }
4110
- const parsed = JSON.parse(value);
4123
+ let parsed;
4124
+ try {
4125
+ parsed = JSON.parse(value);
4126
+ } catch {
4127
+ throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
4128
+ }
4111
4129
  const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : void 0;
4112
4130
  if (clientId !== void 0) {
4113
4131
  const client = { clientId };
@@ -4134,6 +4152,7 @@ function createNamedSecretStore(key2, options, defaults) {
4134
4152
  const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
4135
4153
  const fileStore = {
4136
4154
  ...options.fileStore,
4155
+ throwOnInvalidDocument: true,
4137
4156
  filePath: parsedFilePath === null ? void 0 : path4.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
4138
4157
  salt: options.fileStore?.salt ?? defaults.salt,
4139
4158
  defaultDirectory: options.fileStore?.defaultDirectory || defaults.directory,
@@ -4176,7 +4195,7 @@ function isStoredOAuthSession(value) {
4176
4195
  if (!isObjectRecord(value)) {
4177
4196
  return false;
4178
4197
  }
4179
- return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
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"));
4180
4199
  }
4181
4200
  function isStoredOAuthClient(value) {
4182
4201
  if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
@@ -4605,7 +4624,9 @@ var OAuthError = class extends Error {
4605
4624
  status;
4606
4625
  retryable;
4607
4626
  terminal;
4608
- constructor(shape, status) {
4627
+ /** True only when a complete OAuth error response establishes rejection. */
4628
+ outcomeKnown;
4629
+ constructor(shape, status, outcomeKnown = true) {
4609
4630
  super(shape.error_description ?? shape.error);
4610
4631
  this.name = "OAuthError";
4611
4632
  this.error = shape.error;
@@ -4616,6 +4637,7 @@ var OAuthError = class extends Error {
4616
4637
  this.status = status;
4617
4638
  this.retryable = isRetryableOAuthError(this);
4618
4639
  this.terminal = !this.retryable;
4640
+ this.outcomeKnown = outcomeKnown;
4619
4641
  }
4620
4642
  };
4621
4643
  function isRetryableOAuthError(error) {
@@ -4730,7 +4752,8 @@ async function readOAuthJsonObjectResponse(response, signal) {
4730
4752
  }
4731
4753
  const record2 = payload;
4732
4754
  if (!response.ok) {
4733
- throw new OAuthError(readOAuthError(record2, fallbackError.error), response.status);
4755
+ const error = getOwnEntry5(record2, "error");
4756
+ throw new OAuthError(readOAuthError(record2, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
4734
4757
  }
4735
4758
  return record2;
4736
4759
  }
@@ -4749,7 +4772,7 @@ function getOwnEntry5(record2, key2) {
4749
4772
  }
4750
4773
  function createFallbackOAuthError(status) {
4751
4774
  const error = status === 503 ? "temporarily_unavailable" : "server_error";
4752
- return new OAuthError({ error }, status);
4775
+ return new OAuthError({ error }, status, false);
4753
4776
  }
4754
4777
  function normalizeBearerTokenType(value) {
4755
4778
  if (typeof value !== "string") {
@@ -4935,6 +4958,11 @@ function createDefaultOAuthClientProvider(options) {
4935
4958
  if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
4936
4959
  forceRefresh = false;
4937
4960
  const sessionDiscovery = resolveDiscovery(discovery, session);
4961
+ if (session?.refreshState === "pending") {
4962
+ if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
4963
+ throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
4964
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch2, signal);
4965
+ }
4938
4966
  if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
4939
4967
  return session;
4940
4968
  }
@@ -4962,6 +4990,9 @@ function createDefaultOAuthClientProvider(options) {
4962
4990
  if (session.tokens?.refreshToken === void 0) {
4963
4991
  return session;
4964
4992
  }
4993
+ const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
4994
+ await saveSession(resource, pendingSession);
4995
+ signal?.throwIfAborted();
4965
4996
  let refreshAttempted = false;
4966
4997
  let refreshedTokens;
4967
4998
  while (true) {
@@ -4979,7 +5010,9 @@ function createDefaultOAuthClientProvider(options) {
4979
5010
  break;
4980
5011
  } catch (error) {
4981
5012
  signal?.throwIfAborted();
4982
- if (error instanceof OAuthError && error.error === "invalid_grant") {
5013
+ if (!(error instanceof OAuthError) || !error.outcomeKnown)
5014
+ throw error;
5015
+ if (error.error === "invalid_grant") {
4983
5016
  const clearedSession = clearSessionTokens(session);
4984
5017
  await saveSession(resource, clearedSession);
4985
5018
  return clearedSession;
@@ -4993,6 +5026,7 @@ function createDefaultOAuthClientProvider(options) {
4993
5026
  refreshAttempted = true;
4994
5027
  continue;
4995
5028
  }
5029
+ await saveSession(resource, session);
4996
5030
  throw error;
4997
5031
  }
4998
5032
  }
@@ -5239,6 +5273,7 @@ function sameTokenGrant(left, right) {
5239
5273
  function clearSessionTokens(session) {
5240
5274
  const nextSession = { ...session };
5241
5275
  delete nextSession.tokens;
5276
+ delete nextSession.refreshState;
5242
5277
  return nextSession;
5243
5278
  }
5244
5279
  function hasCachedAccessToken(session) {
@@ -5248,6 +5283,9 @@ function normalizeLoadedSession(session) {
5248
5283
  if (session === null) {
5249
5284
  return null;
5250
5285
  }
5286
+ const refreshState = getOwnEntry6(session, "refreshState");
5287
+ if (refreshState !== void 0 && (refreshState !== "pending" || getOwnEntry6(session, "tokens") !== void 0))
5288
+ throw new Error("Stored OAuth refresh state is invalid");
5251
5289
  const client = normalizeStoredClient(getOwnEntry6(session, "client"));
5252
5290
  if (client === null) {
5253
5291
  return { ...session, client: { clientId: "" }, tokens: void 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",