tiny-http-mcp-server 0.1.26 → 0.1.28

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.26",
21
+ "version": "0.1.28",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -25,6 +25,22 @@ const value = await store.get(); // "secret-value"
25
25
  await store.delete();
26
26
  ```
27
27
 
28
+ Both built-in backends expose `store.withLock(operation, { signal, timeoutMs })`
29
+ for transactions spanning a read, external operation and write. Independent
30
+ instances and processes serialize the same encrypted file or Keychain
31
+ service/account; unrelated identities proceed independently. The default
32
+ acquisition timeout is 30 seconds. Cancellation during acquisition does not
33
+ release the active owner's lock. Individual `get`, `set` and `delete` calls do
34
+ not implicitly acquire it.
35
+
36
+ Locks use private filesystem claim directories, containing PID/random names
37
+ and numeric tickets without credentials. Dead-owner claims are recovered;
38
+ live claims are never stolen because of age. Empty directories remain so an
39
+ arriving contender cannot race directory removal. Keychain lock storage defaults
40
+ to `~/.auth-store/keychain-locks`; `keychainStore.lock` can select another
41
+ directory or filesystem adapter. Injected encrypted-file adapters need
42
+ `readdir` support when using transactions.
43
+
28
44
  ## Backends
29
45
 
30
46
  | `backendEnvVar` value | Platform | Backend |
@@ -1,9 +1,11 @@
1
1
  import type { SecretStore } from "./types.js";
2
+ import { type SecretStoreLockOptions } from "./transaction-lock.js";
2
3
  export interface MachineIdentity {
3
4
  hostname: string;
4
5
  username: string;
5
6
  }
6
7
  export interface EncryptedFileStoreFileSystem {
8
+ readdir?(path: string): Promise<string[]>;
7
9
  readFile(path: string, encoding: BufferEncoding): Promise<string>;
8
10
  writeFile(path: string, data: string | NodeJS.ArrayBufferView, options?: {
9
11
  encoding?: BufferEncoding;
@@ -40,6 +42,7 @@ export declare class EncryptedFileStore implements SecretStore {
40
42
  private keyPromise;
41
43
  constructor(input: EncryptedFileStoreInput);
42
44
  get(): Promise<string | null>;
45
+ withLock<T>(operation: () => Promise<T>, options?: SecretStoreLockOptions): Promise<T>;
43
46
  set(value: string): Promise<void>;
44
47
  delete(): Promise<void>;
45
48
  private assertCredentialPathHasNoSymbolicLinks;
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
3
3
  import { homedir, hostname, userInfo } from "node:os";
4
4
  import path from "node:path";
5
5
  import { hasOwnErrorCode } from "./error-codes.js";
6
+ import { withSecretStoreFileLock } from "./transaction-lock.js";
6
7
  const derivedKeyCache = new Map();
7
8
  const ENCRYPTION_ALGORITHM = "aes-256-gcm";
8
9
  const ENCRYPTION_VERSION = 1;
@@ -71,6 +72,12 @@ export class EncryptedFileStore {
71
72
  return null;
72
73
  }
73
74
  }
75
+ async withLock(operation, options = {}) {
76
+ await this.assertCredentialPathHasNoSymbolicLinks(`${this.filePath}.lock`);
77
+ if (this.fs.readdir === undefined)
78
+ throw new Error("Secret-store transaction locks require filesystem readdir support");
79
+ return withSecretStoreFileLock(this.fs, `${this.filePath}.lock`, operation, options);
80
+ }
74
81
  async set(value) {
75
82
  await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
76
83
  const key = await this.getEncryptionKey();
@@ -1,6 +1,7 @@
1
1
  export { createSecretStore } from "./create-secret-store.js";
2
2
  export { EncryptedFileStore } from "./encrypted-file-store.js";
3
3
  export { KeychainStore } from "./keychain-store.js";
4
+ export type { SecretStoreLockOptions, SecretStoreLockFileSystem } from "./transaction-lock.js";
4
5
  export { key, MigratingSecretStore } from "./provider-store.js";
5
6
  export type { SecretStore, StoreBackend, CreateSecretStoreInput, CreateSecretStoreResult } from "./types.js";
6
7
  export type { MachineIdentity, EncryptedFileStoreInput, EncryptedFileStoreFileSystem } from "./encrypted-file-store.js";
@@ -1,4 +1,5 @@
1
1
  import type { SecretStore } from "./types.js";
2
+ import { type SecretStoreLockFileSystem, type SecretStoreLockOptions } from "./transaction-lock.js";
2
3
  export interface KeychainCommandResult {
3
4
  stdout: string;
4
5
  stderr: string;
@@ -12,13 +13,20 @@ export interface KeychainStoreInput {
12
13
  runCommand?: KeychainCommandRunner;
13
14
  service: string;
14
15
  account: string;
16
+ lock?: {
17
+ fs?: SecretStoreLockFileSystem;
18
+ directory?: string;
19
+ };
15
20
  }
16
21
  export declare class KeychainStore implements SecretStore {
17
22
  private readonly runCommand;
18
23
  private readonly service;
19
24
  private readonly account;
25
+ private readonly lockFs;
26
+ private readonly lockDirectory;
20
27
  constructor(input: KeychainStoreInput);
21
28
  get(): Promise<string | null>;
29
+ withLock<T>(operation: () => Promise<T>, options?: SecretStoreLockOptions): Promise<T>;
22
30
  set(value: string): Promise<void>;
23
31
  delete(): Promise<void>;
24
32
  private executeSecurityCommand;
@@ -1,14 +1,23 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { promises as nodeFs } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { withSecretStoreFileLock } from "./transaction-lock.js";
2
7
  const SECURITY_CLI = "security";
3
8
  const KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE = 44;
4
9
  export class KeychainStore {
5
10
  runCommand;
6
11
  service;
7
12
  account;
13
+ lockFs;
14
+ lockDirectory;
8
15
  constructor(input) {
9
16
  this.runCommand = input.runCommand ?? runSecurityCommand;
10
17
  this.service = input.service.trim();
11
18
  this.account = input.account.trim();
19
+ this.lockFs = input.lock?.fs ?? nodeFs;
20
+ this.lockDirectory = input.lock?.directory ?? path.join(homedir(), ".auth-store", "keychain-locks");
12
21
  if (this.service.length === 0) {
13
22
  throw new Error("Keychain service must not be empty");
14
23
  }
@@ -26,6 +35,10 @@ export class KeychainStore {
26
35
  }
27
36
  throw createSecurityCliFailure("read secret from macOS Keychain", result);
28
37
  }
38
+ async withLock(operation, options = {}) {
39
+ const identity = createHash("sha256").update(JSON.stringify([this.service, this.account])).digest("hex");
40
+ return withSecretStoreFileLock(this.lockFs, path.join(this.lockDirectory, identity), operation, options);
41
+ }
29
42
  async set(value) {
30
43
  if (value.includes("\n") || value.includes("\r")) {
31
44
  throw new Error("Keychain secrets cannot contain line breaks");
@@ -0,0 +1,24 @@
1
+ export interface SecretStoreLockOptions {
2
+ signal?: AbortSignal;
3
+ timeoutMs?: number;
4
+ }
5
+ export interface SecretStoreLockFileSystem {
6
+ mkdir(path: string, options?: {
7
+ recursive?: boolean;
8
+ mode?: number;
9
+ }): Promise<unknown>;
10
+ readdir(path: string): Promise<string[]>;
11
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
12
+ writeFile(path: string, value: string, options: {
13
+ encoding: BufferEncoding;
14
+ flag: string;
15
+ mode: number;
16
+ }): Promise<void>;
17
+ rename(from: string, to: string): Promise<void>;
18
+ unlink(path: string): Promise<void>;
19
+ lstat(path: string): Promise<{
20
+ isSymbolicLink(): boolean;
21
+ }>;
22
+ }
23
+ /** Filesystem bakery lock: unique claims allow dead-owner cleanup without deleting a replacement owner's lock. */
24
+ export declare function withSecretStoreFileLock<T>(fs: SecretStoreLockFileSystem, lockDirectory: string, operation: () => Promise<T>, options?: SecretStoreLockOptions): Promise<T>;
@@ -0,0 +1,152 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import { hasOwnErrorCode } from "./error-codes.js";
4
+ /** Filesystem bakery lock: unique claims allow dead-owner cleanup without deleting a replacement owner's lock. */
5
+ export async function withSecretStoreFileLock(fs, lockDirectory, operation, options = {}) {
6
+ const timeoutMs = options.timeoutMs ?? 30_000;
7
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 2_147_483_647)
8
+ throw new Error("Invalid secret-store transaction lock timeout");
9
+ options.signal?.throwIfAborted();
10
+ const deadline = performance.now() + timeoutMs;
11
+ const directory = path.resolve(lockDirectory);
12
+ await assertLockDirectoryPath(fs, directory);
13
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
14
+ await assertLockDirectoryPath(fs, directory);
15
+ const name = `${process.pid}-${randomUUID()}.claim`;
16
+ const claimPath = path.join(directory, name);
17
+ const temporaryPath = `${claimPath}.tmp`;
18
+ let claimed = true;
19
+ let temporaryCreated = false;
20
+ let outcome;
21
+ try {
22
+ try {
23
+ await fs.writeFile(claimPath, JSON.stringify({ ticket: null }), { encoding: "utf8", flag: "wx", mode: 0o600 });
24
+ }
25
+ catch (error) {
26
+ if (hasOwnErrorCode(error, "EEXIST"))
27
+ claimed = false;
28
+ throw error;
29
+ }
30
+ const existing = await readClaims(fs, directory, name);
31
+ const ticket = existing.reduce((max, claim) => Math.max(max, claim.ticket ?? 0), 0) + 1;
32
+ if (!Number.isSafeInteger(ticket))
33
+ throw new Error("Secret-store transaction lock ticket overflow");
34
+ temporaryCreated = true;
35
+ try {
36
+ await fs.writeFile(temporaryPath, JSON.stringify({ ticket }), { encoding: "utf8", flag: "wx", mode: 0o600 });
37
+ }
38
+ catch (error) {
39
+ if (hasOwnErrorCode(error, "EEXIST"))
40
+ temporaryCreated = false;
41
+ throw error;
42
+ }
43
+ await fs.rename(temporaryPath, claimPath);
44
+ temporaryCreated = false;
45
+ for (;;) {
46
+ options.signal?.throwIfAborted();
47
+ const peers = await readClaims(fs, directory, name);
48
+ if (!peers.some(peer => peer.ticket === null || peer.ticket < ticket || (peer.ticket === ticket && peer.name < name)))
49
+ break;
50
+ const remaining = deadline - performance.now();
51
+ if (remaining <= 0)
52
+ throw new Error("Timed out waiting for secret-store transaction lock");
53
+ await new Promise((resolve, reject) => {
54
+ const abort = () => { clearTimeout(timer); options.signal?.removeEventListener("abort", abort); reject(options.signal?.reason); };
55
+ const timer = setTimeout(() => { options.signal?.removeEventListener("abort", abort); resolve(); }, Math.min(10, remaining));
56
+ options.signal?.addEventListener("abort", abort, { once: true });
57
+ if (options.signal?.aborted)
58
+ abort();
59
+ });
60
+ }
61
+ options.signal?.throwIfAborted();
62
+ outcome = { result: await operation() };
63
+ }
64
+ catch (error) {
65
+ outcome = { error };
66
+ }
67
+ const cleanup = [];
68
+ for (const target of [...(temporaryCreated ? [temporaryPath] : []), ...(claimed ? [claimPath] : [])]) {
69
+ try {
70
+ await fs.unlink(target);
71
+ }
72
+ catch (error) {
73
+ if (!hasOwnErrorCode(error, "ENOENT"))
74
+ cleanup.push(error);
75
+ }
76
+ }
77
+ if (cleanup.length)
78
+ throw new AggregateError([...("error" in outcome ? [outcome.error] : []), ...cleanup], "Secret-store transaction lock cleanup failed");
79
+ if ("error" in outcome)
80
+ throw outcome.error;
81
+ return outcome.result;
82
+ }
83
+ async function assertNoSymbolicLink(fs, target) {
84
+ try {
85
+ if ((await fs.lstat(target)).isSymbolicLink())
86
+ throw new Error("Refusing secret-store transaction lock through symbolic link");
87
+ }
88
+ catch (error) {
89
+ if (!hasOwnErrorCode(error, "ENOENT"))
90
+ throw error;
91
+ }
92
+ }
93
+ async function assertLockDirectoryPath(fs, directory) {
94
+ const root = path.parse(directory).root;
95
+ const segments = directory.slice(root.length).split(path.sep).filter(Boolean);
96
+ let current = root;
97
+ for (const [index, segment] of segments.entries()) {
98
+ current = path.join(current, segment);
99
+ // Match encrypted credential paths: allow OS root aliases such as macOS /var.
100
+ if (index > 0 || segments.length === 1)
101
+ await assertNoSymbolicLink(fs, current);
102
+ }
103
+ }
104
+ async function readClaims(fs, directory, ownName) {
105
+ const claims = [];
106
+ for (const name of await fs.readdir(directory)) {
107
+ if (name === ownName || !name.endsWith(".claim"))
108
+ continue;
109
+ const pidText = name.slice(0, name.indexOf("-"));
110
+ const pid = Number(pidText);
111
+ if (!Number.isSafeInteger(pid) || pid < 1 || String(pid) !== pidText)
112
+ throw new Error("Malformed secret-store transaction lock owner");
113
+ const target = path.join(directory, name);
114
+ await assertNoSymbolicLink(fs, target);
115
+ let alive = true;
116
+ try {
117
+ process.kill(pid, 0);
118
+ }
119
+ catch (error) {
120
+ alive = !hasOwnErrorCode(error, "ESRCH");
121
+ }
122
+ if (!alive) {
123
+ try {
124
+ await fs.unlink(target);
125
+ }
126
+ catch (error) {
127
+ if (!hasOwnErrorCode(error, "ENOENT"))
128
+ throw error;
129
+ }
130
+ continue;
131
+ }
132
+ let ticket = null;
133
+ let raw;
134
+ try {
135
+ raw = await fs.readFile(target, "utf8");
136
+ }
137
+ catch (error) {
138
+ if (hasOwnErrorCode(error, "ENOENT"))
139
+ continue;
140
+ throw error;
141
+ }
142
+ try {
143
+ const value = JSON.parse(raw);
144
+ if (value !== null && typeof value === "object" && "ticket" in value &&
145
+ typeof value.ticket === "number" && Number.isSafeInteger(value.ticket) && value.ticket > 0)
146
+ ticket = value.ticket;
147
+ }
148
+ catch { /* Incomplete live claims remain in the choosing phase; never steal them by age. */ }
149
+ claims.push({ name, ticket });
150
+ }
151
+ return claims;
152
+ }
@@ -1,11 +1,13 @@
1
1
  import type { EncryptedFileStoreInput } from "./encrypted-file-store.js";
2
2
  import type { KeychainStoreInput } from "./keychain-store.js";
3
+ import type { SecretStoreLockOptions } from "./transaction-lock.js";
3
4
  export interface SecretStore {
4
5
  get(options?: {
5
6
  readOnly?: boolean;
6
7
  }): Promise<string | null>;
7
8
  set(value: string): Promise<void>;
8
9
  delete(): Promise<void>;
10
+ withLock?<T>(operation: () => Promise<T>, options?: SecretStoreLockOptions): Promise<T>;
9
11
  }
10
12
  export type StoreBackend = "file" | "keychain";
11
13
  export interface CreateSecretStoreInput {
@@ -115,7 +115,20 @@ active owner's lock. Custom stores may implement
115
115
  transaction across store instances or processes. The hook must honor acquisition
116
116
  cancellation and keep the lock until the operation settles. The timeout bounds
117
117
  acquisition, while token and browser operations retain their own deadlines.
118
- Cross-process locking for the native secret-store backend is under development.
118
+ The native `auth-store` session adapter implements this hook for both encrypted
119
+ files and Keychain identities, including across independent processes. Locks
120
+ cover the complete read, refresh/authorization and persisted winner. Dead-owner
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.
119
132
 
120
133
  ## Environment Variables
121
134
 
@@ -11,6 +11,12 @@ const DEFAULT_CLIENT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth-clients";
11
11
  const MAX_JS_DATE_MS = 8_640_000_000_000_000;
12
12
  export function createAuthStoreSessionStore(options = {}) {
13
13
  return {
14
+ async withLock(resource, operation, lockOptions) {
15
+ const store = createResourceSecretStore(resource, options);
16
+ if (store.withLock === undefined)
17
+ throw new Error("OAuth secret-store backend does not support transaction locks");
18
+ return store.withLock(operation, lockOptions);
19
+ },
14
20
  async load(resource) {
15
21
  const store = createResourceSecretStore(resource, options);
16
22
  const value = await store.get();
@@ -119,6 +125,8 @@ function isStoredOAuthSession(value) {
119
125
  isNonBlankOwnString(value, "authorizationServer") &&
120
126
  isStoredOAuthClient(getOwnEntry(value, "client")) &&
121
127
  isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
128
+ (getOwnEntry(value, "refreshState") === undefined ||
129
+ (getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
122
130
  isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
123
131
  }
124
132
  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>;
@@ -20,11 +20,31 @@ interface McpSubscription {
20
20
  cancel(): void;
21
21
  }
22
22
 
23
+ interface SecretStoreLockFileSystem {
24
+ mkdir(path: string, options?: {
25
+ recursive?: boolean;
26
+ mode?: number;
27
+ }): Promise<unknown>;
28
+ readdir(path: string): Promise<string[]>;
29
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
30
+ writeFile(path: string, value: string, options: {
31
+ encoding: BufferEncoding;
32
+ flag: string;
33
+ mode: number;
34
+ }): Promise<void>;
35
+ rename(from: string, to: string): Promise<void>;
36
+ unlink(path: string): Promise<void>;
37
+ lstat(path: string): Promise<{
38
+ isSymbolicLink(): boolean;
39
+ }>;
40
+ }
41
+
23
42
  interface MachineIdentity {
24
43
  hostname: string;
25
44
  username: string;
26
45
  }
27
46
  interface EncryptedFileStoreFileSystem {
47
+ readdir?(path: string): Promise<string[]>;
28
48
  readFile(path: string, encoding: BufferEncoding): Promise<string>;
29
49
  writeFile(path: string, data: string | NodeJS.ArrayBufferView, options?: {
30
50
  encoding?: BufferEncoding;
@@ -65,6 +85,10 @@ interface KeychainStoreInput {
65
85
  runCommand?: KeychainCommandRunner;
66
86
  service: string;
67
87
  account: string;
88
+ lock?: {
89
+ fs?: SecretStoreLockFileSystem;
90
+ directory?: string;
91
+ };
68
92
  }
69
93
 
70
94
  type StoreBackend = "file" | "keychain";
@@ -155,6 +179,8 @@ interface StoredOAuthSession {
155
179
  clientSecret?: string;
156
180
  };
157
181
  tokens?: StoredOAuthTokens;
182
+ /** A refresh was begun; its winning response may not have been persisted. */
183
+ refreshState?: "pending";
158
184
  discovery: {
159
185
  resourceMetadataUrl: string;
160
186
  resourceMetadata: Record<string, unknown>;
@@ -279,8 +279,8 @@ function receivedType(value) {
279
279
  }
280
280
  return typeof value;
281
281
  }
282
- function issue(path3, expected, value, message, keyword = keywordFor(expected)) {
283
- return { path: path3, expected, received: receivedType(value), message, keyword };
282
+ function issue(path5, expected, value, message, keyword = keywordFor(expected)) {
283
+ return { path: path5, expected, received: receivedType(value), message, keyword };
284
284
  }
285
285
  function keywordFor(expected) {
286
286
  if (["null", "boolean", "object", "array", "number", "integer", "string"].includes(expected)) {
@@ -997,113 +997,113 @@ function evaluateArrayApplicators(graph, node, schema, value, context) {
997
997
  }
998
998
  return results;
999
999
  }
1000
- function evaluateValidationKeywords(node, schema, value, path3) {
1000
+ function evaluateValidationKeywords(node, schema, value, path5) {
1001
1001
  const results = [];
1002
1002
  const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
1003
1003
  if (types.length > 0 && !types.some((type) => typeof type === "string" && typeMatches(type, value))) {
1004
- results.push(invalidResult(issue(path3, types.join(","), value, `must be ${types.join(",")}`)));
1004
+ results.push(invalidResult(issue(path5, types.join(","), value, `must be ${types.join(",")}`)));
1005
1005
  return results;
1006
1006
  }
1007
1007
  if (schema.const !== void 0 && !deepEqual(schema.const, value)) {
1008
- results.push(invalidResult(issue(path3, "const", value, "must be equal to constant")));
1008
+ results.push(invalidResult(issue(path5, "const", value, "must be equal to constant")));
1009
1009
  }
1010
1010
  if (Array.isArray(schema.enum) && !schema.enum.some((entry) => deepEqual(entry, value))) {
1011
- results.push(invalidResult(issue(path3, "enum", value, "must be equal to one of the allowed values")));
1011
+ results.push(invalidResult(issue(path5, "enum", value, "must be equal to one of the allowed values")));
1012
1012
  }
1013
1013
  if (typeof value === "number" && Number.isFinite(value)) {
1014
- evaluateNumberKeywords(schema, value, path3, results);
1014
+ evaluateNumberKeywords(schema, value, path5, results);
1015
1015
  }
1016
1016
  if (typeof value === "string") {
1017
- evaluateStringKeywords(schema, value, path3, results);
1017
+ evaluateStringKeywords(schema, value, path5, results);
1018
1018
  }
1019
1019
  if (Array.isArray(value)) {
1020
- evaluateArrayKeywords(schema, value, path3, results);
1020
+ evaluateArrayKeywords(schema, value, path5, results);
1021
1021
  }
1022
1022
  if (isObject(value)) {
1023
- evaluateObjectKeywords(node, schema, value, path3, results);
1023
+ evaluateObjectKeywords(node, schema, value, path5, results);
1024
1024
  }
1025
1025
  return results;
1026
1026
  }
1027
- function evaluateNumberKeywords(schema, value, path3, results) {
1027
+ function evaluateNumberKeywords(schema, value, path5, results) {
1028
1028
  if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
1029
- results.push(invalidResult(issue(path3, `multiple of ${schema.multipleOf}`, value, `must be multiple of ${schema.multipleOf}`)));
1029
+ results.push(invalidResult(issue(path5, `multiple of ${schema.multipleOf}`, value, `must be multiple of ${schema.multipleOf}`)));
1030
1030
  }
1031
1031
  if (typeof schema.maximum === "number" && value > schema.maximum) {
1032
- results.push(invalidResult(issue(path3, `<= ${schema.maximum}`, value, `must be <= ${schema.maximum}`)));
1032
+ results.push(invalidResult(issue(path5, `<= ${schema.maximum}`, value, `must be <= ${schema.maximum}`)));
1033
1033
  }
1034
1034
  if (typeof schema.minimum === "number" && value < schema.minimum) {
1035
- results.push(invalidResult(issue(path3, `>= ${schema.minimum}`, value, `must be >= ${schema.minimum}`)));
1035
+ results.push(invalidResult(issue(path5, `>= ${schema.minimum}`, value, `must be >= ${schema.minimum}`)));
1036
1036
  }
1037
1037
  if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum) {
1038
- results.push(invalidResult(issue(path3, `< ${schema.exclusiveMaximum}`, value, `must be < ${schema.exclusiveMaximum}`)));
1038
+ results.push(invalidResult(issue(path5, `< ${schema.exclusiveMaximum}`, value, `must be < ${schema.exclusiveMaximum}`)));
1039
1039
  }
1040
1040
  if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum) {
1041
- results.push(invalidResult(issue(path3, `> ${schema.exclusiveMinimum}`, value, `must be > ${schema.exclusiveMinimum}`)));
1041
+ results.push(invalidResult(issue(path5, `> ${schema.exclusiveMinimum}`, value, `must be > ${schema.exclusiveMinimum}`)));
1042
1042
  }
1043
1043
  }
1044
- function evaluateStringKeywords(schema, value, path3, results) {
1044
+ function evaluateStringKeywords(schema, value, path5, results) {
1045
1045
  const length = unicodeLength(value);
1046
1046
  if (typeof schema.maxLength === "number" && length > schema.maxLength) {
1047
- results.push(invalidResult(issue(path3, `length <= ${schema.maxLength}`, value, `must NOT have more than ${schema.maxLength} characters`)));
1047
+ results.push(invalidResult(issue(path5, `length <= ${schema.maxLength}`, value, `must NOT have more than ${schema.maxLength} characters`)));
1048
1048
  }
1049
1049
  if (typeof schema.minLength === "number" && length < schema.minLength) {
1050
- results.push(invalidResult(issue(path3, `length >= ${schema.minLength}`, value, `must NOT have fewer than ${schema.minLength} characters`)));
1050
+ results.push(invalidResult(issue(path5, `length >= ${schema.minLength}`, value, `must NOT have fewer than ${schema.minLength} characters`)));
1051
1051
  }
1052
1052
  if (typeof schema.pattern === "string" && !new RegExp(schema.pattern, "u").test(value)) {
1053
- results.push(invalidResult(issue(path3, `pattern ${schema.pattern}`, value, `must match pattern ${schema.pattern}`)));
1053
+ results.push(invalidResult(issue(path5, `pattern ${schema.pattern}`, value, `must match pattern ${schema.pattern}`)));
1054
1054
  }
1055
1055
  }
1056
- function evaluateArrayKeywords(schema, value, path3, results) {
1056
+ function evaluateArrayKeywords(schema, value, path5, results) {
1057
1057
  if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
1058
- results.push(invalidResult(issue(path3, `items <= ${schema.maxItems}`, value, `must NOT have more than ${schema.maxItems} items`)));
1058
+ results.push(invalidResult(issue(path5, `items <= ${schema.maxItems}`, value, `must NOT have more than ${schema.maxItems} items`)));
1059
1059
  }
1060
1060
  if (typeof schema.minItems === "number" && value.length < schema.minItems) {
1061
- results.push(invalidResult(issue(path3, `items >= ${schema.minItems}`, value, `must NOT have fewer than ${schema.minItems} items`)));
1061
+ results.push(invalidResult(issue(path5, `items >= ${schema.minItems}`, value, `must NOT have fewer than ${schema.minItems} items`)));
1062
1062
  }
1063
1063
  if (schema.uniqueItems === true) {
1064
1064
  for (let left = 0; left < value.length; left += 1) {
1065
1065
  for (let right = left + 1; right < value.length; right += 1) {
1066
1066
  if (deepEqual(value[left], value[right])) {
1067
- results.push(invalidResult(issue(path3, "unique items", value, "must NOT have duplicate items")));
1067
+ results.push(invalidResult(issue(path5, "unique items", value, "must NOT have duplicate items")));
1068
1068
  return;
1069
1069
  }
1070
1070
  }
1071
1071
  }
1072
1072
  }
1073
1073
  }
1074
- function evaluateObjectKeywords(node, schema, value, path3, results) {
1074
+ function evaluateObjectKeywords(node, schema, value, path5, results) {
1075
1075
  const keys = Object.keys(value);
1076
1076
  if (typeof schema.maxProperties === "number" && keys.length > schema.maxProperties) {
1077
- results.push(invalidResult(issue(path3, `properties <= ${schema.maxProperties}`, value, `must NOT have more than ${schema.maxProperties} properties`)));
1077
+ results.push(invalidResult(issue(path5, `properties <= ${schema.maxProperties}`, value, `must NOT have more than ${schema.maxProperties} properties`)));
1078
1078
  }
1079
1079
  if (typeof schema.minProperties === "number" && keys.length < schema.minProperties) {
1080
- results.push(invalidResult(issue(path3, `properties >= ${schema.minProperties}`, value, `must NOT have fewer than ${schema.minProperties} properties`)));
1080
+ results.push(invalidResult(issue(path5, `properties >= ${schema.minProperties}`, value, `must NOT have fewer than ${schema.minProperties} properties`)));
1081
1081
  }
1082
1082
  if (Array.isArray(schema.required)) {
1083
1083
  for (const key2 of schema.required) {
1084
1084
  if (typeof key2 === "string" && !Object.prototype.hasOwnProperty.call(value, key2)) {
1085
- results.push(invalidResult(issue([...path3, key2], "required", void 0, `must have required property '${key2}'`)));
1085
+ results.push(invalidResult(issue([...path5, key2], "required", void 0, `must have required property '${key2}'`)));
1086
1086
  }
1087
1087
  }
1088
1088
  }
1089
1089
  const dependentRequired = isObject(schema.dependentRequired) ? schema.dependentRequired : {};
1090
1090
  for (const [key2, dependencies] of Object.entries(dependentRequired)) {
1091
1091
  if (Object.prototype.hasOwnProperty.call(value, key2) && Array.isArray(dependencies)) {
1092
- addMissingDependencies(value, dependencies, path3, results);
1092
+ addMissingDependencies(value, dependencies, path5, results);
1093
1093
  }
1094
1094
  }
1095
1095
  if (node.dialect === "draft7" && isObject(schema.dependencies)) {
1096
1096
  for (const [key2, dependencies] of Object.entries(schema.dependencies)) {
1097
1097
  if (Object.prototype.hasOwnProperty.call(value, key2) && Array.isArray(dependencies)) {
1098
- addMissingDependencies(value, dependencies, path3, results);
1098
+ addMissingDependencies(value, dependencies, path5, results);
1099
1099
  }
1100
1100
  }
1101
1101
  }
1102
1102
  }
1103
- function addMissingDependencies(value, dependencies, path3, results) {
1103
+ function addMissingDependencies(value, dependencies, path5, results) {
1104
1104
  for (const dependency of dependencies) {
1105
1105
  if (typeof dependency === "string" && !Object.prototype.hasOwnProperty.call(value, dependency)) {
1106
- results.push(invalidResult(issue([...path3, dependency], "dependency", void 0, `must have property '${dependency}'`)));
1106
+ results.push(invalidResult(issue([...path5, dependency], "dependency", void 0, `must have property '${dependency}'`)));
1107
1107
  }
1108
1108
  }
1109
1109
  }
@@ -3397,19 +3397,168 @@ var SubscriptionManager = class {
3397
3397
 
3398
3398
  // ../mcp-oauth/dist/client/auth-store-session-store.js
3399
3399
  import crypto from "node:crypto";
3400
- import path2 from "node:path";
3400
+ import path4 from "node:path";
3401
3401
 
3402
3402
  // ../auth-store/dist/encrypted-file-store.js
3403
- import { createCipheriv, createDecipheriv, randomBytes, randomUUID, scrypt } from "node:crypto";
3403
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID as randomUUID2, scrypt } from "node:crypto";
3404
3404
  import { promises as fs } from "node:fs";
3405
3405
  import { homedir, hostname, userInfo } from "node:os";
3406
- import path from "node:path";
3406
+ import path2 from "node:path";
3407
3407
 
3408
3408
  // ../auth-store/dist/error-codes.js
3409
3409
  function hasOwnErrorCode(error, code) {
3410
3410
  return error instanceof Error && Object.prototype.hasOwnProperty.call(error, "code") && error.code === code;
3411
3411
  }
3412
3412
 
3413
+ // ../auth-store/dist/transaction-lock.js
3414
+ import { randomUUID } from "node:crypto";
3415
+ import path from "node:path";
3416
+ async function withSecretStoreFileLock(fs2, lockDirectory, operation, options = {}) {
3417
+ const timeoutMs = options.timeoutMs ?? 3e4;
3418
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 2147483647)
3419
+ throw new Error("Invalid secret-store transaction lock timeout");
3420
+ options.signal?.throwIfAborted();
3421
+ const deadline = performance.now() + timeoutMs;
3422
+ const directory = path.resolve(lockDirectory);
3423
+ await assertLockDirectoryPath(fs2, directory);
3424
+ await fs2.mkdir(directory, { recursive: true, mode: 448 });
3425
+ await assertLockDirectoryPath(fs2, directory);
3426
+ const name = `${process.pid}-${randomUUID()}.claim`;
3427
+ const claimPath = path.join(directory, name);
3428
+ const temporaryPath = `${claimPath}.tmp`;
3429
+ let claimed = true;
3430
+ let temporaryCreated = false;
3431
+ let outcome;
3432
+ try {
3433
+ try {
3434
+ await fs2.writeFile(claimPath, JSON.stringify({ ticket: null }), { encoding: "utf8", flag: "wx", mode: 384 });
3435
+ } catch (error) {
3436
+ if (hasOwnErrorCode(error, "EEXIST"))
3437
+ claimed = false;
3438
+ throw error;
3439
+ }
3440
+ const existing = await readClaims(fs2, directory, name);
3441
+ const ticket = existing.reduce((max, claim) => Math.max(max, claim.ticket ?? 0), 0) + 1;
3442
+ if (!Number.isSafeInteger(ticket))
3443
+ throw new Error("Secret-store transaction lock ticket overflow");
3444
+ temporaryCreated = true;
3445
+ try {
3446
+ await fs2.writeFile(temporaryPath, JSON.stringify({ ticket }), { encoding: "utf8", flag: "wx", mode: 384 });
3447
+ } catch (error) {
3448
+ if (hasOwnErrorCode(error, "EEXIST"))
3449
+ temporaryCreated = false;
3450
+ throw error;
3451
+ }
3452
+ await fs2.rename(temporaryPath, claimPath);
3453
+ temporaryCreated = false;
3454
+ for (; ; ) {
3455
+ options.signal?.throwIfAborted();
3456
+ const peers = await readClaims(fs2, directory, name);
3457
+ if (!peers.some((peer) => peer.ticket === null || peer.ticket < ticket || peer.ticket === ticket && peer.name < name))
3458
+ break;
3459
+ const remaining = deadline - performance.now();
3460
+ if (remaining <= 0)
3461
+ throw new Error("Timed out waiting for secret-store transaction lock");
3462
+ await new Promise((resolve, reject) => {
3463
+ const abort = () => {
3464
+ clearTimeout(timer);
3465
+ options.signal?.removeEventListener("abort", abort);
3466
+ reject(options.signal?.reason);
3467
+ };
3468
+ const timer = setTimeout(() => {
3469
+ options.signal?.removeEventListener("abort", abort);
3470
+ resolve();
3471
+ }, Math.min(10, remaining));
3472
+ options.signal?.addEventListener("abort", abort, { once: true });
3473
+ if (options.signal?.aborted)
3474
+ abort();
3475
+ });
3476
+ }
3477
+ options.signal?.throwIfAborted();
3478
+ outcome = { result: await operation() };
3479
+ } catch (error) {
3480
+ outcome = { error };
3481
+ }
3482
+ const cleanup = [];
3483
+ for (const target of [...temporaryCreated ? [temporaryPath] : [], ...claimed ? [claimPath] : []]) {
3484
+ try {
3485
+ await fs2.unlink(target);
3486
+ } catch (error) {
3487
+ if (!hasOwnErrorCode(error, "ENOENT"))
3488
+ cleanup.push(error);
3489
+ }
3490
+ }
3491
+ if (cleanup.length)
3492
+ throw new AggregateError([..."error" in outcome ? [outcome.error] : [], ...cleanup], "Secret-store transaction lock cleanup failed");
3493
+ if ("error" in outcome)
3494
+ throw outcome.error;
3495
+ return outcome.result;
3496
+ }
3497
+ async function assertNoSymbolicLink(fs2, target) {
3498
+ try {
3499
+ if ((await fs2.lstat(target)).isSymbolicLink())
3500
+ throw new Error("Refusing secret-store transaction lock through symbolic link");
3501
+ } catch (error) {
3502
+ if (!hasOwnErrorCode(error, "ENOENT"))
3503
+ throw error;
3504
+ }
3505
+ }
3506
+ async function assertLockDirectoryPath(fs2, directory) {
3507
+ const root = path.parse(directory).root;
3508
+ const segments = directory.slice(root.length).split(path.sep).filter(Boolean);
3509
+ let current = root;
3510
+ for (const [index, segment] of segments.entries()) {
3511
+ current = path.join(current, segment);
3512
+ if (index > 0 || segments.length === 1)
3513
+ await assertNoSymbolicLink(fs2, current);
3514
+ }
3515
+ }
3516
+ async function readClaims(fs2, directory, ownName) {
3517
+ const claims = [];
3518
+ for (const name of await fs2.readdir(directory)) {
3519
+ if (name === ownName || !name.endsWith(".claim"))
3520
+ continue;
3521
+ const pidText = name.slice(0, name.indexOf("-"));
3522
+ const pid = Number(pidText);
3523
+ if (!Number.isSafeInteger(pid) || pid < 1 || String(pid) !== pidText)
3524
+ throw new Error("Malformed secret-store transaction lock owner");
3525
+ const target = path.join(directory, name);
3526
+ await assertNoSymbolicLink(fs2, target);
3527
+ let alive = true;
3528
+ try {
3529
+ process.kill(pid, 0);
3530
+ } catch (error) {
3531
+ alive = !hasOwnErrorCode(error, "ESRCH");
3532
+ }
3533
+ if (!alive) {
3534
+ try {
3535
+ await fs2.unlink(target);
3536
+ } catch (error) {
3537
+ if (!hasOwnErrorCode(error, "ENOENT"))
3538
+ throw error;
3539
+ }
3540
+ continue;
3541
+ }
3542
+ let ticket = null;
3543
+ let raw;
3544
+ try {
3545
+ raw = await fs2.readFile(target, "utf8");
3546
+ } catch (error) {
3547
+ if (hasOwnErrorCode(error, "ENOENT"))
3548
+ continue;
3549
+ throw error;
3550
+ }
3551
+ try {
3552
+ const value = JSON.parse(raw);
3553
+ if (value !== null && typeof value === "object" && "ticket" in value && typeof value.ticket === "number" && Number.isSafeInteger(value.ticket) && value.ticket > 0)
3554
+ ticket = value.ticket;
3555
+ } catch {
3556
+ }
3557
+ claims.push({ name, ticket });
3558
+ }
3559
+ return claims;
3560
+ }
3561
+
3413
3562
  // ../auth-store/dist/encrypted-file-store.js
3414
3563
  var derivedKeyCache = /* @__PURE__ */ new Map();
3415
3564
  var ENCRYPTION_ALGORITHM = "aes-256-gcm";
@@ -3435,7 +3584,7 @@ var EncryptedFileStore = class {
3435
3584
  const defaultFileName = input.defaultFileName ?? "credentials.enc";
3436
3585
  assertSafeDefaultDirectory(defaultDirectory);
3437
3586
  assertSafeDefaultFileName(defaultFileName);
3438
- this.filePath = path.join(homeDirectory, defaultDirectory, defaultFileName);
3587
+ this.filePath = path2.join(homeDirectory, defaultDirectory, defaultFileName);
3439
3588
  this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
3440
3589
  } else {
3441
3590
  this.filePath = input.filePath;
@@ -3475,6 +3624,12 @@ var EncryptedFileStore = class {
3475
3624
  return null;
3476
3625
  }
3477
3626
  }
3627
+ async withLock(operation, options = {}) {
3628
+ await this.assertCredentialPathHasNoSymbolicLinks(`${this.filePath}.lock`);
3629
+ if (this.fs.readdir === void 0)
3630
+ throw new Error("Secret-store transaction locks require filesystem readdir support");
3631
+ return withSecretStoreFileLock(this.fs, `${this.filePath}.lock`, operation, options);
3632
+ }
3478
3633
  async set(value) {
3479
3634
  await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
3480
3635
  const key2 = await this.getEncryptionKey();
@@ -3491,9 +3646,9 @@ var EncryptedFileStore = class {
3491
3646
  authTag: authTag.toString("base64"),
3492
3647
  ciphertext: ciphertext.toString("base64")
3493
3648
  };
3494
- await this.fs.mkdir(path.dirname(this.filePath), { recursive: true });
3649
+ await this.fs.mkdir(path2.dirname(this.filePath), { recursive: true });
3495
3650
  await this.assertCredentialPathHasNoSymbolicLinks(this.filePath);
3496
- const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
3651
+ const temporaryPath = `${this.filePath}.${process.pid}.${randomUUID2()}.tmp`;
3497
3652
  let temporaryCreated = false;
3498
3653
  try {
3499
3654
  await this.assertCredentialPathHasNoSymbolicLinks(temporaryPath);
@@ -3523,7 +3678,7 @@ var EncryptedFileStore = class {
3523
3678
  }
3524
3679
  }
3525
3680
  async assertCredentialPathHasNoSymbolicLinks(targetPath) {
3526
- const resolvedPath = path.resolve(targetPath);
3681
+ const resolvedPath = path2.resolve(targetPath);
3527
3682
  const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
3528
3683
  for (const currentPath of protectedPaths) {
3529
3684
  try {
@@ -3554,26 +3709,26 @@ var EncryptedFileStore = class {
3554
3709
  };
3555
3710
  function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
3556
3711
  const [firstSegment] = defaultDirectory.split(/[\\/]+/).filter(Boolean);
3557
- return path.resolve(homeDirectory, firstSegment ?? ".");
3712
+ return path2.resolve(homeDirectory, firstSegment ?? ".");
3558
3713
  }
3559
3714
  function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
3560
3715
  if (symbolicLinkCheckStartPath === null) {
3561
3716
  return getExplicitProtectedCredentialPaths(resolvedPath);
3562
3717
  }
3563
- const resolvedStartPath = path.resolve(symbolicLinkCheckStartPath);
3718
+ const resolvedStartPath = path2.resolve(symbolicLinkCheckStartPath);
3564
3719
  if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
3565
- return [path.dirname(resolvedPath), resolvedPath];
3720
+ return [path2.dirname(resolvedPath), resolvedPath];
3566
3721
  }
3567
3722
  const protectedPaths = [resolvedStartPath];
3568
3723
  let currentPath = resolvedStartPath;
3569
- for (const segment of path.relative(resolvedStartPath, resolvedPath).split(path.sep).filter(Boolean)) {
3570
- currentPath = path.join(currentPath, segment);
3724
+ for (const segment of path2.relative(resolvedStartPath, resolvedPath).split(path2.sep).filter(Boolean)) {
3725
+ currentPath = path2.join(currentPath, segment);
3571
3726
  protectedPaths.push(currentPath);
3572
3727
  }
3573
3728
  return protectedPaths;
3574
3729
  }
3575
3730
  function assertSafeDefaultDirectory(defaultDirectory) {
3576
- if (path.isAbsolute(defaultDirectory) || path.win32.isAbsolute(defaultDirectory)) {
3731
+ if (path2.isAbsolute(defaultDirectory) || path2.win32.isAbsolute(defaultDirectory)) {
3577
3732
  throw new Error("defaultDirectory must be a relative path inside the home directory");
3578
3733
  }
3579
3734
  for (const segment of splitPathSegments(defaultDirectory)) {
@@ -3591,15 +3746,15 @@ function splitPathSegments(value) {
3591
3746
  return value.split("/").flatMap((segment) => segment.split("\\")).filter((segment) => segment.length > 0);
3592
3747
  }
3593
3748
  function getExplicitProtectedCredentialPaths(resolvedPath) {
3594
- const parsed = path.parse(resolvedPath);
3595
- const segments = resolvedPath.slice(parsed.root.length).split(path.sep).filter((segment) => segment.length > 0);
3749
+ const parsed = path2.parse(resolvedPath);
3750
+ const segments = resolvedPath.slice(parsed.root.length).split(path2.sep).filter((segment) => segment.length > 0);
3596
3751
  if (segments.length <= 1) {
3597
3752
  return [resolvedPath];
3598
3753
  }
3599
3754
  const protectedPaths = [];
3600
3755
  let currentPath = parsed.root;
3601
3756
  for (const [index, segment] of segments.entries()) {
3602
- currentPath = path.join(currentPath, segment);
3757
+ currentPath = path2.join(currentPath, segment);
3603
3758
  if (index === 0) {
3604
3759
  continue;
3605
3760
  }
@@ -3608,8 +3763,8 @@ function getExplicitProtectedCredentialPaths(resolvedPath) {
3608
3763
  return protectedPaths;
3609
3764
  }
3610
3765
  function isPathInsideOrEqual(childPath, parentPath) {
3611
- const relativePath = path.relative(parentPath, childPath);
3612
- return relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
3766
+ const relativePath = path2.relative(parentPath, childPath);
3767
+ return relativePath === "" || !relativePath.startsWith("..") && !path2.isAbsolute(relativePath);
3613
3768
  }
3614
3769
  async function removeIfPresent(fileSystem, filePath) {
3615
3770
  try {
@@ -3692,16 +3847,24 @@ function isAlreadyExistsError(error) {
3692
3847
 
3693
3848
  // ../auth-store/dist/keychain-store.js
3694
3849
  import { spawn } from "node:child_process";
3850
+ import { createHash } from "node:crypto";
3851
+ import { promises as nodeFs } from "node:fs";
3852
+ import { homedir as homedir2 } from "node:os";
3853
+ import path3 from "node:path";
3695
3854
  var SECURITY_CLI = "security";
3696
3855
  var KEYCHAIN_ITEM_NOT_FOUND_EXIT_CODE = 44;
3697
3856
  var KeychainStore = class {
3698
3857
  runCommand;
3699
3858
  service;
3700
3859
  account;
3860
+ lockFs;
3861
+ lockDirectory;
3701
3862
  constructor(input) {
3702
3863
  this.runCommand = input.runCommand ?? runSecurityCommand;
3703
3864
  this.service = input.service.trim();
3704
3865
  this.account = input.account.trim();
3866
+ this.lockFs = input.lock?.fs ?? nodeFs;
3867
+ this.lockDirectory = input.lock?.directory ?? path3.join(homedir2(), ".auth-store", "keychain-locks");
3705
3868
  if (this.service.length === 0) {
3706
3869
  throw new Error("Keychain service must not be empty");
3707
3870
  }
@@ -3719,6 +3882,10 @@ var KeychainStore = class {
3719
3882
  }
3720
3883
  throw createSecurityCliFailure("read secret from macOS Keychain", result);
3721
3884
  }
3885
+ async withLock(operation, options = {}) {
3886
+ const identity = createHash("sha256").update(JSON.stringify([this.service, this.account])).digest("hex");
3887
+ return withSecretStoreFileLock(this.lockFs, path3.join(this.lockDirectory, identity), operation, options);
3888
+ }
3722
3889
  async set(value) {
3723
3890
  if (value.includes("\n") || value.includes("\r")) {
3724
3891
  throw new Error("Keychain secrets cannot contain line breaks");
@@ -3904,6 +4071,12 @@ var DEFAULT_CLIENT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth-clients";
3904
4071
  var MAX_JS_DATE_MS = 864e13;
3905
4072
  function createAuthStoreSessionStore(options = {}) {
3906
4073
  return {
4074
+ async withLock(resource, operation, lockOptions) {
4075
+ const store = createResourceSecretStore(resource, options);
4076
+ if (store.withLock === void 0)
4077
+ throw new Error("OAuth secret-store backend does not support transaction locks");
4078
+ return store.withLock(operation, lockOptions);
4079
+ },
3907
4080
  async load(resource) {
3908
4081
  const store = createResourceSecretStore(resource, options);
3909
4082
  const value = await store.get();
@@ -3958,10 +4131,10 @@ function createAuthStoreClientStore(options) {
3958
4131
  function createNamedSecretStore(key2, options, defaults) {
3959
4132
  const hash = crypto.createHash("sha256").update(key2).digest("hex");
3960
4133
  const configuredFilePath = options.fileStore?.filePath;
3961
- const parsedFilePath = configuredFilePath === void 0 ? null : path2.parse(configuredFilePath);
4134
+ const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
3962
4135
  const fileStore = {
3963
4136
  ...options.fileStore,
3964
- filePath: parsedFilePath === null ? void 0 : path2.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
4137
+ filePath: parsedFilePath === null ? void 0 : path4.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
3965
4138
  salt: options.fileStore?.salt ?? defaults.salt,
3966
4139
  defaultDirectory: options.fileStore?.defaultDirectory || defaults.directory,
3967
4140
  defaultFileName: parsedFilePath === null ? `${hash}.enc` : `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`
@@ -4003,7 +4176,7 @@ function isStoredOAuthSession(value) {
4003
4176
  if (!isObjectRecord(value)) {
4004
4177
  return false;
4005
4178
  }
4006
- return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && isStoredOAuthClient(getOwnEntry3(value, "client")) && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
4179
+ 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"));
4007
4180
  }
4008
4181
  function isStoredOAuthClient(value) {
4009
4182
  if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
@@ -4432,7 +4605,9 @@ var OAuthError = class extends Error {
4432
4605
  status;
4433
4606
  retryable;
4434
4607
  terminal;
4435
- constructor(shape, status) {
4608
+ /** True only when a complete OAuth error response establishes rejection. */
4609
+ outcomeKnown;
4610
+ constructor(shape, status, outcomeKnown = true) {
4436
4611
  super(shape.error_description ?? shape.error);
4437
4612
  this.name = "OAuthError";
4438
4613
  this.error = shape.error;
@@ -4443,6 +4618,7 @@ var OAuthError = class extends Error {
4443
4618
  this.status = status;
4444
4619
  this.retryable = isRetryableOAuthError(this);
4445
4620
  this.terminal = !this.retryable;
4621
+ this.outcomeKnown = outcomeKnown;
4446
4622
  }
4447
4623
  };
4448
4624
  function isRetryableOAuthError(error) {
@@ -4557,7 +4733,8 @@ async function readOAuthJsonObjectResponse(response, signal) {
4557
4733
  }
4558
4734
  const record2 = payload;
4559
4735
  if (!response.ok) {
4560
- throw new OAuthError(readOAuthError(record2, fallbackError.error), response.status);
4736
+ const error = getOwnEntry5(record2, "error");
4737
+ throw new OAuthError(readOAuthError(record2, fallbackError.error), response.status, typeof error === "string" && error.trim().length > 0);
4561
4738
  }
4562
4739
  return record2;
4563
4740
  }
@@ -4576,7 +4753,7 @@ function getOwnEntry5(record2, key2) {
4576
4753
  }
4577
4754
  function createFallbackOAuthError(status) {
4578
4755
  const error = status === 503 ? "temporarily_unavailable" : "server_error";
4579
- return new OAuthError({ error }, status);
4756
+ return new OAuthError({ error }, status, false);
4580
4757
  }
4581
4758
  function normalizeBearerTokenType(value) {
4582
4759
  if (typeof value !== "string") {
@@ -4762,6 +4939,11 @@ function createDefaultOAuthClientProvider(options) {
4762
4939
  if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
4763
4940
  forceRefresh = false;
4764
4941
  const sessionDiscovery = resolveDiscovery(discovery, session);
4942
+ if (session?.refreshState === "pending") {
4943
+ if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
4944
+ throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
4945
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch2, signal);
4946
+ }
4765
4947
  if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
4766
4948
  return session;
4767
4949
  }
@@ -4789,6 +4971,9 @@ function createDefaultOAuthClientProvider(options) {
4789
4971
  if (session.tokens?.refreshToken === void 0) {
4790
4972
  return session;
4791
4973
  }
4974
+ const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
4975
+ await saveSession(resource, pendingSession);
4976
+ signal?.throwIfAborted();
4792
4977
  let refreshAttempted = false;
4793
4978
  let refreshedTokens;
4794
4979
  while (true) {
@@ -4806,7 +4991,9 @@ function createDefaultOAuthClientProvider(options) {
4806
4991
  break;
4807
4992
  } catch (error) {
4808
4993
  signal?.throwIfAborted();
4809
- if (error instanceof OAuthError && error.error === "invalid_grant") {
4994
+ if (!(error instanceof OAuthError) || !error.outcomeKnown)
4995
+ throw error;
4996
+ if (error.error === "invalid_grant") {
4810
4997
  const clearedSession = clearSessionTokens(session);
4811
4998
  await saveSession(resource, clearedSession);
4812
4999
  return clearedSession;
@@ -4820,6 +5007,7 @@ function createDefaultOAuthClientProvider(options) {
4820
5007
  refreshAttempted = true;
4821
5008
  continue;
4822
5009
  }
5010
+ await saveSession(resource, session);
4823
5011
  throw error;
4824
5012
  }
4825
5013
  }
@@ -5066,6 +5254,7 @@ function sameTokenGrant(left, right) {
5066
5254
  function clearSessionTokens(session) {
5067
5255
  const nextSession = { ...session };
5068
5256
  delete nextSession.tokens;
5257
+ delete nextSession.refreshState;
5069
5258
  return nextSession;
5070
5259
  }
5071
5260
  function hasCachedAccessToken(session) {
@@ -5075,6 +5264,9 @@ function normalizeLoadedSession(session) {
5075
5264
  if (session === null) {
5076
5265
  return null;
5077
5266
  }
5267
+ const refreshState = getOwnEntry6(session, "refreshState");
5268
+ if (refreshState !== void 0 && (refreshState !== "pending" || getOwnEntry6(session, "tokens") !== void 0))
5269
+ throw new Error("Stored OAuth refresh state is invalid");
5078
5270
  const client = normalizeStoredClient(getOwnEntry6(session, "client"));
5079
5271
  if (client === null) {
5080
5272
  return { ...session, client: { clientId: "" }, tokens: void 0 };
@@ -5347,7 +5539,7 @@ function getParameterHeaders(schema) {
5347
5539
  const names = /* @__PURE__ */ new Set();
5348
5540
  const ancestors = /* @__PURE__ */ new Set();
5349
5541
  let nodes = 0;
5350
- const visit = (value, path3, reachable, depth) => {
5542
+ const visit = (value, path5, reachable, depth) => {
5351
5543
  if (++nodes > 1e4 || depth > 64)
5352
5544
  throw new Error("MCP header schema traversal limit exceeded");
5353
5545
  if (typeof value === "boolean")
@@ -5358,7 +5550,7 @@ function getParameterHeaders(schema) {
5358
5550
  try {
5359
5551
  if (Object.prototype.hasOwnProperty.call(value, "x-mcp-header")) {
5360
5552
  const name = value["x-mcp-header"];
5361
- if (!reachable || path3.length === 0 || typeof name !== "string" || name.length === 0 || [...name].some((character) => {
5553
+ if (!reachable || path5.length === 0 || typeof name !== "string" || name.length === 0 || [...name].some((character) => {
5362
5554
  const code = character.charCodeAt(0);
5363
5555
  return !tokenPunctuation.has(character) && !(code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122);
5364
5556
  }))
@@ -5369,25 +5561,25 @@ function getParameterHeaders(schema) {
5369
5561
  if (names.has(key2))
5370
5562
  throw new Error("Duplicate x-mcp-header name");
5371
5563
  names.add(key2);
5372
- headers.push({ name: `Mcp-Param-${name}`, path: [...path3], type: value.type });
5564
+ headers.push({ name: `Mcp-Param-${name}`, path: [...path5], type: value.type });
5373
5565
  }
5374
5566
  for (const [keyword, child] of Object.entries(value)) {
5375
5567
  if (schemaMaps.has(keyword)) {
5376
5568
  if (!isRecord3(child))
5377
5569
  throw new Error("Invalid schema map");
5378
5570
  for (const [key2, nested] of Object.entries(child))
5379
- visit(nested, keyword === "properties" ? [...path3, key2] : path3, reachable && keyword === "properties", depth + 1);
5571
+ visit(nested, keyword === "properties" ? [...path5, key2] : path5, reachable && keyword === "properties", depth + 1);
5380
5572
  } else if (schemaArrays.has(keyword)) {
5381
5573
  if (!Array.isArray(child))
5382
5574
  throw new Error("Invalid schema array");
5383
5575
  for (const nested of child)
5384
- visit(nested, path3, false, depth + 1);
5576
+ visit(nested, path5, false, depth + 1);
5385
5577
  } else if (schemaChildren.has(keyword)) {
5386
5578
  if (Array.isArray(child))
5387
5579
  for (const nested of child)
5388
- visit(nested, path3, false, depth + 1);
5580
+ visit(nested, path5, false, depth + 1);
5389
5581
  else
5390
- visit(child, path3, false, depth + 1);
5582
+ visit(child, path5, false, depth + 1);
5391
5583
  }
5392
5584
  }
5393
5585
  } finally {
@@ -5585,8 +5777,8 @@ function authorizationServerMetadataLocations(issuer) {
5585
5777
  ];
5586
5778
  const issuerUrl = new URL(issuer);
5587
5779
  if (issuerUrl.pathname !== "/") {
5588
- const path3 = issuerUrl.pathname.endsWith("/") ? issuerUrl.pathname.slice(0, -1) : issuerUrl.pathname;
5589
- issuerUrl.pathname = `${path3}/.well-known/openid-configuration`;
5780
+ const path5 = issuerUrl.pathname.endsWith("/") ? issuerUrl.pathname.slice(0, -1) : issuerUrl.pathname;
5781
+ issuerUrl.pathname = `${path5}/.well-known/openid-configuration`;
5590
5782
  locations.push(issuerUrl.toString());
5591
5783
  }
5592
5784
  return locations;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",