toolcraft 0.0.319 → 0.0.321

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 (21) 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 +28 -0
  5. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/bounded-json.d.ts +2 -0
  6. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/bounded-json.js +46 -0
  7. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/client-registration.js +8 -41
  8. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +14 -2
  9. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/resource-bound-store.d.ts +17 -6
  10. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/resource-bound-store.js +59 -1
  11. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-grant.d.ts +10 -0
  12. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/token-grant.js +52 -0
  13. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +2 -0
  14. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.d.ts +4 -0
  15. package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/index.js +2 -0
  16. package/node_modules/tiny-http-mcp-server/package.json +1 -1
  17. package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
  18. package/node_modules/tiny-mcp-client/dist/index.js +138 -53
  19. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  20. package/node_modules/toolcraft-schema/package.json +1 -1
  21. 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.40",
101
+ "version": "0.1.43",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.319",
116
+ "version": "0.0.321",
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.319",
126
+ "version": "0.0.321",
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.40",
101
+ "version": "0.1.43",
102
102
  "license": "MIT"
103
103
  },
104
104
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.319",
116
+ "version": "0.0.321",
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.319",
126
+ "version": "0.0.321",
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.40",
21
+ "version": "0.1.43",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -103,6 +103,14 @@ returns `void` if a new discovery lookup is needed. It honors `allowInteractive`
103
103
  recovers pending refresh outcomes through consent, and returns an owned token
104
104
  snapshot. Normal transport request authorization remains noninteractive.
105
105
 
106
+ `createResourceBoundOAuthStores(authStore, persistenceNamespace, resourceIdentity)`
107
+ exposes the native named session/client stores and `reset(resource, { signal,
108
+ timeoutMs })`. Reset acquires the raw identity backend lock, so it can recover
109
+ corrupt or undecryptable records without reading their old contents. It atomically
110
+ retires the identity's grant and registrations and writes a marker that suppresses
111
+ stale initial grants. The default lock wait is 30 seconds. Other names/profiles
112
+ are untouched, and symlink paths are still refused.
113
+
106
114
  Configure `client.metadata.scope` to request a precise scope set; broader
107
115
  discovery metadata does not override it. Explicit scopes must match the cached
108
116
  or imported grant's scope set; ordering, repeated spaces and duplicates are
@@ -134,6 +142,23 @@ Discovery binds an expired or explicitly rejected grant before silent refresh,
134
142
  using the original configured client. Persisted sessions take precedence,
135
143
  including sessions whose tokens have been cleared; an import cannot revive them.
136
144
  Input tokens are copied and invalid expiry values fail before authorization.
145
+ For a raw OAuth response, `parseOAuthTokenGrant(response, { issuedAt, expiresAt })`
146
+ returns normalized `StoredOAuthTokens`. It accepts `access_token`, `refresh_token`,
147
+ `token_type`, `scope`, `expires_in` (seconds), `expires_at` (epoch seconds), and
148
+ `expiresAt` (epoch milliseconds). Numeric absolute expiry wins over relative
149
+ lifetime; the options timestamp wins over response timestamps. The optional
150
+ `now` clock anchors a new import. JSON is bounded to 64 KiB/64 levels; malformed
151
+ credentials, scope, timing, accessors and non-JSON metadata are rejected without
152
+ quoting the input. The parser makes no network or storage requests.
153
+ For named native persistence, `createResourceBoundOAuthStores(authStore,
154
+ namespace, identity).importSession(session, { signal, timeoutMs })` explicitly
155
+ replaces the bound grant and original client in one locked document. It takes
156
+ an owned, bounded session snapshot before waiting; validates resource/issuer
157
+ binding; marks full registrations as caller-owned; and retires previous clients.
158
+ Its durable marker suppresses stale automatic environment imports after tokens
159
+ are cleared. Like reset, explicit import can recover a corrupt old document
160
+ without decrypting it. The default lock wait is 30 seconds. A session must
161
+ contain a complete usable grant and matching validated discovery metadata.
137
162
  Pass a complete DCR response as `client.registration` (or validate untrusted JSON
138
163
  with `parseOAuthClientRegistration`). Dynamic clients infer their original ID
139
164
  and secret from that response and reuse it without registering another app.
@@ -157,6 +182,9 @@ Authorization and code exchange always use the actual listener URI. Silent
157
182
  refresh keeps its original client regardless of callback changes. At interactive
158
183
  authorization, a native registration with an obsolete captured callback is
159
184
  replaced; caller-owned full registration imports retain their original identity.
185
+ Their persisted `registrationOwnership: "caller"` survives reload and refresh;
186
+ invalid-client responses never silently replace these apps. Callback changes or
187
+ expired imported secrets require an explicit registration update.
160
188
  Set `client.tokenEndpointAuthMethod` to `none`, `client_secret_post` or
