toolcraft 0.0.315 → 0.0.317

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.
Files changed (19) hide show
  1. package/composition.json +3 -3
  2. package/dist/composition.json +3 -3
  3. package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
  4. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/README.md +32 -2
  5. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +9 -2
  6. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +2 -2
  7. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.d.ts +2 -0
  8. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.js +45 -0
  9. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +71 -9
  10. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/resource-bound-store.d.ts +9 -0
  11. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/resource-bound-store.js +108 -0
  12. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +13 -1
  13. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.d.ts +1 -1
  14. package/node_modules/tiny-http-mcp-server/package.json +1 -1
  15. package/node_modules/tiny-mcp-client/dist/index.d.ts +13 -1
  16. package/node_modules/tiny-mcp-client/dist/index.js +1364 -1159
  17. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  18. package/node_modules/toolcraft-schema/package.json +1 -1
  19. package/package.json +2 -2
package/composition.json CHANGED
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.34",
101
+ "version": "0.1.37",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.315",
116
+ "version": "0.0.317",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.315",
126
+ "version": "0.0.317",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "tiny-http-mcp-server",
101
- "version": "0.1.34",
101
+ "version": "0.1.37",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.315",
116
+ "version": "0.0.317",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.315",
126
+ "version": "0.0.317",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.34",
21
+ "version": "0.1.37",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -102,12 +102,26 @@ 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
 
108
117
  Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
