tiny-http-mcp-server 0.1.36 → 0.1.37

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.36",
21
+ "version": "0.1.37",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -102,6 +102,15 @@ normalized. An imported grant must declare its scope when a scope is configured.
102
102
  Authorization records the requested set when the endpoint omits scope, and
103
103
  refresh retains the previous granted set. Mismatched responses never activate
104
104
  credentials; an unusable refresh response retains the pending refresh record.
105
+ `resourceIdentity` selects a native-owned logical server within its optional
106
+ persistence namespace. One encrypted, locked document owns that identity's
107
+ current resource URL, sessions and registrations. Changing the URL retires its
108
+ credentials permanently; returning to the old URL does not restore them.
109
+ Retired identities also withhold stale initial grants; authorize again or select
110
+ a fresh explicit profile to import a new grant. This option requires native persistence; custom
111
+ session stores own their durable resource trust policy. Without it, the native
112
+ client retains its existing resource-URL cache behavior.
113
+
105
114
  Select a separate persistence namespace for another scope profile. No scope is
106
115
  invented when the client does not configure one.
107
116
 
@@ -1,5 +1,5 @@
1
- import { type CreateSecretStoreInput } from "auth-store";
2
- import type { OAuthSessionStore, StoredOAuthClient } from "./types.js";
1
+ import { type CreateSecretStoreInput, type SecretStore } from "auth-store";
2
+ import type { OAuthSessionStore, StoredOAuthSession, StoredOAuthClient } from "./types.js";
3
3
  export interface OAuthClientStore {
4
4
  load(issuer: string): Promise<StoredOAuthClient | null>;
5
5
  save(issuer: string, client: StoredOAuthClient): Promise<void>;
@@ -7,4 +7,11 @@ export interface OAuthClientStore {
7
7
  }
8
8
  export declare function createAuthStoreSessionStore(options?: CreateSecretStoreInput, namespace?: string): OAuthSessionStore;
9
9
  export declare function createAuthStoreClientStore(options: CreateSecretStoreInput, namespace?: string): OAuthClientStore;
10
+ export declare function createNamedSecretStore(key: string, options: CreateSecretStoreInput, defaults: {
11
+ salt: string;
12
+ directory: string;
13
+ service: string;
14
+ accountPrefix: string;
15
+ }, namespace?: string): SecretStore;
10
16
  export declare function assertPersistenceNamespace(namespace: string | undefined): void;
17
+ export declare function isStoredOAuthSession(value: unknown): value is StoredOAuthSession;
@@ -79,7 +79,7 @@ export function createAuthStoreClientStore(options, namespace) {
79
79
  }
80
80
  };
81
81
  }