161
189
  `client_secret_basic`; a full registration can supply the same field as
162
190
  `token_endpoint_auth_method`. Public clients never transmit a stored secret.
@@ -0,0 +1,2 @@
1
+ /** Copy credential JSON without invoking accessors or custom serialization. */
2
+ export declare function copyBoundedOAuthJson(value: unknown, message: string): unknown;
@@ -0,0 +1,46 @@
1
+ /** Copy credential JSON without invoking accessors or custom serialization. */
2
+ export function copyBoundedOAuthJson(value, message) {
3
+ const invalid = () => new Error(message);
4
+ let nodes = 0;
5
+ function copy(input, depth) {
6
+ if (++nodes > 20_000 || depth > 64)
7
+ throw invalid();
8
+ if (input === null || typeof input === "boolean" || typeof input === "string")
9
+ return input;
10
+ if (typeof input === "number" && Number.isFinite(input))
11
+ return input;
12
+ if (typeof input !== "object" || input === null)
13
+ throw invalid();
14
+ const descriptors = Object.getOwnPropertyDescriptors(input);
15
+ if (Array.isArray(input)) {
16
+ const length = descriptors.length?.value;
17
+ if (length > 20_000)
18
+ throw invalid();
19
+ const result = [];
20
+ for (let index = 0; index < length; index++) {
21
+ const descriptor = descriptors[String(index)];
22
+ if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
23
+ throw invalid();
24
+ result.push(copy(descriptor.value, depth + 1));
25
+ }
26
+ return result;
27
+ }
28
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
29
+ throw invalid();
30
+ return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key, descriptor]) => {
31
+ if (!Object.hasOwn(descriptor, "value"))
32
+ throw invalid();
33
+ return [key, copy(descriptor.value, depth + 1)];
34
+ }));
35
+ }
36
+ let result;
37
+ try {
38
+ result = copy(value, 0);
39
+ }
40
+ catch {
41
+ throw invalid();
42
+ }
43
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > 64 * 1024)
44
+ throw invalid();
45
+ return result;
46
+ }
@@ -1,48 +1,11 @@
1
+ import { copyBoundedOAuthJson } from "./bounded-json.js";
1
2
  import { loopbackTarget } from "./loopback-authorization.js";
2
3
  import { normalizeOAuthScope } from "./scope.js";
3
4
  import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
4
5
  /** Validate and copy a bounded JSON DCR response without quoting credential input. */
5
6
  export function parseOAuthClientRegistration(value) {
6
7
  const invalid = () => new Error("Invalid OAuth client registration metadata");
7
- let nodes = 0;
8
- function copy(input, depth) {
9
- if (++nodes > 20_000 || depth > 64)
10
- throw invalid();
11
- if (input === null || typeof input === "boolean" || typeof input === "string")
12
- return input;
13
- if (typeof input === "number" && Number.isFinite(input))
14
- return input;
15
- if (typeof input !== "object" || input === null)
16
- throw invalid();
17
- const descriptors = Object.getOwnPropertyDescriptors(input);
18
- if (Array.isArray(input)) {
19
- const length = descriptors.length?.value;
20
- if (length > 20_000)
21
- throw invalid();
22
- const result = [];
23
- for (let index = 0; index < length; index++) {
24
- const descriptor = descriptors[String(index)];
25
- if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
26
- throw invalid();
27
- result.push(copy(descriptor.value, depth + 1));
28
- }
29
- return result;
30
- }
31
- if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
32
- throw invalid();
33
- return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key, descriptor]) => {
34
- if (!Object.hasOwn(descriptor, "value"))
35
- throw invalid();
36
- return [key, copy(descriptor.value, depth + 1)];
37
- }));
38
- }
39
- let result;
40
- try {
41
- result = copy(value, 0);
42
- }
43
- catch {
44
- throw invalid();
45
- }
8
+ const result = copyBoundedOAuthJson(value, "Invalid OAuth client registration metadata");
46
9
  if (typeof result !== "object" || result === null || Array.isArray(result))
47
10
  throw invalid();
48
11
  const record = result;
@@ -71,8 +34,6 @@ export function parseOAuthClientRegistration(value) {
71
34
  catch {
72
35
  throw invalid();
73
36
  }
74
- if (Buffer.byteLength(JSON.stringify(record), "utf8") > 64 * 1024)
75
- throw invalid();
76
37
  return record;
77
38
  }