109
- `tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
110
- and optional `scope`. A fresh imported token is used only for its resource.
118
+ `tokenType: "Bearer"`, optional `expiresAt` (Unix epoch milliseconds or `null`
119
+ if unknown), and optional `scope`. `expiresIn` is a lifetime in seconds and is
120
+ anchored once at import. For a delayed import, provide the original `issuedAt`
121
+ in epoch milliseconds or its real absolute `expiresAt`; a numeric absolute
122
+ expiry takes precedence. Without either, the relative value means remaining
123
+ lifetime at import. Omitted expiry stays unknown. A fresh imported token is
124
+ used only for its resource.
111
125
  Discovery binds an expired or explicitly rejected grant before silent refresh,
112
126
  using the original configured client. Persisted sessions take precedence,
113
127
  including sessions whose tokens have been cleared; an import cannot revive them.
@@ -119,6 +133,22 @@ Explicit ID/secret values must agree with the imported response. Sessions and
119
133
  native registration stores retain arrays, issuance/expiry timestamps and JSON
120
134
  provider metadata. Registration input is copied, bounded to 64 KiB and 64
121
135
  levels, and rejects invalid standard field types and non-JSON extensions.
136
+ An optional registration `issuer` must match discovery and the persisted
137
+ authorization server exactly. Contradictory metadata fails without activating
138
+ or redeeming the grant. `client_secret_expires_at` uses Unix epoch seconds;
139
+ zero means no expiry. Live access tokens remain usable after secret expiry,
140
+ but an expired secret is never submitted for refresh. Native DCR can replace
141
+ an expired registration during explicit authorization; caller-owned imports
142
+ must be updated. Headless requests retain the old record and report recovery
143
+ is required without creating a pending refresh marker.
144
+ Native registrations retain `requestedRedirectUri`, the actual listener URI
145
+ submitted to DCR, separately from the full response metadata. Fresh responses
146
+ may normalize a loopback port or represent IPv4 loopback as portless localhost;
147
+ host, path, scheme, query and fragment differences outside that boundary fail.
148
+ Authorization and code exchange always use the actual listener URI. Silent
149
+ refresh keeps its original client regardless of callback changes. At interactive
150
+ authorization, a native registration with an obsolete captured callback is
151
+ replaced; caller-owned full registration imports retain their original identity.
122
152
  Set `client.tokenEndpointAuthMethod` to `none`, `client_secret_post` or
123
153
  `client_secret_basic`; a full registration can supply the same field as
124
154
  `token_endpoint_auth_method`. Public clients never transmit a stored secret.
@@ -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
  }
@@ -2,3 +2,5 @@ import type { OAuthClientRegistration, StoredOAuthClient } from "./types.js";
2
2
  /** Validate and copy a bounded JSON DCR response without quoting credential input. */
3
3
  export declare function parseOAuthClientRegistration(value: unknown): OAuthClientRegistration;
4
4
  export declare function normalizeStoredOAuthClient(value: unknown): StoredOAuthClient | null;
5
+ /** Fresh DCR may describe a normalized loopback port; saved identity stays exact. */
6
+ export declare function registrationMatchesRedirect(client: StoredOAuthClient, requestedUri: string, fresh?: boolean): boolean;
@@ -1,3 +1,4 @@
1
+ import { loopbackTarget } from "./loopback-authorization.js";
1
2
  import { normalizeOAuthScope } from "./scope.js";
2
3
  import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
3
4
  /** Validate and copy a bounded JSON DCR response without quoting credential input. */
@@ -84,6 +85,18 @@ export function normalizeStoredOAuthClient(value) {
84
85
  (clientSecret !== undefined && (typeof clientSecret !== "string" || clientSecret.trim() === "")))
85
86
  return null;
86
87
  const client = { clientId: clientId.trim(), ...(clientSecret === undefined ? {} : { clientSecret: clientSecret.trim() }) };
88
+ const requestedRedirectUri = Object.hasOwn(record, "requestedRedirectUri") ? record.requestedRedirectUri : undefined;
89
+ if (requestedRedirectUri !== undefined) {
90
+ try {
91
+ if (typeof requestedRedirectUri !== "string")
92
+ throw new Error("Invalid redirect identity");
93
+ loopbackTarget({ redirectUri: requestedRedirectUri });
94
+ }
95
+ catch {
96
+ throw new Error("Invalid stored OAuth registration redirect identity");
97
+ }
98
+ client.requestedRedirectUri = requestedRedirectUri;
99
+ }
87
100
  const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record, "tokenEndpointAuthMethod") ? record.tokenEndpointAuthMethod : undefined);
88
101
  if (Object.hasOwn(record, "registration") && record.registration !== undefined) {
89
102
  const registration = parseOAuthClientRegistration(record.registration);
@@ -101,3 +114,35 @@ export function normalizeStoredOAuthClient(value) {
101
114
  client.tokenEndpointAuthMethod = method;
102
115
  return client;
103
116
  }
117
+ /** Fresh DCR may describe a normalized loopback port; saved identity stays exact. */
118
+ export function registrationMatchesRedirect(client, requestedUri, fresh = false) {
119
+ if (client.requestedRedirectUri !== undefined)
120
+ return client.requestedRedirectUri === requestedUri;
121
+ const redirects = client.registration?.redirect_uris;
122
+ if (redirects === undefined || redirects === null || redirects.length === 0)
123
+ return true;
124
+ return redirects.some(returnedUri => {
125
+ if (returnedUri === requestedUri)
126
+ return true;
127
+ if (!fresh)
128
+ return false;
129
+ let returned, requested;
130
+ try {
131
+ returned = new URL(returnedUri);
132
+ requested = new URL(requestedUri);
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ if (requested.protocol !== "http:" || returned.protocol !== "http:" ||
138
+ !["127.0.0.1", "[::1]", "localhost"].includes(requested.hostname))
139
+ return false;
140
+ const sameHost = returned.hostname === requested.hostname;
141
+ const normalizedIpv4 = requested.hostname === "127.0.0.1" && returned.hostname === "localhost" && returned.port === "";
142
+ if (!sameHost && !normalizedIpv4)
143
+ return false;
144
+ returned.hostname = requested.hostname;
145
+ returned.port = requested.port;
146
+ return returned.href === requested.href;
147
+ });
148
+ }
@@ -1,4 +1,5 @@
1
- import { normalizeStoredOAuthClient, parseOAuthClientRegistration } from "./client-registration.js";
1
+ import { createResourceBoundOAuthStores } from "./resource-bound-store.js";
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";
4
5
  import { isIP } from "node:net";
@@ -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) {
@@ -43,7 +49,7 @@ export function createDefaultOAuthClientProvider(options) {
43
49
  }
44
50
  const initialGrant = options.initialGrant === undefined ? undefined : {
45
51
  resource: canonicalizeResourceIndicator(options.initialGrant.resource),
46
- tokens: normalizeStoredTokens(options.initialGrant.tokens),
52
+ tokens: normalizeImportedTokens(options.initialGrant.tokens, now),
47
53
  client: configuredClient
48
54
  };
49
55
  if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
@@ -127,6 +133,15 @@ 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
+ }
141
+ if (session !== null)
142
+ assertRegistrationIssuer(session.client, session.authorizationServer);
143
+ if (configuredClient !== null && discovery !== undefined)
144
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
130
145
  if (session !== null && initialGrant?.resource === canonicalResource)
131
146
  initialGrantConsumed = true;
132
147
  signal?.throwIfAborted();
@@ -173,6 +188,11 @@ export function createDefaultOAuthClientProvider(options) {
173
188
  if (session?.tokens?.refreshToken !== undefined &&
174
189
  sessionDiscovery !== undefined &&
175
190
  (forceRefresh || isExpired(session.tokens, now))) {
191
+ if (hasExpiredClientSecret(session.client, now)) {
192
+ if (!allowInteractive || options.allowInteractive === false || options.client.mode === "static" || configuredClient?.registration !== undefined)
193
+ throw new Error("OAuth client secret has expired; authorize again or update the imported registration");
194
+ return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
195
+ }
176
196
  session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch, signal);
177
197
  if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
178
198
  return session;
@@ -343,6 +363,9 @@ export function createDefaultOAuthClientProvider(options) {
343
363
  if (configuredClient === null) {
344
364
  throw new Error("OAuth client_id must not be blank");
345
365
  }
366
+ assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
367
+ if (hasExpiredClientSecret(configuredClient, now))
368
+ throw new Error("OAuth client secret has expired; update the imported registration");
346
369
  return {
347
370
  kind: "static",
348
371
  fromStoredRegistration: false,
@@ -357,7 +380,14 @@ export function createDefaultOAuthClientProvider(options) {
357
380
  client: configuredClient
358
381
  };
359
382
  }
360
- const storedClient = await loadRegisteredClient(discovery.authorizationServer);
383
+ let storedClient = await loadRegisteredClient(discovery.authorizationServer);
384
+ if (storedClient !== null) {
385
+ assertRegistrationIssuer(storedClient, discovery.authorizationServer);
386
+ if (hasExpiredClientSecret(storedClient, now) || !registrationMatchesRedirect(storedClient, redirectUri)) {
387
+ await clearRegisteredClient(discovery.authorizationServer);
388
+ storedClient = null;
389
+ }
390
+ }
361
391
  if (storedClient !== null) {
362
392
  return {
363
393
  kind: "dynamic",
@@ -366,7 +396,7 @@ export function createDefaultOAuthClientProvider(options) {
366
396
  };
367
397
  }
368
398
  if (registrationEndpoint === undefined) {
369
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
399
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
370
400
  return {
371
401
  kind: "dynamic",
372
402
  fromStoredRegistration: true,
@@ -375,7 +405,7 @@ export function createDefaultOAuthClientProvider(options) {
375
405
  }
376
406
  throw new Error("Authorization server metadata is missing registration_endpoint");
377
407
  }
378
- if (existingSession !== null && existingSession.client.clientId.length > 0) {
408
+ if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now) && registrationMatchesRedirect(existingSession.client, redirectUri)) {
379
409
  const isConfiguredStaticFallback = configuredClient !== null &&
380
410
  existingSession.client.clientId === configuredClient.clientId &&
381
411
  existingSession.client.clientSecret === configuredClient.clientSecret;
@@ -415,11 +445,17 @@ export function createDefaultOAuthClientProvider(options) {
415
445
  ...(responseMethod === undefined ? {} : { tokenEndpointAuthMethod: responseMethod }),
416
446
  registration
417
447
  };
418
- await saveRegisteredClient(discovery.authorizationServer, registeredClient);
448
+ assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
449
+ if (hasExpiredClientSecret(registeredClient, now))
450
+ throw new Error("OAuth client secret has expired in the registration response");
451
+ if (!registrationMatchesRedirect(registeredClient, redirectUri, true))
452
+ throw new Error("OAuth registration response does not match the requested redirect URI");
453
+ const clientWithRedirect = { ...registeredClient, requestedRedirectUri: redirectUri };
454
+ await saveRegisteredClient(discovery.authorizationServer, clientWithRedirect);
419
455
  return {
420
456
  kind: "dynamic",
421
457
  fromStoredRegistration: false,
422
- client: registeredClient
458
+ client: clientWithRedirect
423
459
  };
424
460
  }
425
461
  async function loadSession(resource) {
@@ -528,6 +564,21 @@ function normalizeLoadedSession(session) {
528
564
  tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
529
565
  };
530
566
  }
567
+ function normalizeImportedTokens(value, now) {
568
+ if (!isObjectRecord(value))
569
+ return undefined;
570
+ const absolute = getOwnEntry(value, "expiresAt");
571
+ const lifetime = getOwnEntry(value, "expiresIn");
572
+ const issuedAt = getOwnEntry(value, "issuedAt");
573
+ if (lifetime !== undefined && (typeof lifetime !== "number" || !Number.isSafeInteger(lifetime) || lifetime < 0))
574
+ throw new Error("OAuth initial grant has invalid relative expiry");
575
+ if (issuedAt !== undefined && (typeof issuedAt !== "number" || !Number.isSafeInteger(issuedAt) ||
576
+ Math.abs(issuedAt) > MAX_JS_DATE_MS))
577
+ throw new Error("OAuth initial grant has invalid issuance time");
578
+ const expiresAt = absolute !== undefined && absolute !== null ? absolute :
579
+ lifetime === undefined ? null : (issuedAt === undefined ? now() : issuedAt) + lifetime * 1000;
580
+ return normalizeStoredTokens({ ...value, expiresAt });
581
+ }
531
582
  function normalizeStoredTokens(value) {
532
583
  if (value === undefined || !isObjectRecord(value)) {
533
584
  return undefined;
@@ -689,6 +740,17 @@ function assertRequestMatchesResource(requestUrl, resource) {
689
740
  throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
690
741
  }
691
742
  }
743
+ function assertRegistrationIssuer(client, issuer) {
744
+ const registrationIssuer = client.registration === undefined ? undefined : getOwnString(client.registration, "issuer");
745
+ if (registrationIssuer !== undefined && registrationIssuer !== issuer)
746
+ throw new Error("OAuth client registration issuer does not match the authorization server");
747
+ }
748
+ function hasExpiredClientSecret(client, now) {
749
+ if (client.clientSecret === undefined || client.tokenEndpointAuthMethod === "none" || client.registration === undefined)
750
+ return false;
751
+ const expiry = getOwnEntry(client.registration, "client_secret_expires_at");
752
+ return typeof expiry === "number" && expiry !== 0 && expiry <= now() / 1000;
753
+ }
692
754
  function getSupportedTokenAuthMethods(metadata) {
693
755
  const value = getOwnEntry(metadata, "token_endpoint_auth_methods_supported");
694
756
  if (value === 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
+ }
@@ -70,6 +70,14 @@ export interface StoredOAuthTokens {
70
70
  expiresAt: number | null;
71
71
  scope?: string;
72
72
  }
73
+ /** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
74
+ export interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
75
+ expiresAt?: number | null;
76
+ /** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
77
+ expiresIn?: number;
78
+ /** Original issuance time in Unix epoch milliseconds. */
79
+ issuedAt?: number;
80
+ }
73
81
  /** Full RFC 7591 response, including JSON provider extensions. */
74
82
  export interface OAuthClientRegistration extends Record<string, unknown> {
75
83
  client_id: string;
@@ -84,6 +92,8 @@ export interface OAuthClientRegistration extends Record<string, unknown> {
84
92
  }
85
93
  export interface StoredOAuthClient {
86
94
  clientId: string;
95
+ /** Actual listener URI submitted when this native client was registered. */
96
+ requestedRedirectUri?: string;
87
97
  clientSecret?: string;
88
98
  registration?: OAuthClientRegistration;
89
99
  tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
@@ -137,10 +147,12 @@ export interface DefaultOAuthClientProviderOptions {
137
147
  sessionLockTimeoutMs?: number;
138
148
  /** Isolate native persisted sessions and registrations for a named profile. */
139
149
  persistenceNamespace?: string;
150
+ /** Native-owned durable logical server identity; changing URL retires its credentials. */
151
+ resourceIdentity?: string;
140
152
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
141
153
  initialGrant?: {
142
154
  resource: string;
143
- tokens: StoredOAuthTokens;
155
+ tokens: ImportedOAuthTokens;
144
156
  };
145
157
  browser: {
146
158
  openBrowser?(url: string): Promise<void>;
@@ -6,7 +6,7 @@ export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
6
6
  export { OAuthError, } from "./client/token-endpoint.js";
7
7
  export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
8
8
  export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
9
- export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, OAuthTokenEndpointAuthMethod, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, } from "./client/types.js";
9
+ export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, OAuthTokenEndpointAuthMethod, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, ImportedOAuthTokens, } from "./client/types.js";
10
10
  export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessToken, } from "./server/jwks-token-verifier.js";
11
11
  export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
12
12
  export { readBoundedResponseText } from "./http-response.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.34",
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",
@@ -173,6 +173,14 @@ interface StoredOAuthTokens {
173
173
  expiresAt: number | null;
174
174
  scope?: string;
175
175
  }
176
+ /** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
177
+ interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
178
+ expiresAt?: number | null;
179
+ /** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
180
+ expiresIn?: number;
181
+ /** Original issuance time in Unix epoch milliseconds. */
182
+ issuedAt?: number;
183
+ }
176
184
  /** Full RFC 7591 response, including JSON provider extensions. */
177
185
  interface OAuthClientRegistration extends Record<string, unknown> {
178
186
  client_id: string;
@@ -187,6 +195,8 @@ interface OAuthClientRegistration extends Record<string, unknown> {
187
195
  }
188
196
  interface StoredOAuthClient {
189
197
  clientId: string;
198
+ /** Actual listener URI submitted when this native client was registered. */
199
+ requestedRedirectUri?: string;
190
200
  clientSecret?: string;
191
201
  registration?: OAuthClientRegistration;
192
202
  tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
@@ -240,10 +250,12 @@ interface DefaultOAuthClientProviderOptions {
240
250
  sessionLockTimeoutMs?: number;
241
251
  /** Isolate native persisted sessions and registrations for a named profile. */
242
252
  persistenceNamespace?: string;
253
+ /** Native-owned durable logical server identity; changing URL retires its credentials. */
254
+ resourceIdentity?: string;
243
255
  /** Import an existing grant for one resource. Persisted sessions take precedence. */
244
256
  initialGrant?: {
245
257
  resource: string;
246
- tokens: StoredOAuthTokens;
258
+ tokens: ImportedOAuthTokens;
247
259
  };
248
260
  browser: {
249
261
  openBrowser?(url: string): Promise<void>;