tiny-http-mcp-server 0.1.28 → 0.1.30

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.28",
21
+ "version": "0.1.30",
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
  }
@@ -104,6 +104,10 @@ Discovery binds an expired or explicitly rejected grant before silent refresh,
104
104
  using the original configured client. Persisted sessions take precedence,
105
105
  including sessions whose tokens have been cleared; an import cannot revive them.
106
106
  Input tokens are copied and invalid expiry values fail before authorization.
107
+ Static clients and dynamic initial-grant imports require cached grants to match
108
+ the original normalized client ID and secret. A different client configuration
109
+ fails before attaching or refreshing credentials and retains the stored record;
110
+ select separate persistence or explicitly reset the session to change apps.
107
111
 
108
112
  `createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
109
113
 
@@ -130,6 +134,12 @@ can recover it; headless requests fail with an explicit unknown-outcome error.
130
134
  Only complete OAuth error responses establish a rejected request and allow a
131
135
  transient retry or restoration of the original grant. Gateway error pages do not.
132
136
 
137
+ Native persisted OAuth reads always reject corrupt encrypted documents and
138
+ invalid stored JSON, with diagnostics that omit decrypted contents. They retain
139
+ the existing record for explicit reset rather than interpreting corruption as
140
+ an absent session and reviving an initial grant. Caller file-backend settings
141
+ cannot disable this policy.
142
+
133
143
  ## Environment Variables
134
144
 
135
145
  This package exposes no direct environment variables. When `authStore` is used,
@@ -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"}`),
@@ -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 ((options.client.mode === "static" || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
144
+ const configured = normalizeConfiguredClient(options.client);
145
+ if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
146
+ throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
147
+ }
143
148
  if (session?.refreshState === "pending") {
144
149
  if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
145
150
  throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
@@ -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 {
@@ -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,
@@ -4939,6 +4958,11 @@ function createDefaultOAuthClientProvider(options) {
4939
4958
  if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
4940
4959
  forceRefresh = false;
4941
4960
  const sessionDiscovery = resolveDiscovery(discovery, session);
4961
+ if ((options.client.mode === "static" || initialGrant !== void 0) && session !== null && (session.tokens !== void 0 || session.refreshState === "pending")) {
4962
+ const configured = normalizeConfiguredClient(options.client);
4963
+ if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
4964
+ throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
4965
+ }
4942
4966
  if (session?.refreshState === "pending") {
4943
4967
  if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
4944
4968
  throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",