82
- function createNamedSecretStore(key, options, defaults, namespace) {
82
+ export function createNamedSecretStore(key, options, defaults, namespace) {
83
83
  const hash = crypto.createHash("sha256").update(namespace === undefined ? key : JSON.stringify([namespace, key])).digest("hex");
84
84
  const configuredFilePath = options.fileStore?.filePath;
85
85
  const parsedFilePath = configuredFilePath === undefined ? null : path.parse(configuredFilePath);
@@ -132,7 +132,7 @@ function getOwnString(record, key) {
132
132
  const value = getOwnEntry(record, key);
133
133
  return typeof value === "string" ? value : undefined;
134
134
  }
135
- function isStoredOAuthSession(value) {
135
+ export function isStoredOAuthSession(value) {
136
136
  if (!isObjectRecord(value)) {
137
137
  return false;
138
138
  }
@@ -1,3 +1,4 @@
1
+ import { createResourceBoundOAuthStores } from "./resource-bound-store.js";
1
2
  import { normalizeStoredOAuthClient, parseOAuthClientRegistration, registrationMatchesRedirect } from "./client-registration.js";
2
3
  import { normalizeOAuthScope } from "./scope.js";
3
4
  import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
@@ -26,8 +27,13 @@ export function createDefaultOAuthClientProvider(options) {
26
27
  const configuredClient = normalizeConfiguredClient(options.client);
27
28
  const requestedTokenMethod = normalizeOAuthTokenEndpointAuthMethod(options.client.tokenEndpointAuthMethod);
28
29
  const configuredTokenMethod = requestedTokenMethod ?? configuredClient?.tokenEndpointAuthMethod;
29
- const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
30
- const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
30
+ if (options.resourceIdentity !== undefined && options.sessionStore !== undefined)
31
+ throw new Error("OAuth resourceIdentity requires native-owned persistence; custom stores own their resource trust policy");
32
+ const resourceStores = options.resourceIdentity === undefined ? undefined :
33
+ createResourceBoundOAuthStores(options.authStore ?? {}, options.persistenceNamespace, options.resourceIdentity);
34
+ const sessionStore = resourceStores?.sessionStore ?? options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
35
+ const clientStore = resourceStores?.clientStore ??
36
+ (options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace));
31
37
  const now = options.now ?? Date.now;
32
38
  const registeredClients = new Map();
33
39
  if (options.initialGrant !== undefined) {
@@ -127,6 +133,11 @@ export function createDefaultOAuthClientProvider(options) {
127
133
  const canonicalResource = canonicalizeResourceIndicator(resource);
128
134
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
129
135
  let session = await loadSession(canonicalResource);
136
+ if (resourceStores !== undefined) {
137
+ registeredClients.clear();
138
+ if (!resourceStores.initialGrantAllowed)
139
+ initialGrantConsumed = true;
140
+ }
130
141
  if (session !== null)
131
142
  assertRegistrationIssuer(session.client, session.authorizationServer);
132
143
  if (configuredClient !== null && discovery !== undefined)
@@ -0,0 +1,9 @@
1
+ import type { CreateSecretStoreInput } from "auth-store";
2
+ import { type OAuthClientStore } from "./auth-store-session-store.js";
3
+ import type { OAuthSessionStore } from "./types.js";
4
+ /** One locked document owns a logical server's URL history, grant and clients. */
5
+ export declare function createResourceBoundOAuthStores(options: CreateSecretStoreInput, namespace: string | undefined, identity: string): {
6
+ sessionStore: OAuthSessionStore;
7
+ clientStore: OAuthClientStore;
8
+ initialGrantAllowed: boolean;
9
+ };
@@ -0,0 +1,108 @@
1
+ import { canonicalizeResourceIndicator } from "../resource-indicator.js";
2
+ import { assertPersistenceNamespace, createNamedSecretStore, isStoredOAuthSession } from "./auth-store-session-store.js";
3
+ import { normalizeStoredOAuthClient } from "./client-registration.js";
4
+ /** One locked document owns a logical server's URL history, grant and clients. */
5
+ export function createResourceBoundOAuthStores(options, namespace, identity) {
6
+ assertPersistenceNamespace(identity);
7
+ const store = createNamedSecretStore(identity, options, { salt: "poe-code:mcp-oauth:resources:v1",
8
+ directory: ".poe-code/mcp-oauth/resources", service: "poe-code-mcp-oauth-resources", accountPrefix: "resource" }, namespace);
9
+ const result = { initialGrantAllowed: true, sessionStore: {}, clientStore: {} };
10
+ async function read() {
11
+ const raw = await store.get();
12
+ if (raw === null)
13
+ return null;
14
+ let value;
15
+ try {
16
+ value = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ throw new Error("Stored OAuth resource identity must be valid JSON; reset explicitly to recover");
20
+ }
21
+ if (typeof value !== "object" || value === null || Array.isArray(value))
22
+ throw new Error("Invalid stored OAuth resource identity");
23
+ const record = value;
24
+ if (!["version", "resource", "generation", "session", "clients"].every(key => Object.hasOwn(record, key)) || record.version !== 1 || typeof record.resource !== "string" || record.resource !== canonicalizeResourceIndicator(record.resource) ||
25
+ !Number.isSafeInteger(record.generation) || record.generation < 0 ||
26
+ (record.session !== null && (!isStoredOAuthSession(record.session) || record.session.resource !== record.resource)) ||
27
+ typeof record.clients !== "object" || record.clients === null || Array.isArray(record.clients))
28
+ throw new Error("Invalid stored OAuth resource identity; reset explicitly to recover");
29
+ let resourceUrl;
30
+ try {
31
+ resourceUrl = new URL(record.resource);
32
+ }
33
+ catch {
34
+ throw new Error("Invalid stored OAuth resource identity URL");
35
+ }
36
+ if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.hash)
37
+ throw new Error("Invalid stored OAuth resource identity URL");
38
+ for (const [issuer, client] of Object.entries(record.clients)) {
39
+ const normalized = normalizeStoredOAuthClient(client);
40
+ let url;
41
+ try {
42
+ url = new URL(issuer);
43
+ }
44
+ catch {
45
+ throw new Error("Invalid stored OAuth resource client issuer");
46
+ }
47
+ if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
48
+ throw new Error("Invalid stored OAuth resource client");
49
+ Object.defineProperty(record.clients, issuer, { value: normalized, enumerable: true, configurable: true, writable: true });
50
+ }
51
+ return record;
52
+ }
53
+ async function reconcile(resource) {
54
+ resource = canonicalizeResourceIndicator(resource);
55
+ const existing = await read();
56
+ if (existing?.resource === resource) {
57
+ result.initialGrantAllowed = existing.generation === 0;
58
+ return existing;
59
+ }
60
+ const record = { version: 1, resource, generation: existing === null ? 0 : existing.generation + 1, session: null, clients: {} };
61
+ if (!Number.isSafeInteger(record.generation))
62
+ throw new Error("OAuth resource identity generation limit exceeded");
63
+ await store.set(JSON.stringify(record));
64
+ result.initialGrantAllowed = record.generation === 0;
65
+ return record;
66
+ }
67
+ result.sessionStore = {
68
+ async withLock(_resource, operation, options) {
69
+ if (store.withLock === undefined)
70
+ throw new Error("OAuth resource identity backend must support transaction locks");
71
+ return store.withLock(operation, options);
72
+ },
73
+ async load(resource) { return (await reconcile(resource)).session; },
74
+ async save(resource, session) {
75
+ const record = await reconcile(resource);
76
+ if (canonicalizeResourceIndicator(session.resource) !== record.resource)
77
+ throw new Error("OAuth session does not match its resource identity");
78
+ record.session = session;
79
+ await store.set(JSON.stringify(record));
80
+ },
81
+ async clear(resource) {
82
+ const record = await reconcile(resource);
83
+ record.session = null;
84
+ // Preserve a tombstone so environment grants cannot revive a cleared family.
85
+ record.generation = Math.max(1, record.generation);
86
+ result.initialGrantAllowed = false;
87
+ await store.set(JSON.stringify(record));
88
+ }
89
+ };
90
+ result.clientStore = {
91
+ async load(issuer) { const record = await read(); return record !== null && Object.hasOwn(record.clients, issuer) ? record.clients[issuer] : null; },
92
+ async save(issuer, client) {
93
+ const record = await read();
94
+ if (record === null)
95
+ throw new Error("OAuth resource identity must be bound before registering a client");
96
+ Object.defineProperty(record.clients, issuer, { value: client, enumerable: true, configurable: true, writable: true });
97
+ await store.set(JSON.stringify(record));
98
+ },
99
+ async clear(issuer) {
100
+ const record = await read();
101
+ if (record === null || !Object.hasOwn(record.clients, issuer))
102
+ return;
103
+ delete record.clients[issuer];
104
+ await store.set(JSON.stringify(record));
105
+ }
106
+ };
107
+ return result;
108
+ }
@@ -147,6 +147,8 @@ export interface DefaultOAuthClientProviderOptions {
147
147
  sessionLockTimeoutMs?: number;
148
148
  /** Isolate native persisted sessions and registrations for a named profile. */
149
149
  persistenceNamespace?: string;
150
+ /** Native-owned durable logical server identity; changing URL retires its credentials. */
151
+ resourceIdentity?: string;
150
152
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
151
153
  initialGrant?: {
152
154
  resource: string;
@@ -250,6 +250,8 @@ interface DefaultOAuthClientProviderOptions {
250
250
  sessionLockTimeoutMs?: number;
251
251
  /** Isolate native persisted sessions and registrations for a named profile. */
252
252
  persistenceNamespace?: string;
253
+ /** Native-owned durable logical server identity; changing URL retires its credentials. */
254
+ resourceIdentity?: string;
253
255
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
254
256
  initialGrant?: {
255
257
  resource: string;
@@ -4709,6 +4709,114 @@ function isNonBlankOwnString(record2, key2) {
4709
4709
  return value !== void 0 && value.trim().length > 0;
4710
4710
  }
4711
4711
 
4712
+ // ../mcp-oauth/dist/client/resource-bound-store.js
4713
+ function createResourceBoundOAuthStores(options, namespace, identity) {
4714
+ assertPersistenceNamespace(identity);
4715
+ const store = createNamedSecretStore(identity, options, {
4716
+ salt: "poe-code:mcp-oauth:resources:v1",
4717
+ directory: ".poe-code/mcp-oauth/resources",
4718
+ service: "poe-code-mcp-oauth-resources",
4719
+ accountPrefix: "resource"
4720
+ }, namespace);
4721
+ const result = { initialGrantAllowed: true, sessionStore: {}, clientStore: {} };
4722
+ async function read() {
4723
+ const raw = await store.get();
4724
+ if (raw === null)
4725
+ return null;
4726
+ let value;
4727
+ try {
4728
+ value = JSON.parse(raw);
4729
+ } catch {
4730
+ throw new Error("Stored OAuth resource identity must be valid JSON; reset explicitly to recover");
4731
+ }
4732
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4733
+ throw new Error("Invalid stored OAuth resource identity");
4734
+ const record2 = value;
4735
+ if (!["version", "resource", "generation", "session", "clients"].every((key2) => Object.hasOwn(record2, key2)) || record2.version !== 1 || typeof record2.resource !== "string" || record2.resource !== canonicalizeResourceIndicator(record2.resource) || !Number.isSafeInteger(record2.generation) || record2.generation < 0 || record2.session !== null && (!isStoredOAuthSession(record2.session) || record2.session.resource !== record2.resource) || typeof record2.clients !== "object" || record2.clients === null || Array.isArray(record2.clients))
4736
+ throw new Error("Invalid stored OAuth resource identity; reset explicitly to recover");
4737
+ let resourceUrl;
4738
+ try {
4739
+ resourceUrl = new URL(record2.resource);
4740
+ } catch {
4741
+ throw new Error("Invalid stored OAuth resource identity URL");
4742
+ }
4743
+ if (!["http:", "https:"].includes(resourceUrl.protocol) || resourceUrl.username || resourceUrl.password || resourceUrl.hash)
4744
+ throw new Error("Invalid stored OAuth resource identity URL");
4745
+ for (const [issuer, client] of Object.entries(record2.clients)) {
4746
+ const normalized = normalizeStoredOAuthClient(client);
4747
+ let url;
4748
+ try {
4749
+ url = new URL(issuer);
4750
+ } catch {
4751
+ throw new Error("Invalid stored OAuth resource client issuer");
4752
+ }
4753
+ if (normalized === null || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
4754
+ throw new Error("Invalid stored OAuth resource client");
4755
+ Object.defineProperty(record2.clients, issuer, { value: normalized, enumerable: true, configurable: true, writable: true });
4756
+ }
4757
+ return record2;
4758
+ }
4759
+ async function reconcile(resource) {
4760
+ resource = canonicalizeResourceIndicator(resource);
4761
+ const existing = await read();
4762
+ if (existing?.resource === resource) {
4763
+ result.initialGrantAllowed = existing.generation === 0;
4764
+ return existing;
4765
+ }
4766
+ const record2 = { version: 1, resource, generation: existing === null ? 0 : existing.generation + 1, session: null, clients: {} };
4767
+ if (!Number.isSafeInteger(record2.generation))
4768
+ throw new Error("OAuth resource identity generation limit exceeded");
4769
+ await store.set(JSON.stringify(record2));
4770
+ result.initialGrantAllowed = record2.generation === 0;
4771
+ return record2;
4772
+ }
4773
+ result.sessionStore = {
4774
+ async withLock(_resource, operation, options2) {
4775
+ if (store.withLock === void 0)
4776
+ throw new Error("OAuth resource identity backend must support transaction locks");
4777
+ return store.withLock(operation, options2);
4778
+ },
4779
+ async load(resource) {
4780
+ return (await reconcile(resource)).session;
4781
+ },
4782
+ async save(resource, session) {
4783
+ const record2 = await reconcile(resource);
4784
+ if (canonicalizeResourceIndicator(session.resource) !== record2.resource)
4785
+ throw new Error("OAuth session does not match its resource identity");
4786
+ record2.session = session;
4787
+ await store.set(JSON.stringify(record2));
4788
+ },
4789
+ async clear(resource) {
4790
+ const record2 = await reconcile(resource);
4791
+ record2.session = null;
4792
+ record2.generation = Math.max(1, record2.generation);
4793
+ result.initialGrantAllowed = false;
4794
+ await store.set(JSON.stringify(record2));
4795
+ }
4796
+ };
4797
+ result.clientStore = {
4798
+ async load(issuer) {
4799
+ const record2 = await read();
4800
+ return record2 !== null && Object.hasOwn(record2.clients, issuer) ? record2.clients[issuer] : null;
4801
+ },
4802
+ async save(issuer, client) {
4803
+ const record2 = await read();
4804
+ if (record2 === null)
4805
+ throw new Error("OAuth resource identity must be bound before registering a client");
4806
+ Object.defineProperty(record2.clients, issuer, { value: client, enumerable: true, configurable: true, writable: true });
4807
+ await store.set(JSON.stringify(record2));
4808
+ },
4809
+ async clear(issuer) {
4810
+ const record2 = await read();
4811
+ if (record2 === null || !Object.hasOwn(record2.clients, issuer))
4812
+ return;
4813
+ delete record2.clients[issuer];
4814
+ await store.set(JSON.stringify(record2));
4815
+ }
4816
+ };
4817
+ return result;
4818
+ }
4819
+
4712
4820
  // ../mcp-oauth/dist/client/default-oauth-client-provider.js
4713
4821
  import { isIP } from "node:net";
4714
4822
 
@@ -5023,8 +5131,11 @@ function createDefaultOAuthClientProvider(options) {
5023
5131
  const configuredClient = normalizeConfiguredClient(options.client);
5024
5132
  const requestedTokenMethod = normalizeOAuthTokenEndpointAuthMethod(options.client.tokenEndpointAuthMethod);
5025
5133
  const configuredTokenMethod = requestedTokenMethod ?? configuredClient?.tokenEndpointAuthMethod;
5026
- const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
5027
- const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
5134
+ if (options.resourceIdentity !== void 0 && options.sessionStore !== void 0)
5135
+ throw new Error("OAuth resourceIdentity requires native-owned persistence; custom stores own their resource trust policy");
5136
+ const resourceStores = options.resourceIdentity === void 0 ? void 0 : createResourceBoundOAuthStores(options.authStore ?? {}, options.persistenceNamespace, options.resourceIdentity);
5137
+ const sessionStore = resourceStores?.sessionStore ?? options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
5138
+ const clientStore = resourceStores?.clientStore ?? (options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace));
5028
5139
  const now = options.now ?? Date.now;
5029
5140
  const registeredClients = /* @__PURE__ */ new Map();
5030
5141
  if (options.initialGrant !== void 0) {
@@ -5117,6 +5228,11 @@ function createDefaultOAuthClientProvider(options) {
5117
5228
  const canonicalResource = canonicalizeResourceIndicator(resource);
5118
5229
  return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
5119
5230
  let session = await loadSession(canonicalResource);
5231
+ if (resourceStores !== void 0) {
5232
+ registeredClients.clear();
5233
+ if (!resourceStores.initialGrantAllowed)
5234
+ initialGrantConsumed = true;
5235
+ }
5120
5236
  if (session !== null)
5121
5237
  assertRegistrationIssuer(session.client, session.authorizationServer);
5122
5238
  if (configuredClient !== null && discovery !== void 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",