toolcraft 0.0.308 → 0.0.310
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.
- package/composition.json +3 -3
- package/dist/composition.json +3 -3
- package/node_modules/tiny-http-mcp-server/dist/composition.json +1 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/README.md +22 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +72 -5
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +5 -0
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/loopback-authorization.js +1 -1
- package/node_modules/tiny-http-mcp-server/node_modules/mcp-oauth/dist/client/types.d.ts +10 -1
- package/node_modules/tiny-http-mcp-server/package.json +1 -1
- package/node_modules/tiny-mcp-client/dist/index.d.ts +11 -1
- package/node_modules/tiny-mcp-client/dist/index.js +84 -8
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- 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.
|
|
101
|
+
"version": "0.1.25",
|
|
102
102
|
"license": "MIT"
|
|
103
103
|
},
|
|
104
104
|
{
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
116
|
+
"version": "0.0.310",
|
|
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.
|
|
126
|
+
"version": "0.0.310",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
package/dist/composition.json
CHANGED
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"name": "tiny-http-mcp-server",
|
|
101
|
-
"version": "0.1.
|
|
101
|
+
"version": "0.1.25",
|
|
102
102
|
"license": "MIT"
|
|
103
103
|
},
|
|
104
104
|
{
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
116
|
+
"version": "0.0.310",
|
|
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.
|
|
126
|
+
"version": "0.0.310",
|
|
127
127
|
"license": "MIT"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
@@ -41,6 +41,7 @@ const verifier = createJwksTokenVerifier({
|
|
|
41
41
|
- `mode: "dynamic"` with optional `metadata`
|
|
42
42
|
- `mode: "static"` with `clientId`, optional `clientSecret`, optional `metadata`
|
|
43
43
|
- `allowInteractive: false` prevents interactive login while retaining cached tokens and silent refresh
|
|
44
|
+
- `initialGrant: { resource, tokens }` optionally imports an existing Bearer grant for one HTTP resource; requires the original client ID
|
|
44
45
|
- `browser.openBrowser(url)` optional
|
|
45
46
|
- `browser.readLine()` optional
|
|
46
47
|
- `browser.createServer()` optional
|
|
@@ -69,7 +70,10 @@ const verifier = createJwksTokenVerifier({
|
|
|
69
70
|
Fixed redirects support `localhost`, `127.0.0.1`, and `::1` over HTTP. Their
|
|
70
71
|
exact spelling, port, path and query are preserved through registration,
|
|
71
72
|
authorization and code exchange. Credentials, fragments, port zero and reserved
|
|
72
|
-
OAuth callback query parameters are rejected before binding a listener.
|
|
73
|
+
OAuth callback query parameters are rejected before binding a listener.
|
|
74
|
+
`createDefaultOAuthClientProvider` also checks the configured redirect before
|
|
75
|
+
creating the provider. Imported tokens that cannot be sent as HTTP header
|
|
76
|
+
values fail with diagnostics that omit their contents. Omit
|
|
73
77
|
`redirectUri` to allocate a random loopback port. Standalone callback sessions
|
|
74
78
|
accept the same `redirectUri`, `signal` and `timeoutMs` options. Cancellation,
|
|
75
79
|
timeout and explicit close settle pending code waits and release listeners.
|
|
@@ -80,9 +84,26 @@ registration, token requests and bounded token-body reads. Cancellation retains
|
|
|
80
84
|
its original reason and does not retry authorization. Custom providers should
|
|
81
85
|
observe the supplied signal and pass it to any work they start.
|
|
82
86
|
|
|
87
|
+
`authorizeRequest` may return an owned token snapshot for the request it
|
|
88
|
+
authorized. The HTTP client supplies that snapshot as `presentedTokens`, along
|
|
89
|
+
with the actual request's `requestHeaders`, to `handleUnauthorized`. Providers
|
|
90
|
+
that return `void` remain supported. The native provider compares the rejected
|
|
91
|
+
snapshot with persisted credentials: delayed 401s retry with a newer grant
|
|
92
|
+
without redeeming its refresh token again. A proven current token is refreshed
|
|
93
|
+
on 401 even when the server omits `error="invalid_token"`. Invalid provenance
|
|
94
|
+
fails without quoting token values.
|
|
95
|
+
|
|
83
96
|
Configure `client.metadata.scope` to request a precise scope set; broader
|
|
84
97
|
discovery metadata does not override it.
|
|
85
98
|
|
|
99
|
+
Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
|
|
100
|
+
`tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
|
|
101
|
+
and optional `scope`. A fresh imported token is used only for its resource.
|
|
102
|
+
Discovery binds an expired or explicitly rejected grant before silent refresh,
|
|
103
|
+
using the original configured client. Persisted sessions take precedence,
|
|
104
|
+
including sessions whose tokens have been cleared; an import cannot revive them.
|
|
105
|
+
Input tokens are copied and invalid expiry values fail before authorization.
|
|
106
|
+
|
|
86
107
|
`createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
|
|
87
108
|
|
|
88
109
|
## Environment Variables
|
|
@@ -2,7 +2,7 @@ import { isIP } from "node:net";
|
|
|
2
2
|
import { fetchMcpResponse } from "../http-fetch.js";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { createAuthStoreClientStore, createAuthStoreSessionStore } from "./auth-store-session-store.js";
|
|
5
|
-
import { createLoopbackAuthorizationSession } from "./loopback-authorization.js";
|
|
5
|
+
import { createLoopbackAuthorizationSession, loopbackTarget } from "./loopback-authorization.js";
|
|
6
6
|
import { createAuthorizationState } from "./authorization-state.js";
|
|
7
7
|
import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
|
|
8
8
|
import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse } from "./token-endpoint.js";
|
|
@@ -15,18 +15,51 @@ export function createOAuthClientProvider(options) {
|
|
|
15
15
|
return createDefaultOAuthClientProvider(options);
|
|
16
16
|
}
|
|
17
17
|
export function createDefaultOAuthClientProvider(options) {
|
|
18
|
+
loopbackTarget(options.browser);
|
|
18
19
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
|
|
19
20
|
const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
|
|
20
21
|
const now = options.now ?? Date.now;
|
|
21
22
|
const registeredClients = new Map();
|
|
22
23
|
const refreshPromises = new Map();
|
|
23
24
|
const authorizationPromises = new Map();
|
|
25
|
+
if (options.initialGrant !== undefined) {
|
|
26
|
+
let resource;
|
|
27
|
+
try {
|
|
28
|
+
resource = new URL(options.initialGrant.resource);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
|
|
32
|
+
}
|
|
33
|
+
if ((resource.protocol !== "http:" && resource.protocol !== "https:") || resource.username || resource.password || resource.hash)
|
|
34
|
+
throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
|
|
35
|
+
}
|
|
36
|
+
const initialGrant = options.initialGrant === undefined ? undefined : {
|
|
37
|
+
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
38
|
+
tokens: normalizeStoredTokens(options.initialGrant.tokens),
|
|
39
|
+
client: normalizeConfiguredClient(options.client)
|
|
40
|
+
};
|
|
41
|
+
if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
|
|
42
|
+
throw new Error("OAuth initial grant requires valid tokens and the original client ID");
|
|
43
|
+
if (initialGrant?.tokens !== undefined) {
|
|
44
|
+
try {
|
|
45
|
+
new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("OAuth initial grant access token is not a valid HTTP header value");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let initialGrantConsumed = false;
|
|
24
52
|
return {
|
|
25
53
|
async authorizeRequest(input) {
|
|
26
54
|
assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
|
|
27
55
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
28
56
|
const session = await ensureAuthorizedSession(requestUrl, undefined, input.fetch, false, false, input.signal);
|
|
29
57
|
const accessToken = session?.tokens?.accessToken;
|
|
58
|
+
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl &&
|
|
59
|
+
initialGrant.tokens !== undefined && !isExpired(initialGrant.tokens, now)) {
|
|
60
|
+
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
61
|
+
return { ...initialGrant.tokens };
|
|
62
|
+
}
|
|
30
63
|
if (session === null ||
|
|
31
64
|
accessToken === undefined ||
|
|
32
65
|
session.tokens === undefined ||
|
|
@@ -35,6 +68,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
35
68
|
}
|
|
36
69
|
assertRequestMatchesResource(requestUrl, session.resource);
|
|
37
70
|
input.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
71
|
+
return { ...session.tokens };
|
|
38
72
|
},
|
|
39
73
|
async handleUnauthorized(input) {
|
|
40
74
|
try {
|
|
@@ -42,12 +76,28 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
42
76
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
43
77
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
44
78
|
assertRequestMatchesResource(requestUrl, resource);
|
|
45
|
-
const
|
|
46
|
-
|
|
79
|
+
const cached = await loadSession(resource);
|
|
80
|
+
const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : undefined);
|
|
81
|
+
let rejectedCurrentGrant = hasCachedAccessToken(cached) || (!initialGrantConsumed && initialGrant?.resource === resource);
|
|
82
|
+
let presentedTokens = input.presentedTokens;
|
|
83
|
+
if (input.presentedTokens !== undefined) {
|
|
84
|
+
rejectedCurrentGrant = false;
|
|
85
|
+
if (input.presentedTokens !== null) {
|
|
86
|
+
const presented = normalizeStoredTokens(input.presentedTokens);
|
|
87
|
+
const header = input.requestHeaders?.get("Authorization") ?? "";
|
|
88
|
+
const separator = header.indexOf(" ");
|
|
89
|
+
if (presented === undefined || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
|
|
90
|
+
throw new Error("OAuth rejected-request provenance does not match its authorization header");
|
|
91
|
+
presentedTokens = presented;
|
|
92
|
+
rejectedCurrentGrant = currentTokens !== undefined && sameTokenGrant(currentTokens, presented);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const challengeError = input.challenge?.params.error;
|
|
96
|
+
const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || (input.presentedTokens !== undefined && challengeError === undefined));
|
|
47
97
|
const session = await ensureAuthorizedSession(resource, {
|
|
48
98
|
...input.discovery,
|
|
49
99
|
resource
|
|
50
|
-
}, input.fetch, true, forceRefresh, input.signal);
|
|
100
|
+
}, input.fetch, true, forceRefresh, input.signal, presentedTokens);
|
|
51
101
|
if (session?.tokens?.accessToken === undefined) {
|
|
52
102
|
return { action: "fail" };
|
|
53
103
|
}
|
|
@@ -62,10 +112,12 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
62
112
|
}
|
|
63
113
|
}
|
|
64
114
|
};
|
|
65
|
-
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal) {
|
|
115
|
+
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
66
116
|
signal?.throwIfAborted();
|
|
67
117
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
68
118
|
let session = await loadSession(canonicalResource);
|
|
119
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
120
|
+
initialGrantConsumed = true;
|
|
69
121
|
signal?.throwIfAborted();
|
|
70
122
|
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
71
123
|
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
@@ -76,6 +128,17 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
76
128
|
await clearSession(canonicalResource);
|
|
77
129
|
session = null;
|
|
78
130
|
}
|
|
131
|
+
if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
|
|
132
|
+
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
133
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
134
|
+
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
135
|
+
client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
|
|
136
|
+
await saveSession(canonicalResource, session);
|
|
137
|
+
initialGrantConsumed = true;
|
|
138
|
+
signal?.throwIfAborted();
|
|
139
|
+
}
|
|
140
|
+
if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
141
|
+
forceRefresh = false;
|
|
79
142
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
80
143
|
if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
81
144
|
return session;
|
|
@@ -410,6 +473,10 @@ function resolveDiscovery(discovery, session) {
|
|
|
410
473
|
authorizationServerMetadata: metadata
|
|
411
474
|
};
|
|
412
475
|
}
|
|
476
|
+
function sameTokenGrant(left, right) {
|
|
477
|
+
return left.accessToken === right.accessToken && left.refreshToken === right.refreshToken &&
|
|
478
|
+
left.tokenType === right.tokenType && left.expiresAt === right.expiresAt && left.scope === right.scope;
|
|
479
|
+
}
|
|
413
480
|
function clearSessionTokens(session) {
|
|
414
481
|
const nextSession = { ...session };
|
|
415
482
|
delete nextSession.tokens;
|
|
@@ -21,5 +21,10 @@ export interface LoopbackAuthorizationSession {
|
|
|
21
21
|
close(): void;
|
|
22
22
|
}
|
|
23
23
|
export declare function createLoopbackAuthorizationSession(options?: LoopbackAuthorizationOptions): Promise<LoopbackAuthorizationSession>;
|
|
24
|
+
export declare function loopbackTarget(options: LoopbackAuthorizationOptions): {
|
|
25
|
+
port: number;
|
|
26
|
+
host: string;
|
|
27
|
+
callbackPath: string;
|
|
28
|
+
};
|
|
24
29
|
export declare function extractCodeFromInput(input: string): string | null;
|
|
25
30
|
export declare function buildSuccessPage(landingPage?: OAuthLandingPage): string;
|
|
@@ -33,7 +33,7 @@ export interface OAuthClientProvider {
|
|
|
33
33
|
headers: Headers;
|
|
34
34
|
fetch: OAuthMetadataFetch;
|
|
35
35
|
signal?: AbortSignal;
|
|
36
|
-
}): Promise<void> | void;
|
|
36
|
+
}): Promise<StoredOAuthTokens | void> | StoredOAuthTokens | void;
|
|
37
37
|
handleUnauthorized(input: {
|
|
38
38
|
requestUrl: URL;
|
|
39
39
|
response: Response;
|
|
@@ -41,6 +41,10 @@ export interface OAuthClientProvider {
|
|
|
41
41
|
discovery: OAuthDiscoveryResult;
|
|
42
42
|
fetch: OAuthMetadataFetch;
|
|
43
43
|
signal?: AbortSignal;
|
|
44
|
+
/** Headers actually attached to this rejected request. */
|
|
45
|
+
requestHeaders?: Headers;
|
|
46
|
+
/** Owned snapshot returned when this request was authorized, or null if absent. */
|
|
47
|
+
presentedTokens?: StoredOAuthTokens | null;
|
|
44
48
|
}): Promise<{
|
|
45
49
|
action: "retry";
|
|
46
50
|
} | {
|
|
@@ -99,6 +103,11 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
99
103
|
};
|
|
100
104
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
101
105
|
allowInteractive?: boolean;
|
|
106
|
+
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
107
|
+
initialGrant?: {
|
|
108
|
+
resource: string;
|
|
109
|
+
tokens: StoredOAuthTokens;
|
|
110
|
+
};
|
|
102
111
|
browser: {
|
|
103
112
|
openBrowser?(url: string): Promise<void>;
|
|
104
113
|
/** Exact registered HTTP loopback redirect URI. */
|
|
@@ -110,7 +110,7 @@ interface OAuthClientProvider {
|
|
|
110
110
|
headers: Headers;
|
|
111
111
|
fetch: OAuthMetadataFetch;
|
|
112
112
|
signal?: AbortSignal;
|
|
113
|
-
}): Promise<void> | void;
|
|
113
|
+
}): Promise<StoredOAuthTokens | void> | StoredOAuthTokens | void;
|
|
114
114
|
handleUnauthorized(input: {
|
|
115
115
|
requestUrl: URL;
|
|
116
116
|
response: Response;
|
|
@@ -118,6 +118,10 @@ interface OAuthClientProvider {
|
|
|
118
118
|
discovery: OAuthDiscoveryResult;
|
|
119
119
|
fetch: OAuthMetadataFetch;
|
|
120
120
|
signal?: AbortSignal;
|
|
121
|
+
/** Headers actually attached to this rejected request. */
|
|
122
|
+
requestHeaders?: Headers;
|
|
123
|
+
/** Owned snapshot returned when this request was authorized, or null if absent. */
|
|
124
|
+
presentedTokens?: StoredOAuthTokens | null;
|
|
121
125
|
}): Promise<{
|
|
122
126
|
action: "retry";
|
|
123
127
|
} | {
|
|
@@ -176,6 +180,11 @@ interface DefaultOAuthClientProviderOptions {
|
|
|
176
180
|
};
|
|
177
181
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
178
182
|
allowInteractive?: boolean;
|
|
183
|
+
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
184
|
+
initialGrant?: {
|
|
185
|
+
resource: string;
|
|
186
|
+
tokens: StoredOAuthTokens;
|
|
187
|
+
};
|
|
179
188
|
browser: {
|
|
180
189
|
openBrowser?(url: string): Promise<void>;
|
|
181
190
|
/** Exact registered HTTP loopback redirect URI. */
|
|
@@ -644,6 +653,7 @@ declare class HttpTransport implements McpTransport {
|
|
|
644
653
|
private readonly oauthMetadataDiscovery;
|
|
645
654
|
private readonly inFlightFetchAbortControllers;
|
|
646
655
|
private readonly inFlightOAuthAbortControllers;
|
|
656
|
+
private readonly oauthRequestTokens;
|
|
647
657
|
private readonly openResponseReaders;
|
|
648
658
|
private readonly modernRequests;
|
|
649
659
|
private modernMode;
|
|
@@ -4594,23 +4594,54 @@ function createOAuthClientProvider(options) {
|
|
|
4594
4594
|
return createDefaultOAuthClientProvider(options);
|
|
4595
4595
|
}
|
|
4596
4596
|
function createDefaultOAuthClientProvider(options) {
|
|
4597
|
+
loopbackTarget(options.browser);
|
|
4597
4598
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore);
|
|
4598
4599
|
const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore);
|
|
4599
4600
|
const now = options.now ?? Date.now;
|
|
4600
4601
|
const registeredClients = /* @__PURE__ */ new Map();
|
|
4601
4602
|
const refreshPromises = /* @__PURE__ */ new Map();
|
|
4602
4603
|
const authorizationPromises = /* @__PURE__ */ new Map();
|
|
4604
|
+
if (options.initialGrant !== void 0) {
|
|
4605
|
+
let resource;
|
|
4606
|
+
try {
|
|
4607
|
+
resource = new URL2(options.initialGrant.resource);
|
|
4608
|
+
} catch {
|
|
4609
|
+
throw new Error("OAuth initial grant resource must be an absolute HTTP URL");
|
|
4610
|
+
}
|
|
4611
|
+
if (resource.protocol !== "http:" && resource.protocol !== "https:" || resource.username || resource.password || resource.hash)
|
|
4612
|
+
throw new Error("OAuth initial grant resource must be an HTTP URL without credentials or fragments");
|
|
4613
|
+
}
|
|
4614
|
+
const initialGrant = options.initialGrant === void 0 ? void 0 : {
|
|
4615
|
+
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
4616
|
+
tokens: normalizeStoredTokens(options.initialGrant.tokens),
|
|
4617
|
+
client: normalizeConfiguredClient(options.client)
|
|
4618
|
+
};
|
|
4619
|
+
if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
|
|
4620
|
+
throw new Error("OAuth initial grant requires valid tokens and the original client ID");
|
|
4621
|
+
if (initialGrant?.tokens !== void 0) {
|
|
4622
|
+
try {
|
|
4623
|
+
new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
|
|
4624
|
+
} catch {
|
|
4625
|
+
throw new Error("OAuth initial grant access token is not a valid HTTP header value");
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
let initialGrantConsumed = false;
|
|
4603
4629
|
return {
|
|
4604
4630
|
async authorizeRequest(input) {
|
|
4605
4631
|
assertNoAccessTokenInUrl(input.requestUrl, "Protected resource request URL");
|
|
4606
4632
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
4607
4633
|
const session = await ensureAuthorizedSession(requestUrl, void 0, input.fetch, false, false, input.signal);
|
|
4608
4634
|
const accessToken = session?.tokens?.accessToken;
|
|
4635
|
+
if (session === null && !initialGrantConsumed && initialGrant?.resource === requestUrl && initialGrant.tokens !== void 0 && !isExpired(initialGrant.tokens, now)) {
|
|
4636
|
+
input.headers.set("Authorization", `Bearer ${initialGrant.tokens.accessToken}`);
|
|
4637
|
+
return { ...initialGrant.tokens };
|
|
4638
|
+
}
|
|
4609
4639
|
if (session === null || accessToken === void 0 || session.tokens === void 0 || isExpired(session.tokens, now)) {
|
|
4610
4640
|
return;
|
|
4611
4641
|
}
|
|
4612
4642
|
assertRequestMatchesResource(requestUrl, session.resource);
|
|
4613
4643
|
input.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
4644
|
+
return { ...session.tokens };
|
|
4614
4645
|
},
|
|
4615
4646
|
async handleUnauthorized(input) {
|
|
4616
4647
|
try {
|
|
@@ -4618,11 +4649,28 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4618
4649
|
const requestUrl = canonicalizeResourceIndicator(input.requestUrl);
|
|
4619
4650
|
const resource = canonicalizeResourceIndicator(input.discovery.resource);
|
|
4620
4651
|
assertRequestMatchesResource(requestUrl, resource);
|
|
4621
|
-
const
|
|
4652
|
+
const cached = await loadSession(resource);
|
|
4653
|
+
const currentTokens = cached?.tokens ?? (!initialGrantConsumed && initialGrant?.resource === resource ? initialGrant.tokens : void 0);
|
|
4654
|
+
let rejectedCurrentGrant = hasCachedAccessToken(cached) || !initialGrantConsumed && initialGrant?.resource === resource;
|
|
4655
|
+
let presentedTokens = input.presentedTokens;
|
|
4656
|
+
if (input.presentedTokens !== void 0) {
|
|
4657
|
+
rejectedCurrentGrant = false;
|
|
4658
|
+
if (input.presentedTokens !== null) {
|
|
4659
|
+
const presented = normalizeStoredTokens(input.presentedTokens);
|
|
4660
|
+
const header = input.requestHeaders?.get("Authorization") ?? "";
|
|
4661
|
+
const separator = header.indexOf(" ");
|
|
4662
|
+
if (presented === void 0 || header.slice(0, separator).toLowerCase() !== "bearer" || header.slice(separator + 1).trim() !== presented.accessToken)
|
|
4663
|
+
throw new Error("OAuth rejected-request provenance does not match its authorization header");
|
|
4664
|
+
presentedTokens = presented;
|
|
4665
|
+
rejectedCurrentGrant = currentTokens !== void 0 && sameTokenGrant(currentTokens, presented);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
const challengeError = input.challenge?.params.error;
|
|
4669
|
+
const forceRefresh = rejectedCurrentGrant && (challengeError === "invalid_token" || input.presentedTokens !== void 0 && challengeError === void 0);
|
|
4622
4670
|
const session = await ensureAuthorizedSession(resource, {
|
|
4623
4671
|
...input.discovery,
|
|
4624
4672
|
resource
|
|
4625
|
-
}, input.fetch, true, forceRefresh, input.signal);
|
|
4673
|
+
}, input.fetch, true, forceRefresh, input.signal, presentedTokens);
|
|
4626
4674
|
if (session?.tokens?.accessToken === void 0) {
|
|
4627
4675
|
return { action: "fail" };
|
|
4628
4676
|
}
|
|
@@ -4636,10 +4684,12 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4636
4684
|
}
|
|
4637
4685
|
}
|
|
4638
4686
|
};
|
|
4639
|
-
async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal) {
|
|
4687
|
+
async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
4640
4688
|
signal?.throwIfAborted();
|
|
4641
4689
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
4642
4690
|
let session = await loadSession(canonicalResource);
|
|
4691
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
4692
|
+
initialGrantConsumed = true;
|
|
4643
4693
|
signal?.throwIfAborted();
|
|
4644
4694
|
if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
4645
4695
|
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
@@ -4648,6 +4698,21 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4648
4698
|
await clearSession(canonicalResource);
|
|
4649
4699
|
session = null;
|
|
4650
4700
|
}
|
|
4701
|
+
if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
|
|
4702
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4703
|
+
session = {
|
|
4704
|
+
resource: canonicalResource,
|
|
4705
|
+
authorizationServer: discovery.authorizationServer,
|
|
4706
|
+
client: initialGrant.client,
|
|
4707
|
+
tokens: initialGrant.tokens,
|
|
4708
|
+
discovery: toStoredDiscovery(discovery)
|
|
4709
|
+
};
|
|
4710
|
+
await saveSession(canonicalResource, session);
|
|
4711
|
+
initialGrantConsumed = true;
|
|
4712
|
+
signal?.throwIfAborted();
|
|
4713
|
+
}
|
|
4714
|
+
if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
4715
|
+
forceRefresh = false;
|
|
4651
4716
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
4652
4717
|
if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
4653
4718
|
return session;
|
|
@@ -4969,6 +5034,9 @@ function resolveDiscovery(discovery, session) {
|
|
|
4969
5034
|
authorizationServerMetadata: metadata
|
|
4970
5035
|
};
|
|
4971
5036
|
}
|
|
5037
|
+
function sameTokenGrant(left, right) {
|
|
5038
|
+
return left.accessToken === right.accessToken && left.refreshToken === right.refreshToken && left.tokenType === right.tokenType && left.expiresAt === right.expiresAt && left.scope === right.scope;
|
|
5039
|
+
}
|
|
4972
5040
|
function clearSessionTokens(session) {
|
|
4973
5041
|
const nextSession = { ...session };
|
|
4974
5042
|
delete nextSession.tokens;
|
|
@@ -6744,6 +6812,7 @@ var HttpTransport = class {
|
|
|
6744
6812
|
oauthMetadataDiscovery;
|
|
6745
6813
|
inFlightFetchAbortControllers = /* @__PURE__ */ new Set();
|
|
6746
6814
|
inFlightOAuthAbortControllers = /* @__PURE__ */ new Set();
|
|
6815
|
+
oauthRequestTokens = /* @__PURE__ */ new WeakMap();
|
|
6747
6816
|
openResponseReaders = /* @__PURE__ */ new Set();
|
|
6748
6817
|
modernRequests = /* @__PURE__ */ new Map();
|
|
6749
6818
|
modernMode = false;
|
|
@@ -7014,7 +7083,7 @@ var HttpTransport = class {
|
|
|
7014
7083
|
}
|
|
7015
7084
|
async authorizeRequestHeaders(headers, signal) {
|
|
7016
7085
|
signal?.throwIfAborted();
|
|
7017
|
-
await this.oauthProvider?.authorizeRequest?.({
|
|
7086
|
+
const tokens = await this.oauthProvider?.authorizeRequest?.({
|
|
7018
7087
|
requestUrl: new URL(this.url),
|
|
7019
7088
|
headers,
|
|
7020
7089
|
signal,
|
|
@@ -7023,6 +7092,7 @@ var HttpTransport = class {
|
|
|
7023
7092
|
signal: signal === void 0 ? init?.signal : init?.signal == null ? signal : AbortSignal.any([signal, init.signal])
|
|
7024
7093
|
})
|
|
7025
7094
|
});
|
|
7095
|
+
if (tokens !== void 0) this.oauthRequestTokens.set(headers, { ...tokens });
|
|
7026
7096
|
signal?.throwIfAborted();
|
|
7027
7097
|
return headers;
|
|
7028
7098
|
}
|
|
@@ -7166,7 +7236,7 @@ var HttpTransport = class {
|
|
|
7166
7236
|
const message = responseBody.length === 0 ? `HTTP transport POST failed (${statusDescriptor})` : `HTTP transport POST failed (${statusDescriptor}): ${responseBody}`;
|
|
7167
7237
|
throw new HttpTransportError(message, response.status, "POST");
|
|
7168
7238
|
}
|
|
7169
|
-
async maybeHandleUnauthorizedResponse(response, signal) {
|
|
7239
|
+
async maybeHandleUnauthorizedResponse(response, signal, requestHeaders, presentedTokens) {
|
|
7170
7240
|
if (response.status !== 401 || this.oauthProvider === void 0) {
|
|
7171
7241
|
return false;
|
|
7172
7242
|
}
|
|
@@ -7187,6 +7257,8 @@ var HttpTransport = class {
|
|
|
7187
7257
|
challenge,
|
|
7188
7258
|
discovery,
|
|
7189
7259
|
signal,
|
|
7260
|
+
requestHeaders: new Headers(requestHeaders),
|
|
7261
|
+
presentedTokens,
|
|
7190
7262
|
fetch: (url, init) => fetchMcpResponse(this.fetchImpl, url, {
|
|
7191
7263
|
...init,
|
|
7192
7264
|
signal: init?.signal == null ? signal : AbortSignal.any([signal, init.signal])
|
|
@@ -7327,16 +7399,20 @@ var HttpTransport = class {
|
|
|
7327
7399
|
const request = async () => {
|
|
7328
7400
|
controller.signal.throwIfAborted();
|
|
7329
7401
|
const headers = await input.createHeaders(controller.signal);
|
|
7402
|
+
const headerSnapshot = new Headers(headers);
|
|
7403
|
+
const tokens = this.oauthRequestTokens.get(headers) ?? null;
|
|
7330
7404
|
controller.signal.throwIfAborted();
|
|
7331
|
-
|
|
7405
|
+
const response = await this.fetchWithAbort(input.url ?? this.url, {
|
|
7332
7406
|
method: input.method,
|
|
7333
7407
|
headers,
|
|
7334
7408
|
body: input.body
|
|
7335
7409
|
}, controller);
|
|
7410
|
+
return { response, headers: headerSnapshot, tokens };
|
|
7336
7411
|
};
|
|
7337
7412
|
try {
|
|
7338
|
-
let
|
|
7339
|
-
if (await this.maybeHandleUnauthorizedResponse(response, controller.signal))
|
|
7413
|
+
let attempt = await request();
|
|
7414
|
+
if (await this.maybeHandleUnauthorizedResponse(attempt.response, controller.signal, attempt.headers, attempt.tokens)) attempt = await request();
|
|
7415
|
+
const response = attempt.response;
|
|
7340
7416
|
const oauthError = this.oauthProvider === void 0 ? null : this.readOAuthChallengeError(response);
|
|
7341
7417
|
if (oauthError !== null) {
|
|
7342
7418
|
void response.body?.cancel().catch(() => void 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.310",
|
|
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.
|
|
161
|
+
"toolcraft-schema": "0.0.310",
|
|
162
162
|
"toolcraft-design": "*",
|
|
163
163
|
"@poe-code/frontmatter": "*",
|
|
164
164
|
"@poe-code/agent-mcp-config": "*",
|