78
39
  export function normalizeStoredOAuthClient(value) {
@@ -112,6 +73,12 @@ export function normalizeStoredOAuthClient(value) {
112
73
  }
113
74
  if (method !== undefined)
114
75
  client.tokenEndpointAuthMethod = method;
76
+ const ownership = Object.hasOwn(record, "registrationOwnership") ? record.registrationOwnership : undefined;
77
+ if (ownership !== undefined) {
78
+ if (ownership !== "caller" || client.registration === undefined)
79
+ throw new Error("Invalid stored OAuth registration ownership");
80
+ client.registrationOwnership = "caller";
81
+ }
115
82
  return client;
116
83
  }
117
84
  /** Fresh DCR may describe a normalized loopback port; saved identity stays exact. */
@@ -401,6 +401,16 @@ export function createDefaultOAuthClientProvider(options) {
401
401
  };
402
402
  }
403
403
  let storedClient = await loadRegisteredClient(discovery.authorizationServer);
404
+ const importedClient = existingSession?.client.registrationOwnership === "caller" ? existingSession.client :
405
+ storedClient?.registrationOwnership === "caller" ? storedClient : undefined;
406
+ if (importedClient !== undefined) {
407
+ assertRegistrationIssuer(importedClient, discovery.authorizationServer);
408
+ if (hasExpiredClientSecret(importedClient, now))
409
+ throw new Error("OAuth client secret has expired; update the imported registration");
410
+ if (!registrationMatchesRedirect(importedClient, redirectUri))
411
+ throw new Error("OAuth imported registration does not match the configured redirect URI; update its callback configuration");
412
+ return { kind: "static", fromStoredRegistration: false, client: importedClient };
413
+ }
404
414
  if (storedClient !== null) {
405
415
  assertRegistrationIssuer(storedClient, discovery.authorizationServer);
406
416
  if (hasExpiredClientSecret(storedClient, now) || !registrationMatchesRedirect(storedClient, redirectUri)) {
@@ -653,7 +663,9 @@ function normalizeConfiguredClient(client) {
653
663
  if (clientId === undefined)
654
664
  return null;
655
665
  const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === undefined ? undefined : getOwnString(registration, "client_secret")?.trim());
656
- return normalizeStoredOAuthClient({ clientId, clientSecret, registration, tokenEndpointAuthMethod: client.tokenEndpointAuthMethod });
666
+ return normalizeStoredOAuthClient({ clientId, clientSecret, registration,
667
+ ...(registration === undefined ? {} : { registrationOwnership: "caller" }),
668
+ tokenEndpointAuthMethod: client.tokenEndpointAuthMethod });
657
669
  }
658
670
  function normalizeOptionalOAuthString(value) {
659
671
  if (value === undefined) {
@@ -829,5 +841,5 @@ function shouldReRegisterStoredDynamicClient(error, client, alreadyAttempted) {
829
841
  if ("kind" in client) {
830
842
  return client.kind === "dynamic" && client.fromStoredRegistration;
831
843
  }
832
- return true;
844
+ return client.registrationOwnership !== "caller";
833
845
  }
@@ -1,9 +1,20 @@
1
1
  import type { CreateSecretStoreInput } from "auth-store";
2
2
  import { type OAuthClientStore } from "./auth-store-session-store.js";
3
- import type { OAuthSessionStore } from "./types.js";
3
+ import type { OAuthSessionStore, StoredOAuthSession } from "./types.js";
4
+ export interface ResourceBoundOAuthStores {
5
+ readonly sessionStore: OAuthSessionStore;
6
+ readonly clientStore: OAuthClientStore;
7
+ readonly initialGrantAllowed: boolean;
8
+ /** Retire grants/clients even when their document cannot be decrypted or parsed. */
9
+ /** Explicitly replace the bound grant and original client in one transaction. */
10
+ importSession(session: StoredOAuthSession, options?: {
11
+ signal?: AbortSignal;
12
+ timeoutMs?: number;
13
+ }): Promise<void>;
14
+ reset(resource: string, options?: {
15
+ signal?: AbortSignal;
16
+ timeoutMs?: number;
17
+ }): Promise<void>;
18
+ }
4
19
  /** 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
- };
20
+ export declare function createResourceBoundOAuthStores(options: CreateSecretStoreInput, namespace: string | undefined, identity: string): ResourceBoundOAuthStores;
@@ -1,12 +1,70 @@
1
+ import { copyBoundedOAuthJson } from "./bounded-json.js";
1
2
  import { canonicalizeResourceIndicator } from "../resource-indicator.js";
2
3
  import { assertPersistenceNamespace, createNamedSecretStore, isStoredOAuthSession } from "./auth-store-session-store.js";
3
4
  import { normalizeStoredOAuthClient } from "./client-registration.js";
4
5
  /** One locked document owns a logical server's URL history, grant and clients. */
5
6
  export function createResourceBoundOAuthStores(options, namespace, identity) {
6
7
  assertPersistenceNamespace(identity);
8
+ assertPersistenceNamespace(namespace);
7
9
  const store = createNamedSecretStore(identity, options, { salt: "poe-code:mcp-oauth:resources:v1",
8
10
  directory: ".poe-code/mcp-oauth/resources", service: "poe-code-mcp-oauth-resources", accountPrefix: "resource" }, namespace);
9
- const result = { initialGrantAllowed: true, sessionStore: {}, clientStore: {} };
11
+ const result = { initialGrantAllowed: true, sessionStore: {}, clientStore: {},
12
+ async reset(resource, options = {}) {
13
+ options.signal?.throwIfAborted();
14
+ let url;
15
+ try {
16
+ url = new URL(resource);
17
+ }
18
+ catch {
19
+ throw new Error("OAuth reset resource must be an absolute HTTP URL");
20
+ }
21
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
22
+ throw new Error("OAuth reset resource must be an HTTP URL without credentials or fragment");
23
+ await replace({ version: 1, resource: canonicalizeResourceIndicator(url), generation: 1, session: null, clients: {} }, options);
24
+ },
25
+ async importSession(value, options = {}) {
26
+ options.signal?.throwIfAborted();
27
+ const session = copyBoundedOAuthJson(value, "Invalid OAuth import session");
28
+ try {
29
+ if (!isStoredOAuthSession(session) || session.tokens === undefined || session.refreshState !== undefined)
30
+ throw new Error("Invalid session");
31
+ const resource = new URL(session.resource), issuer = new URL(session.authorizationServer);
32
+ if ([resource, issuer].some(url => !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash) ||
33
+ session.discovery.authorizationServerMetadata.issuer !== session.authorizationServer ||
34
+ typeof session.discovery.resourceMetadata.resource !== "string" ||
35
+ canonicalizeResourceIndicator(session.discovery.resourceMetadata.resource) !== canonicalizeResourceIndicator(resource))
36
+ throw new Error("Invalid binding");
37
+ session.client = normalizeStoredOAuthClient(session.client);
38
+ if (session.client.registration !== undefined)
39
+ session.client.registrationOwnership = "caller";
40
+ if (session.client.registration?.issuer !== undefined && session.client.registration.issuer !== null &&
41
+ session.client.registration.issuer !== session.authorizationServer)
42
+ throw new Error("Invalid registration issuer");
43
+ new Headers({ Authorization: `Bearer ${session.tokens.accessToken}` });
44
+ session.resource = canonicalizeResourceIndicator(resource);
45
+ }
46
+ catch {
47
+ throw new Error("Invalid OAuth import session or resource binding");
48
+ }
49
+ await replace({ version: 1, resource: session.resource, generation: 1, session,
50
+ clients: { [session.authorizationServer]: session.client } }, options);
51
+ }
52
+ };
53
+ async function replace(record, options) {
54
+ options.signal?.throwIfAborted();
55
+ const timeoutMs = options.timeoutMs ?? 30_000;
56
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647)
57
+ throw new Error("OAuth replacement timeoutMs must be a positive supported timer interval");
58
+ if (store.withLock === undefined)
59
+ throw new Error("OAuth resource identity backend must support transaction locks");
60
+ // Strict reconciliation reads are bypassed only for explicit replacement,
61
+ // while the raw stable backend lock still serializes every identity writer.
62
+ await store.withLock(async () => {
63
+ options.signal?.throwIfAborted();
64
+ await store.set(JSON.stringify(record));
65
+ result.initialGrantAllowed = false;
66
+ }, { signal: options.signal, timeoutMs });
67
+ }
10
68
  async function read() {
11
69
  const raw = await store.get();
12
70
  if (raw === null)
@@ -0,0 +1,10 @@
1
+ import type { StoredOAuthTokens } from "./types.js";
2
+ export interface OAuthTokenGrantImportOptions {
3
+ /** Absolute Unix epoch milliseconds; a numeric value takes precedence. */
4
+ readonly expiresAt?: number | null;
5
+ /** Original Unix epoch milliseconds for a delayed relative-lifetime import. */
6
+ readonly issuedAt?: number;
7
+ readonly now?: () => number;
8
+ }
9
+ /** Validate a raw RFC token response and anchor its lifetime once for persistence. */
10
+ export declare function parseOAuthTokenGrant(value: unknown, options?: OAuthTokenGrantImportOptions): StoredOAuthTokens;
@@ -0,0 +1,52 @@
1
+ import { copyBoundedOAuthJson } from "./bounded-json.js";
2
+ import { normalizeOAuthScope } from "./scope.js";
3
+ /** Validate a raw RFC token response and anchor its lifetime once for persistence. */
4
+ export function parseOAuthTokenGrant(value, options = {}) {
5
+ const invalid = () => new Error("Invalid OAuth token grant");
6
+ const result = copyBoundedOAuthJson(value, "Invalid OAuth token grant");
7
+ if (typeof result !== "object" || result === null || Array.isArray(result))
8
+ throw invalid();
9
+ const record = result;
10
+ const own = (key) => Object.hasOwn(record, key) ? record[key] : undefined;
11
+ const access = own("access_token"), refresh = own("refresh_token"), type = own("token_type");
12
+ if (typeof access !== "string" || access.trim() === "" || typeof type !== "string" || type.toLowerCase() !== "bearer" ||
13
+ (refresh !== undefined && (typeof refresh !== "string" || refresh.trim() === "")))
14
+ throw invalid();
15
+ const accessToken = access.trim();
16
+ try {
17
+ new Headers({ Authorization: `Bearer ${accessToken}` });
18
+ }
19
+ catch {
20
+ throw invalid();
21
+ }
22
+ const lifetime = own("expires_in"), seconds = own("expires_at"), milliseconds = own("expiresAt");
23
+ if ((lifetime !== undefined && (typeof lifetime !== "number" || !Number.isSafeInteger(lifetime) || lifetime < 0)) ||
24
+ (seconds !== undefined && seconds !== null && (typeof seconds !== "number" || !Number.isSafeInteger(seconds))))
25
+ throw invalid();
26
+ const validTimestamp = (input) => typeof input === "number" && Number.isSafeInteger(input) && Math.abs(input) <= 8_640_000_000_000_000;
27
+ if ((milliseconds !== undefined && milliseconds !== null && !validTimestamp(milliseconds)) ||
28
+ (options.expiresAt !== undefined && options.expiresAt !== null && !validTimestamp(options.expiresAt)) ||
29
+ (options.issuedAt !== undefined && !validTimestamp(options.issuedAt)))
30
+ throw invalid();
31
+ // Validate every supplied timing field, even if a higher-precedence absolute
32
+ // value is selected. Reload uses only the resulting normalized timestamp.
33
+ const absoluteSeconds = typeof seconds === "number" ? seconds * 1000 : undefined;
34
+ if (absoluteSeconds !== undefined && !validTimestamp(absoluteSeconds))
35
+ throw invalid();
36
+ const relative = typeof lifetime === "number" ? (options.issuedAt ?? (options.now ?? Date.now)()) + lifetime * 1000 : undefined;
37
+ if (relative !== undefined && !validTimestamp(relative))
38
+ throw invalid();
39
+ const expiresAt = options.expiresAt ?? (typeof milliseconds === "number" ? milliseconds : undefined) ?? absoluteSeconds ?? relative ?? null;
40
+ let scope;
41
+ try {
42
+ scope = normalizeOAuthScope(own("scope"));
43
+ }
44
+ catch {
45
+ throw invalid();
46
+ }
47
+ if (own("scope") !== undefined && scope === undefined)
48
+ throw invalid();
49
+ return { accessToken, tokenType: "Bearer", expiresAt,
50
+ ...(refresh === undefined ? {} : { refreshToken: refresh.trim() }),
51
+ ...(scope === undefined ? {} : { scope }) };
52
+ }
@@ -100,6 +100,8 @@ export interface OAuthClientRegistration extends Record<string, unknown> {
100
100
  }
101
101
  export interface StoredOAuthClient {
102
102
  clientId: string;
103
+ /** Externally registered apps must never be replaced by native DCR on reload. */
104
+ registrationOwnership?: "caller";
103
105
  /** Actual listener URI submitted when this native client was registered. */
104
106
  requestedRedirectUri?: string;
105
107
  clientSecret?: string;
@@ -1,4 +1,8 @@
1
+ export { parseOAuthTokenGrant } from "./client/token-grant.js";
2
+ export type { OAuthTokenGrantImportOptions } from "./client/token-grant.js";
1
3
  export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
4
+ export { createResourceBoundOAuthStores } from "./client/resource-bound-store.js";
5
+ export type { ResourceBoundOAuthStores } from "./client/resource-bound-store.js";
2
6
  export { parseOAuthClientRegistration } from "./client/client-registration.js";
3
7
  export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
4
8
  export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
@@ -1,4 +1,6 @@
1
+ export { parseOAuthTokenGrant } from "./client/token-grant.js";
1
2
  export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
3
+ export { createResourceBoundOAuthStores } from "./client/resource-bound-store.js";
2
4
  export { parseOAuthClientRegistration } from "./client/client-registration.js";
3
5
  export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
4
6
  export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.40",
3
+ "version": "0.1.43",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -203,6 +203,8 @@ interface OAuthClientRegistration extends Record<string, unknown> {
203
203
  }
204
204
  interface StoredOAuthClient {
205
205
  clientId: string;
206
+ /** Externally registered apps must never be replaced by native DCR on reload. */
207
+ registrationOwnership?: "caller";
206
208
  /** Actual listener URI submitted when this native client was registered. */
207
209
  requestedRedirectUri?: string;
208
210
  clientSecret?: string;
@@ -3395,6 +3395,62 @@ var SubscriptionManager = class {
3395
3395
  }
3396
3396
  };
3397
3397
 
3398
+ // ../mcp-oauth/dist/client/bounded-json.js
3399
+ function copyBoundedOAuthJson(value, message) {
3400
+ const invalid = () => new Error(message);
3401
+ let nodes = 0;
3402
+ function copy(input, depth) {
3403
+ if (++nodes > 2e4 || depth > 64)
3404
+ throw invalid();
3405
+ if (input === null || typeof input === "boolean" || typeof input === "string")
3406
+ return input;
3407
+ if (typeof input === "number" && Number.isFinite(input))
3408
+ return input;
3409
+ if (typeof input !== "object" || input === null)
3410
+ throw invalid();
3411
+ const descriptors = Object.getOwnPropertyDescriptors(input);
3412
+ if (Array.isArray(input)) {
3413
+ const length = descriptors.length?.value;
3414
+ if (length > 2e4)
3415
+ throw invalid();
3416
+ const result2 = [];
3417
+ for (let index = 0; index < length; index++) {
3418
+ const descriptor = descriptors[String(index)];
3419
+ if (descriptor === void 0 || !Object.hasOwn(descriptor, "value"))
3420
+ throw invalid();
3421
+ result2.push(copy(descriptor.value, depth + 1));
3422
+ }
3423
+ return result2;
3424
+ }
3425
+ if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
3426
+ throw invalid();
3427
+ return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key2, descriptor]) => {
3428
+ if (!Object.hasOwn(descriptor, "value"))
3429
+ throw invalid();
3430
+ return [key2, copy(descriptor.value, depth + 1)];
3431
+ }));
3432
+ }
3433
+ let result;
3434
+ try {
3435
+ result = copy(value, 0);
3436
+ } catch {
3437
+ throw invalid();
3438
+ }
3439
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > 64 * 1024)
3440
+ throw invalid();
3441
+ return result;
3442
+ }
3443
+
3444
+ // ../mcp-oauth/dist/client/scope.js
3445
+ function normalizeOAuthScope(scope) {
3446
+ if (scope === void 0)
3447
+ return void 0;
3448
+ if (typeof scope !== "string" || [...scope].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
3449
+ throw new Error("Invalid OAuth scope syntax");
3450
+ const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
3451
+ return normalized || void 0;
3452
+ }
3453
+
3398
3454
  // ../mcp-oauth/dist/client/loopback-authorization.js
3399
3455
  import http from "node:http";
3400
3456
 
@@ -3700,16 +3756,6 @@ function buildSuccessPage(landingPage) {
3700
3756
  ].join("");
3701
3757
  }
3702
3758
 
3703
- // ../mcp-oauth/dist/client/scope.js
3704
- function normalizeOAuthScope(scope) {
3705
- if (scope === void 0)
3706
- return void 0;
3707
- if (typeof scope !== "string" || [...scope].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
3708
- throw new Error("Invalid OAuth scope syntax");
3709
- const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
3710
- return normalized || void 0;
3711
- }
3712
-
3713
3759
  // ../mcp-oauth/dist/client/token-auth-method.js
3714
3760
  function normalizeOAuthTokenEndpointAuthMethod(value) {
3715
3761
  if (value === void 0 || value === null)
@@ -3722,44 +3768,7 @@ function normalizeOAuthTokenEndpointAuthMethod(value) {
3722
3768
  // ../mcp-oauth/dist/client/client-registration.js
3723
3769
  function parseOAuthClientRegistration(value) {
3724
3770
  const invalid = () => new Error("Invalid OAuth client registration metadata");
3725
- let nodes = 0;
3726
- function copy(input, depth) {
3727
- if (++nodes > 2e4 || depth > 64)
3728
- throw invalid();
3729
- if (input === null || typeof input === "boolean" || typeof input === "string")
3730
- return input;
3731
- if (typeof input === "number" && Number.isFinite(input))
3732
- return input;
3733
- if (typeof input !== "object" || input === null)
3734
- throw invalid();
3735
- const descriptors = Object.getOwnPropertyDescriptors(input);
3736
- if (Array.isArray(input)) {
3737
- const length = descriptors.length?.value;
3738
- if (length > 2e4)
3739
- throw invalid();
3740
- const result2 = [];
3741
- for (let index = 0; index < length; index++) {
3742
- const descriptor = descriptors[String(index)];
3743
- if (descriptor === void 0 || !Object.hasOwn(descriptor, "value"))
3744
- throw invalid();
3745
- result2.push(copy(descriptor.value, depth + 1));
3746
- }
3747
- return result2;
3748
- }
3749
- if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
3750
- throw invalid();
3751
- return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key2, descriptor]) => {
3752
- if (!Object.hasOwn(descriptor, "value"))
3753
- throw invalid();
3754
- return [key2, copy(descriptor.value, depth + 1)];
3755
- }));
3756
- }
3757
- let result;
3758
- try {
3759
- result = copy(value, 0);
3760
- } catch {
3761
- throw invalid();
3762
- }
3771
+ const result = copyBoundedOAuthJson(value, "Invalid OAuth client registration metadata");
3763
3772
  if (typeof result !== "object" || result === null || Array.isArray(result))
3764
3773
  throw invalid();
3765
3774
  const record2 = result;
@@ -3804,8 +3813,6 @@ function parseOAuthClientRegistration(value) {
3804
3813
  } catch {
3805
3814
  throw invalid();
3806
3815
  }
3807
- if (Buffer.byteLength(JSON.stringify(record2), "utf8") > 64 * 1024)
3808
- throw invalid();
3809
3816
  return record2;
3810
3817
  }
3811
3818
  function normalizeStoredOAuthClient(value) {
@@ -3843,6 +3850,12 @@ function normalizeStoredOAuthClient(value) {
3843
3850
  }
3844
3851
  if (method !== void 0)
3845
3852
  client.tokenEndpointAuthMethod = method;
3853
+ const ownership = Object.hasOwn(record2, "registrationOwnership") ? record2.registrationOwnership : void 0;
3854
+ if (ownership !== void 0) {
3855
+ if (ownership !== "caller" || client.registration === void 0)
3856
+ throw new Error("Invalid stored OAuth registration ownership");
3857
+ client.registrationOwnership = "caller";
3858
+ }
3846
3859
  return client;
3847
3860
  }
3848
3861
  function registrationMatchesRedirect(client, requestedUri, fresh = false) {
@@ -4712,13 +4725,70 @@ function isNonBlankOwnString(record2, key2) {
4712
4725
  // ../mcp-oauth/dist/client/resource-bound-store.js
4713
4726
  function createResourceBoundOAuthStores(options, namespace, identity) {
4714
4727
  assertPersistenceNamespace(identity);
4728
+ assertPersistenceNamespace(namespace);
4715
4729
  const store = createNamedSecretStore(identity, options, {
4716
4730
  salt: "poe-code:mcp-oauth:resources:v1",
4717
4731
  directory: ".poe-code/mcp-oauth/resources",
4718
4732
  service: "poe-code-mcp-oauth-resources",
4719
4733
  accountPrefix: "resource"
4720
4734
  }, namespace);
4721
- const result = { initialGrantAllowed: true, sessionStore: {}, clientStore: {} };
4735
+ const result = {
4736
+ initialGrantAllowed: true,
4737
+ sessionStore: {},
4738
+ clientStore: {},
4739
+ async reset(resource, options2 = {}) {
4740
+ options2.signal?.throwIfAborted();
4741
+ let url;
4742
+ try {
4743
+ url = new URL(resource);
4744
+ } catch {
4745
+ throw new Error("OAuth reset resource must be an absolute HTTP URL");
4746
+ }
4747
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
4748
+ throw new Error("OAuth reset resource must be an HTTP URL without credentials or fragment");
4749
+ await replace({ version: 1, resource: canonicalizeResourceIndicator(url), generation: 1, session: null, clients: {} }, options2);
4750
+ },
4751
+ async importSession(value, options2 = {}) {
4752
+ options2.signal?.throwIfAborted();
4753
+ const session = copyBoundedOAuthJson(value, "Invalid OAuth import session");
4754
+ try {
4755
+ if (!isStoredOAuthSession(session) || session.tokens === void 0 || session.refreshState !== void 0)
4756
+ throw new Error("Invalid session");
4757
+ const resource = new URL(session.resource), issuer = new URL(session.authorizationServer);
4758
+ if ([resource, issuer].some((url) => !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash) || session.discovery.authorizationServerMetadata.issuer !== session.authorizationServer || typeof session.discovery.resourceMetadata.resource !== "string" || canonicalizeResourceIndicator(session.discovery.resourceMetadata.resource) !== canonicalizeResourceIndicator(resource))
4759
+ throw new Error("Invalid binding");
4760
+ session.client = normalizeStoredOAuthClient(session.client);
4761
+ if (session.client.registration !== void 0)
4762
+ session.client.registrationOwnership = "caller";
4763
+ if (session.client.registration?.issuer !== void 0 && session.client.registration.issuer !== null && session.client.registration.issuer !== session.authorizationServer)
4764
+ throw new Error("Invalid registration issuer");
4765
+ new Headers({ Authorization: `Bearer ${session.tokens.accessToken}` });
4766
+ session.resource = canonicalizeResourceIndicator(resource);
4767
+ } catch {
4768
+ throw new Error("Invalid OAuth import session or resource binding");
4769
+ }
4770
+ await replace({
4771
+ version: 1,
4772
+ resource: session.resource,
4773
+ generation: 1,
4774
+ session,
4775
+ clients: { [session.authorizationServer]: session.client }
4776
+ }, options2);
4777
+ }
4778
+ };
4779
+ async function replace(record2, options2) {
4780
+ options2.signal?.throwIfAborted();
4781
+ const timeoutMs = options2.timeoutMs ?? 3e4;
4782
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
4783
+ throw new Error("OAuth replacement timeoutMs must be a positive supported timer interval");
4784
+ if (store.withLock === void 0)
4785
+ throw new Error("OAuth resource identity backend must support transaction locks");
4786
+ await store.withLock(async () => {
4787
+ options2.signal?.throwIfAborted();
4788
+ await store.set(JSON.stringify(record2));
4789
+ result.initialGrantAllowed = false;
4790
+ }, { signal: options2.signal, timeoutMs });
4791
+ }
4722
4792
  async function read() {
4723
4793
  const raw = await store.get();
4724
4794
  if (raw === null)
@@ -5497,6 +5567,15 @@ function createDefaultOAuthClientProvider(options) {
5497
5567
  };
5498
5568
  }
5499
5569
  let storedClient = await loadRegisteredClient(discovery.authorizationServer);
5570
+ const importedClient = existingSession?.client.registrationOwnership === "caller" ? existingSession.client : storedClient?.registrationOwnership === "caller" ? storedClient : void 0;
5571
+ if (importedClient !== void 0) {
5572
+ assertRegistrationIssuer(importedClient, discovery.authorizationServer);
5573
+ if (hasExpiredClientSecret(importedClient, now))
5574
+ throw new Error("OAuth client secret has expired; update the imported registration");
5575
+ if (!registrationMatchesRedirect(importedClient, redirectUri))
5576
+ throw new Error("OAuth imported registration does not match the configured redirect URI; update its callback configuration");
5577
+ return { kind: "static", fromStoredRegistration: false, client: importedClient };
5578
+ }
5500
5579
  if (storedClient !== null) {
5501
5580
  assertRegistrationIssuer(storedClient, discovery.authorizationServer);
5502
5581
  if (hasExpiredClientSecret(storedClient, now) || !registrationMatchesRedirect(storedClient, redirectUri)) {
@@ -5723,7 +5802,13 @@ function normalizeConfiguredClient(client) {
5723
5802
  if (clientId === void 0)
5724
5803
  return null;
5725
5804
  const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === void 0 ? void 0 : getOwnString2(registration, "client_secret")?.trim());
5726
- return normalizeStoredOAuthClient({ clientId, clientSecret, registration, tokenEndpointAuthMethod: client.tokenEndpointAuthMethod });
5805
+ return normalizeStoredOAuthClient({
5806
+ clientId,
5807
+ clientSecret,
5808
+ registration,
5809
+ ...registration === void 0 ? {} : { registrationOwnership: "caller" },
5810
+ tokenEndpointAuthMethod: client.tokenEndpointAuthMethod
5811
+ });
5727
5812
  }
5728
5813
  function normalizeOptionalOAuthString(value) {
5729
5814
  if (value === void 0) {
@@ -5892,7 +5977,7 @@ function shouldReRegisterStoredDynamicClient(error, client, alreadyAttempted) {
5892
5977
  if ("kind" in client) {
5893
5978
  return client.kind === "dynamic" && client.fromStoredRegistration;
5894
5979
  }
5895
- return true;
5980
+ return client.registrationOwnership !== "caller";
5896
5981
  }
5897
5982
 
5898
5983
  // ../tiny-stdio-mcp-server/dist/headers.js
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.319",
11
+ "version": "0.0.321",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.319",
3
+ "version": "0.0.321",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.319",
3
+ "version": "0.0.321",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -158,7 +158,7 @@
158
158
  "yaml"
159
159
  ],
160
160
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.319",
161
+ "toolcraft-schema": "0.0.321",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",