@yanlinglabs/winter-agent-runtime 0.0.29 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embedded-worker.js +6 -4
- package/dist/embedded.js +6 -4
- package/dist/engine.d.ts +16 -0
- package/dist/{index-jz5d90c6.js → index-51673qbw.js} +151 -120
- package/dist/{index-gmxxtqpe.js → index-9df1q7j4.js} +30 -12
- package/dist/{index-8jgwp1px.js → index-9s68t0pd.js} +170 -11
- package/dist/index-bgeqc7ax.js +52 -0
- package/dist/{index-eyddzcgs.js → index-wsfqsrh5.js} +2 -2
- package/dist/index-z6s3r4xm.js +903 -0
- package/dist/index.js +4 -2
- package/dist/mcp/client.d.ts +20 -1
- package/dist/mcp/lifecycle.d.ts +8 -0
- package/dist/mcp/transports/http.d.ts +10 -1
- package/dist/mcp/transports/sse.d.ts +4 -2
- package/dist/mcp-auth/account.d.ts +33 -0
- package/dist/mcp-auth/config.d.ts +6 -0
- package/dist/mcp-auth/constants.d.ts +30 -0
- package/dist/mcp-auth/discovery.d.ts +72 -0
- package/dist/mcp-auth/engine-wiring.d.ts +52 -0
- package/dist/mcp-auth/errors.d.ts +37 -0
- package/dist/mcp-auth/fetch-policy.d.ts +33 -0
- package/dist/mcp-auth/login.d.ts +59 -0
- package/dist/mcp-auth/records.d.ts +73 -0
- package/dist/mcp-auth/refresh.d.ts +34 -0
- package/dist/mcp-auth/revoke.d.ts +15 -0
- package/dist/mcp-auth/session-provider.d.ts +55 -0
- package/dist/mcp-auth/store.d.ts +85 -0
- package/dist/mcp-auth/test-fixture-as.d.ts +72 -0
- package/dist/mcp-auth.d.ts +14 -0
- package/dist/mcp-auth.js +493 -0
- package/dist/mcp-client.d.ts +12 -2
- package/dist/mcp-client.js +15 -2
- package/dist/production-wiring.d.ts +6 -0
- package/dist/provider/host-credentials.d.ts +48 -0
- package/dist/provider/keychain-store.d.ts +29 -1
- package/dist/provider/session-provider.d.ts +1 -1
- package/dist/store/dialect.d.ts +1 -1
- package/dist/testing.js +5 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type BrandProfile } from "@yanlinglabs/winter-agent-sdk";
|
|
2
|
+
import { type HostRequestSender } from "../provider/host-credentials.js";
|
|
3
|
+
import { type McpOAuthClientRecord, type McpOAuthTokenRecord } from "./records.js";
|
|
4
|
+
export interface McpOAuthStore {
|
|
5
|
+
read(account: string): Promise<string | null>;
|
|
6
|
+
write(account: string, value: string): Promise<void>;
|
|
7
|
+
remove(account: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** A process-local store, for tests and for a host that keeps sign-ins only for its own lifetime. */
|
|
10
|
+
export declare function createMemoryMcpOAuthStore(initial?: Record<string, string>): McpOAuthStore & {
|
|
11
|
+
readonly entries: Map<string, string>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The macOS Keychain, one service. `service` is the host's own (its brand's `keychainService`, per
|
|
15
|
+
* profile); absent, the SDK's `DEFAULT_KEYCHAIN_SERVICE`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createKeychainMcpOAuthStore(service?: string): McpOAuthStore;
|
|
18
|
+
/** Reads and validates a token record; `null` when there is no item. A malformed item throws typed. */
|
|
19
|
+
export declare function readTokenRecord(store: McpOAuthStore, account: string): Promise<McpOAuthTokenRecord | null>;
|
|
20
|
+
/** Reads and validates a client record; `null` when there is no item. */
|
|
21
|
+
export declare function readClientRecord(store: McpOAuthStore, account: string): Promise<McpOAuthClientRecord | null>;
|
|
22
|
+
export declare function writeTokenRecord(store: McpOAuthStore, account: string, record: McpOAuthTokenRecord): Promise<void>;
|
|
23
|
+
export declare function writeClientRecord(store: McpOAuthStore, account: string, record: McpOAuthClientRecord): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Reads a token record, answering `null` for a MALFORMED one as well as an absent one -- for the paths
|
|
26
|
+
* where a broken item must behave as "not signed in" (the session's read, a refresh) rather than fail
|
|
27
|
+
* the connection with a codec error. A NEWER record version is still thrown: acting as "not signed in"
|
|
28
|
+
* on it would send the user to sign in again and overwrite what the newer Winter wrote.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readTokenRecordLenient(store: McpOAuthStore, account: string): Promise<McpOAuthTokenRecord | null>;
|
|
31
|
+
/**
|
|
32
|
+
* TEST-ONLY: a JSON file of `{ account: value }`, re-read on every read so a second process's write
|
|
33
|
+
* (a fake host's refresh) is seen. Selected ONLY by `mcpOAuthTestStoreEnvName(brand)` from the original process env (see `resolveSessionMcpOAuthStore`), the same
|
|
34
|
+
* named-test-seam posture as `WINTER_TEST_PROVIDER`: the compiled-binary gate (`verify:mcp-oauth`) must
|
|
35
|
+
* hand a spawned `winter` the fixture's sign-ins without the Keychain, which would prompt for an item a
|
|
36
|
+
* different binary wrote. It writes the fixture's throwaway tokens to a temp file -- which is exactly why
|
|
37
|
+
* nothing but that variable selects it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createTestFileMcpOAuthStore(path: string): McpOAuthStore & {
|
|
40
|
+
readonly testFile: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The env name of the test seam above, BRAND-DERIVED (`<envPrefix>TEST_MCP_OAUTH_STORE_FILE`;
|
|
44
|
+
* `WINTER_TEST_MCP_OAUTH_STORE_FILE` for Winter). Every settings tier's `env` block refuses it
|
|
45
|
+
* (`settings/env-filter.ts`'s `ALL_TIER_REFUSED_ENV`): a trusted project that could set it would move the
|
|
46
|
+
* session's sign-ins into a file it controls.
|
|
47
|
+
*/
|
|
48
|
+
export declare function mcpOAuthTestStoreEnvName(brand: Pick<BrandProfile, "envPrefix">): string;
|
|
49
|
+
/** Winter's spelling of the name, for the gates that set it. */
|
|
50
|
+
export declare const MCP_OAUTH_TEST_STORE_ENV: string;
|
|
51
|
+
/**
|
|
52
|
+
* The store a SESSION reads its sign-ins from: the session's own Keychain service (the brand's, as for
|
|
53
|
+
* every other `{ kind: "keychain" }` credential), or the test seam's file.
|
|
54
|
+
*
|
|
55
|
+
* The seam is honoured only when ALL of these hold, so no configuration path can reach it:
|
|
56
|
+
* - `testStoreFile` came from the ORIGINAL process environment (the caller reads it before any
|
|
57
|
+
* settings tier's `env` is merged -- production-wiring captures it first, and the tiers refuse the
|
|
58
|
+
* name anyway);
|
|
59
|
+
* - the session names a Keychain service explicitly, and it is NEITHER the SDK's default production
|
|
60
|
+
* service (`DEFAULT_KEYCHAIN_SERVICE`, the dist profile's) NOR its `.dev` twin: a gate names its own throwaway
|
|
61
|
+
* service, a production session never can without also changing where its real credentials live.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveSessionMcpOAuthStore(opts: {
|
|
64
|
+
keychainService: string | undefined;
|
|
65
|
+
testStoreFile: string | undefined;
|
|
66
|
+
}): McpOAuthStore;
|
|
67
|
+
/**
|
|
68
|
+
* What a session sees in place of an MCP sign-in's refresh token when its host holds the real one: a
|
|
69
|
+
* non-secret marker that says "a refresh is possible -- ask the host". The session provider only ever
|
|
70
|
+
* tests a record's `refreshToken` for PRESENCE, so this keeps §1.2's startup rules exact (expired WITH a
|
|
71
|
+
* refresh token -> ask the host; WITHOUT -> needs-auth) while no refresh token enters the session.
|
|
72
|
+
*/
|
|
73
|
+
export declare const MCP_OAUTH_HOST_HELD_REFRESH_TOKEN = "host-held";
|
|
74
|
+
/**
|
|
75
|
+
* For the HOST answering `credential_resolve` for an `mcp-oauth:<id>` account: the stored record's JSON
|
|
76
|
+
* with its refresh token replaced by `MCP_OAUTH_HOST_HELD_REFRESH_TOKEN` (and nothing else changed).
|
|
77
|
+
* Validates on the way (a malformed or newer record is thrown typed, like every read).
|
|
78
|
+
*/
|
|
79
|
+
export declare function toSessionMcpTokenRecord(raw: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* A host-brokered session's MCP token store: `read` is one `credential_resolve` for the account; there is
|
|
82
|
+
* no Keychain read in this process at all. `write`/`remove` refuse typed -- the host is the only writer
|
|
83
|
+
* (it refreshes on `mcp_oauth_refresh`, and signs in and out on its own doors).
|
|
84
|
+
*/
|
|
85
|
+
export declare function createHostBrokeredMcpOAuthStore(sender: HostRequestSender): McpOAuthStore;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface FixtureAsOptions {
|
|
2
|
+
/** false: the legacy variant -- no protected-resource metadata and no RFC 8414 document. Default true. */
|
|
3
|
+
metadata?: boolean;
|
|
4
|
+
/** Advertise `registration_endpoint` and accept RFC 7591 registrations. Default true. */
|
|
5
|
+
dcr?: boolean;
|
|
6
|
+
/** Advertise `client_id_metadata_document_supported`. Default false. */
|
|
7
|
+
cimd?: boolean;
|
|
8
|
+
/** Client ID Metadata Documents by their URL (the client_id). */
|
|
9
|
+
cimdDocuments?: Record<string, {
|
|
10
|
+
client_id: string;
|
|
11
|
+
redirect_uris: string[];
|
|
12
|
+
[k: string]: unknown;
|
|
13
|
+
}>;
|
|
14
|
+
/** Pre-registered clients; each redirect URI must match exactly (port included). */
|
|
15
|
+
preregistered?: Array<{
|
|
16
|
+
clientId: string;
|
|
17
|
+
clientSecret?: string;
|
|
18
|
+
redirectUris: string[];
|
|
19
|
+
}>;
|
|
20
|
+
/** Access token lifetime in seconds; `null` issues no `expires_in` at all. Default 3600. */
|
|
21
|
+
accessTokenTtlSec?: number | null;
|
|
22
|
+
/** Issue refresh tokens. Default true. */
|
|
23
|
+
refreshTokens?: boolean;
|
|
24
|
+
/** `scopes_supported` on both documents, and the scope a sign-in gets when it asks for none. Default ["read"]. */
|
|
25
|
+
scopesSupported?: string[];
|
|
26
|
+
/** The scope the `step_up` tool demands (403 insufficient_scope without it). Default "admin". */
|
|
27
|
+
stepUpScope?: string;
|
|
28
|
+
/** Send RFC 9207 `iss` on the redirect and advertise it. Default true. */
|
|
29
|
+
issParameter?: boolean;
|
|
30
|
+
/** Advertise `revocation_endpoint`. Default true. */
|
|
31
|
+
revocation?: boolean;
|
|
32
|
+
/** Delay every `/token` answer by this many ms (a test ends a sign-in DURING its code exchange). */
|
|
33
|
+
tokenDelayMs?: number;
|
|
34
|
+
/** Publish the protected resource as the bare ORIGIN (no path) -- the RFC 8707 indicator form a URL round trip would change. */
|
|
35
|
+
pathlessResource?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface FixtureAs {
|
|
38
|
+
readonly origin: string;
|
|
39
|
+
readonly issuer: string;
|
|
40
|
+
/** The protected MCP endpoint. */
|
|
41
|
+
readonly mcpUrl: string;
|
|
42
|
+
/** Every request, in order: `METHOD /path`. */
|
|
43
|
+
readonly log: string[];
|
|
44
|
+
/** Every POST to `/token`, by grant type, in order. */
|
|
45
|
+
readonly tokenPosts: string[];
|
|
46
|
+
/** The `resource` form parameter of every POST to `/token`, in order (`null` when absent). */
|
|
47
|
+
readonly tokenResources: Array<string | null>;
|
|
48
|
+
/** Every request that reached `/mcp` (answered or 401). */
|
|
49
|
+
readonly mcpRequests: number;
|
|
50
|
+
/** Registered DCR clients, in order. */
|
|
51
|
+
readonly registrations: Array<{
|
|
52
|
+
clientId: string;
|
|
53
|
+
redirectUris: string[];
|
|
54
|
+
}>;
|
|
55
|
+
/** The redirect URIs `/authorize` was asked to use, in order. */
|
|
56
|
+
readonly authorizeRedirects: string[];
|
|
57
|
+
/** Families revoked by refresh-token reuse. */
|
|
58
|
+
readonly reuseRevokedFamilies: string[];
|
|
59
|
+
/** Tokens revoked through `/revoke`. */
|
|
60
|
+
readonly revokedViaEndpoint: string[];
|
|
61
|
+
/** The browser: GET the authorize URL (auto-approved), then follow its 302 to the loopback callback. Returns the callback's status. */
|
|
62
|
+
approve(authUrl: string): Promise<{
|
|
63
|
+
callbackStatus: number;
|
|
64
|
+
location: string;
|
|
65
|
+
}>;
|
|
66
|
+
/** Forgets every DCR registration, as an authorization server that expired its dynamic clients does. */
|
|
67
|
+
forgetRegistrations(): void;
|
|
68
|
+
/** Makes every access token issued so far invalid at `/mcp` (401), as an expiry would. */
|
|
69
|
+
expireAllAccessTokens(): void;
|
|
70
|
+
close(): void;
|
|
71
|
+
}
|
|
72
|
+
export declare function startFixtureAs(opts?: FixtureAsOptions): FixtureAs;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { canonicalMcpServerUrl, clientAccountForTokenAccount, isMcpOAuthTokenAccount, mcpOAuthAccountId, mcpOAuthClientAccount, mcpOAuthClientSecretAccount, mcpOAuthTokenAccount, MCP_OAUTH_CLIENT_ACCOUNT_PREFIX, MCP_OAUTH_TOKEN_ACCOUNT_PREFIX, } from "./mcp-auth/account.js";
|
|
2
|
+
export { McpOAuthError, type McpOAuthErrorCode } from "./mcp-auth/errors.js";
|
|
3
|
+
export { validateMcpOAuthConfig } from "./mcp-auth/config.js";
|
|
4
|
+
export { evaluateMcpAuthUrl } from "./mcp-auth/fetch-policy.js";
|
|
5
|
+
export { decodeMcpOAuthClientRecord, decodeMcpOAuthClientSecretItem, encodeMcpOAuthClientSecretItem, type McpOAuthClientSecretItem, decodeMcpOAuthTokenRecord, encodeMcpOAuthClientRecord, encodeMcpOAuthTokenRecord, sameIssuer, type McpOAuthClientRecord, type McpOAuthTokenRecord, } from "./mcp-auth/records.js";
|
|
6
|
+
export { discoverMcpOAuthIssuer, type DiscoverMcpOAuthIssuerOptions, type McpOAuthIssuerInfo } from "./mcp-auth/discovery.js";
|
|
7
|
+
export { createKeychainMcpOAuthStore, createMemoryMcpOAuthStore, MCP_OAUTH_HOST_HELD_REFRESH_TOKEN, toSessionMcpTokenRecord, type McpOAuthStore } from "./mcp-auth/store.js";
|
|
8
|
+
export { MCP_OAUTH_CALLBACK_PATH, MCP_OAUTH_EXPIRY_SKEW_MS, MCP_OAUTH_LOGIN_TIMEOUT_MS, MCP_OAUTH_SESSION_CACHE_MS, WINTER_MCP_CLIENT_METADATA_URL, } from "./mcp-auth/constants.js";
|
|
9
|
+
export { startMcpOAuthLogin, type McpOAuthLogin, type McpOAuthLoginOutcome, type StartMcpOAuthLoginOptions } from "./mcp-auth/login.js";
|
|
10
|
+
export { refreshMcpOAuthToken, type RefreshMcpOAuthTokenOptions, type RefreshMcpOAuthTokenResult } from "./mcp-auth/refresh.js";
|
|
11
|
+
export { revokeMcpOAuth, type RevokeMcpOAuthOptions } from "./mcp-auth/revoke.js";
|
|
12
|
+
export { CREDENTIAL_RESOLVE_SUBTYPE, type CredentialResolveAnswer, type CredentialResolveRequest, MCP_OAUTH_REFRESH_SUBTYPE, type McpOAuthConfig, type McpOAuthRefreshAnswer, type McpOAuthRefreshRequest, type McpOAuthSecretRef } from "@yanlinglabs/winter-agent-sdk";
|
|
13
|
+
/** The status string `mcp_status` / `system/init.mcp_servers` report for a server that needs sign-in. */
|
|
14
|
+
export declare const MCP_STATUS_NEEDS_AUTH = "needs-auth";
|
package/dist/mcp-auth.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import {
|
|
2
|
+
McpOAuthError2,
|
|
3
|
+
boundedReason,
|
|
4
|
+
MCP_OAUTH_TOKEN_ACCOUNT_PREFIX2,
|
|
5
|
+
MCP_OAUTH_CLIENT_ACCOUNT_PREFIX2,
|
|
6
|
+
canonicalMcpServerUrl2,
|
|
7
|
+
mcpOAuthAccountId2,
|
|
8
|
+
mcpOAuthTokenAccount2,
|
|
9
|
+
mcpOAuthClientAccount2,
|
|
10
|
+
mcpOAuthClientSecretAccount2,
|
|
11
|
+
isMcpOAuthTokenAccount2,
|
|
12
|
+
clientAccountForTokenAccount2,
|
|
13
|
+
WINTER_MCP_CLIENT_METADATA_URL2,
|
|
14
|
+
MCP_OAUTH_CALLBACK_PATH2,
|
|
15
|
+
MCP_OAUTH_EXPIRY_SKEW_MS2,
|
|
16
|
+
MCP_OAUTH_SESSION_CACHE_MS2,
|
|
17
|
+
MCP_OAUTH_LOGIN_TIMEOUT_MS2,
|
|
18
|
+
MCP_OAUTH_STATE_BYTES,
|
|
19
|
+
evaluateMcpAuthUrl2,
|
|
20
|
+
isLoopbackMcpServer,
|
|
21
|
+
createMcpAuthFetch,
|
|
22
|
+
decodeMcpOAuthTokenRecord2,
|
|
23
|
+
decodeMcpOAuthClientRecord2,
|
|
24
|
+
encodeMcpOAuthTokenRecord2,
|
|
25
|
+
encodeMcpOAuthClientRecord2,
|
|
26
|
+
decodeMcpOAuthClientSecretItem2,
|
|
27
|
+
encodeMcpOAuthClientSecretItem2,
|
|
28
|
+
sameIssuer2,
|
|
29
|
+
fetchAuthorizationServerMetadataDocument,
|
|
30
|
+
loadAuthorizationServer,
|
|
31
|
+
discoverMcpOAuthIssuer2,
|
|
32
|
+
createMemoryMcpOAuthStore2,
|
|
33
|
+
createKeychainMcpOAuthStore2,
|
|
34
|
+
readClientRecord,
|
|
35
|
+
writeTokenRecord,
|
|
36
|
+
writeClientRecord,
|
|
37
|
+
readTokenRecordLenient,
|
|
38
|
+
MCP_OAUTH_HOST_HELD_REFRESH_TOKEN2,
|
|
39
|
+
toSessionMcpTokenRecord2,
|
|
40
|
+
refreshMcpOAuthToken2
|
|
41
|
+
} from "./index-z6s3r4xm.js";
|
|
42
|
+
import {
|
|
43
|
+
validateMcpOAuthConfig2
|
|
44
|
+
} from "./index-bgeqc7ax.js";
|
|
45
|
+
// src/mcp-auth/login.ts
|
|
46
|
+
import { auth, computeScopeUnion, exchangeAuthorization, OAuthError, OAuthErrorCode } from "@modelcontextprotocol/client";
|
|
47
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
48
|
+
var activeFlows = new Map;
|
|
49
|
+
var CLIENT_NAME = "Winter";
|
|
50
|
+
function portOf(redirectUri) {
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(redirectUri);
|
|
53
|
+
return url.hostname === "127.0.0.1" && url.port !== "" ? Number(url.port) : undefined;
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function sameState(a, b) {
|
|
59
|
+
if (a === null)
|
|
60
|
+
return false;
|
|
61
|
+
const x = Buffer.from(a);
|
|
62
|
+
const y = Buffer.from(b);
|
|
63
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
64
|
+
}
|
|
65
|
+
function reasonOf(err) {
|
|
66
|
+
if (err instanceof McpOAuthError2)
|
|
67
|
+
return err.code;
|
|
68
|
+
if (err instanceof OAuthError)
|
|
69
|
+
return `oauth_error:${err.code}`;
|
|
70
|
+
return err instanceof Error ? err.name : "failed";
|
|
71
|
+
}
|
|
72
|
+
var LISTENER_HEADERS = { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "referrer-policy": "no-referrer", connection: "close" };
|
|
73
|
+
function page(title, status) {
|
|
74
|
+
const body = `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:-apple-system,sans-serif;padding:3em"><h2>${title}</h2><p>You can close this window.</p></body>`;
|
|
75
|
+
return new Response(body, { status, headers: LISTENER_HEADERS });
|
|
76
|
+
}
|
|
77
|
+
async function readSecret(opts, account) {
|
|
78
|
+
let secret;
|
|
79
|
+
try {
|
|
80
|
+
secret = opts.readClientSecret !== undefined ? await opts.readClientSecret(account) : await opts.store.read(account);
|
|
81
|
+
} catch {
|
|
82
|
+
throw new McpOAuthError2("client_secret_unavailable", `the client secret at Keychain account "${account}" could not be read`);
|
|
83
|
+
}
|
|
84
|
+
if (secret === null || secret === "")
|
|
85
|
+
throw new McpOAuthError2("client_secret_unavailable", `no client secret is stored at Keychain account "${account}"`);
|
|
86
|
+
return secret;
|
|
87
|
+
}
|
|
88
|
+
function originOf(url) {
|
|
89
|
+
try {
|
|
90
|
+
return new URL(url).origin;
|
|
91
|
+
} catch {
|
|
92
|
+
return "an unparseable issuer";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function bindListener(port, handler) {
|
|
96
|
+
return Bun.serve({ hostname: "127.0.0.1", port, fetch: handler });
|
|
97
|
+
}
|
|
98
|
+
async function startMcpOAuthLogin(opts) {
|
|
99
|
+
const serverVerdict = evaluateMcpAuthUrl2(opts.serverUrl, { allowLoopback: true });
|
|
100
|
+
if (!serverVerdict.ok)
|
|
101
|
+
throw new McpOAuthError2("policy_refused", `the MCP server URL is refused: ${serverVerdict.reason}`);
|
|
102
|
+
const account = mcpOAuthTokenAccount2(opts.serverUrl);
|
|
103
|
+
const clientAccount = mcpOAuthClientAccount2(opts.serverUrl);
|
|
104
|
+
const now = opts.now ?? Date.now;
|
|
105
|
+
const oauth = opts.oauth ?? {};
|
|
106
|
+
const clientMetadataUrl = opts.clientMetadataUrl ?? WINTER_MCP_CLIENT_METADATA_URL2;
|
|
107
|
+
const allowLoopback = isLoopbackMcpServer(opts.serverUrl);
|
|
108
|
+
const fetchFn = createMcpAuthFetch({ ...opts.fetch !== undefined ? { fetch: opts.fetch } : {}, allowLoopback });
|
|
109
|
+
let supersededEarly = false;
|
|
110
|
+
let finishLive;
|
|
111
|
+
const supersede = (reason) => {
|
|
112
|
+
if (finishLive !== undefined)
|
|
113
|
+
finishLive({ ok: false, reason });
|
|
114
|
+
else
|
|
115
|
+
supersededEarly = true;
|
|
116
|
+
};
|
|
117
|
+
activeFlows.get(account)?.("login_superseded");
|
|
118
|
+
activeFlows.set(account, supersede);
|
|
119
|
+
const checkpoint = () => {
|
|
120
|
+
if (supersededEarly)
|
|
121
|
+
throw new McpOAuthError2("login_superseded", "a newer sign-in for this server started");
|
|
122
|
+
};
|
|
123
|
+
const release = () => {
|
|
124
|
+
if (activeFlows.get(account) === supersede)
|
|
125
|
+
activeFlows.delete(account);
|
|
126
|
+
};
|
|
127
|
+
let server;
|
|
128
|
+
let onCallback = async () => page("Not ready", 503);
|
|
129
|
+
let codeVerifier;
|
|
130
|
+
let discovery;
|
|
131
|
+
let authUrl;
|
|
132
|
+
let clientDecision;
|
|
133
|
+
let lastClientInfo;
|
|
134
|
+
let forgetExisting = false;
|
|
135
|
+
let redirectUri = "";
|
|
136
|
+
let existingClient = null;
|
|
137
|
+
let requestedScope;
|
|
138
|
+
let recordFor;
|
|
139
|
+
let preSecretItem;
|
|
140
|
+
let preSecret;
|
|
141
|
+
const state = randomBytes(MCP_OAUTH_STATE_BYTES).toString("base64url");
|
|
142
|
+
try {
|
|
143
|
+
preSecretItem = oauth.clientId !== undefined && oauth.clientSecretRef !== undefined ? decodeMcpOAuthClientSecretItem2(await readSecret(opts, mcpOAuthClientSecretAccount2(opts.serverUrl))) : undefined;
|
|
144
|
+
preSecret = preSecretItem?.secret;
|
|
145
|
+
checkpoint();
|
|
146
|
+
existingClient = await readClientRecord(opts.store, clientAccount).catch(() => null);
|
|
147
|
+
checkpoint();
|
|
148
|
+
const handler = (req) => {
|
|
149
|
+
const url = new URL(req.url);
|
|
150
|
+
if (req.method !== "GET" || url.pathname !== MCP_OAUTH_CALLBACK_PATH2)
|
|
151
|
+
return new Response("not found", { status: 404, headers: { connection: "close" } });
|
|
152
|
+
return onCallback(req);
|
|
153
|
+
};
|
|
154
|
+
let reuseRegistration = false;
|
|
155
|
+
if (oauth.callbackPort !== undefined) {
|
|
156
|
+
try {
|
|
157
|
+
server = bindListener(oauth.callbackPort, handler);
|
|
158
|
+
} catch {
|
|
159
|
+
throw new McpOAuthError2("callback_port_unavailable", `the sign-in callback port ${oauth.callbackPort} (oauth.callbackPort) is in use`);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
const persistedPort = existingClient?.registeredVia === "dcr" && oauth.clientId === undefined ? portOf(existingClient.redirectUri) : undefined;
|
|
163
|
+
if (persistedPort !== undefined) {
|
|
164
|
+
try {
|
|
165
|
+
server = bindListener(persistedPort, handler);
|
|
166
|
+
reuseRegistration = true;
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
server ??= bindListener(0, handler);
|
|
170
|
+
}
|
|
171
|
+
redirectUri = `http://127.0.0.1:${server.port}${MCP_OAUTH_CALLBACK_PATH2}`;
|
|
172
|
+
if (oauth.authServerMetadataUrl !== undefined) {
|
|
173
|
+
try {
|
|
174
|
+
const metadata = await fetchAuthorizationServerMetadataDocument(oauth.authServerMetadataUrl, fetchFn);
|
|
175
|
+
discovery = { authorizationServerUrl: metadata.issuer, authorizationServerMetadata: metadata };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof McpOAuthError2 && (err.code === "metadata_issuer_mismatch" || err.code === "policy_refused"))
|
|
178
|
+
throw err;
|
|
179
|
+
throw new McpOAuthError2("login_failed", `the configured authorization server metadata (oauth.authServerMetadataUrl) could not be read: ${boundedReason(err)}`);
|
|
180
|
+
}
|
|
181
|
+
checkpoint();
|
|
182
|
+
}
|
|
183
|
+
requestedScope = computeScopeUnion(oauth.scopes !== undefined && oauth.scopes.length > 0 ? oauth.scopes.join(" ") : undefined, existingClient?.stepUpScope);
|
|
184
|
+
const existing = existingClient;
|
|
185
|
+
const clientRecordFor = (info, issuer) => {
|
|
186
|
+
const via = oauth.clientId !== undefined && info.client_id === oauth.clientId ? "preregistered" : info.client_id === clientMetadataUrl ? "cimd" : "dcr";
|
|
187
|
+
const secret = via === "preregistered" ? preSecret : info.client_secret;
|
|
188
|
+
const stepUpScope = existing !== null && existing.issuer === issuer ? existing.stepUpScope : undefined;
|
|
189
|
+
return {
|
|
190
|
+
v: 1,
|
|
191
|
+
kind: "mcp-oauth-client",
|
|
192
|
+
serverUrl: opts.serverUrl,
|
|
193
|
+
issuer,
|
|
194
|
+
clientId: info.client_id,
|
|
195
|
+
...secret !== undefined ? { clientSecret: secret } : {},
|
|
196
|
+
registeredVia: via,
|
|
197
|
+
redirectUri,
|
|
198
|
+
...discovery?.resourceMetadataUrl !== undefined ? { resourceMetadataUrl: discovery.resourceMetadataUrl } : {},
|
|
199
|
+
...oauth.authServerMetadataUrl !== undefined ? { authorizationServerUrl: oauth.authServerMetadataUrl } : discovery?.authorizationServerUrl !== undefined ? { authorizationServerUrl: String(discovery.authorizationServerUrl) } : {},
|
|
200
|
+
...stepUpScope !== undefined ? { stepUpScope } : {}
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
recordFor = clientRecordFor;
|
|
204
|
+
const clientInformation = (ctx) => {
|
|
205
|
+
if (clientDecision !== undefined)
|
|
206
|
+
return clientDecision;
|
|
207
|
+
if (oauth.clientId !== undefined) {
|
|
208
|
+
if (preSecretItem !== undefined) {
|
|
209
|
+
const bound = preSecretItem.issuer ?? (existing?.registeredVia === "preregistered" ? existing.issuer : undefined);
|
|
210
|
+
if (ctx === undefined || bound !== undefined && !sameIssuer2(bound, ctx.issuer)) {
|
|
211
|
+
throw new McpOAuthError2("client_secret_issuer_mismatch", `the pre-registered client secret for this server belongs to ${bound !== undefined ? originOf(bound) : "another authorization server"}, but this sign-in's authorization server is ${ctx !== undefined ? originOf(ctx.issuer) : "unknown"}; it is not sent`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const stamp = ctx?.issuer;
|
|
215
|
+
return { client_id: oauth.clientId, ...preSecret !== undefined ? { client_secret: preSecret } : {}, ...stamp !== undefined ? { issuer: stamp } : {} };
|
|
216
|
+
}
|
|
217
|
+
if (forgetExisting || existing === null || ctx === undefined || existing.issuer !== ctx.issuer)
|
|
218
|
+
return;
|
|
219
|
+
if (existing.registeredVia === "dcr" && reuseRegistration && existing.redirectUri === redirectUri) {
|
|
220
|
+
return { client_id: existing.clientId, ...existing.clientSecret !== undefined ? { client_secret: existing.clientSecret } : {}, issuer: existing.issuer };
|
|
221
|
+
}
|
|
222
|
+
if (existing.registeredVia === "cimd" && existing.clientId === clientMetadataUrl) {
|
|
223
|
+
return { client_id: existing.clientId, issuer: existing.issuer };
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
};
|
|
227
|
+
const provider = {
|
|
228
|
+
get redirectUrl() {
|
|
229
|
+
return redirectUri;
|
|
230
|
+
},
|
|
231
|
+
...oauth.clientId === undefined ? { clientMetadataUrl } : {},
|
|
232
|
+
get clientMetadata() {
|
|
233
|
+
return {
|
|
234
|
+
client_name: CLIENT_NAME,
|
|
235
|
+
redirect_uris: [redirectUri],
|
|
236
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
237
|
+
response_types: ["code"],
|
|
238
|
+
token_endpoint_auth_method: "none",
|
|
239
|
+
...requestedScope !== undefined ? { scope: requestedScope } : {}
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
state: () => state,
|
|
243
|
+
clientInformation(ctx) {
|
|
244
|
+
const info = clientInformation(ctx);
|
|
245
|
+
if (info !== undefined)
|
|
246
|
+
lastClientInfo = info;
|
|
247
|
+
return info;
|
|
248
|
+
},
|
|
249
|
+
async saveClientInformation(info, ctx) {
|
|
250
|
+
clientDecision = info;
|
|
251
|
+
lastClientInfo = info;
|
|
252
|
+
const issuer = info.issuer ?? ctx?.issuer;
|
|
253
|
+
if (issuer === undefined)
|
|
254
|
+
return;
|
|
255
|
+
await writeClientRecord(opts.store, clientAccount, clientRecordFor(info, issuer));
|
|
256
|
+
},
|
|
257
|
+
tokens: () => {
|
|
258
|
+
return;
|
|
259
|
+
},
|
|
260
|
+
saveTokens: () => {
|
|
261
|
+
throw new McpOAuthError2("login_failed", "the sign-in's first leg does not receive tokens");
|
|
262
|
+
},
|
|
263
|
+
redirectToAuthorization(url) {
|
|
264
|
+
authUrl = url;
|
|
265
|
+
},
|
|
266
|
+
saveCodeVerifier(verifier) {
|
|
267
|
+
codeVerifier = verifier;
|
|
268
|
+
},
|
|
269
|
+
codeVerifier() {
|
|
270
|
+
if (codeVerifier === undefined)
|
|
271
|
+
throw new McpOAuthError2("login_failed", "no code verifier was recorded for this sign-in");
|
|
272
|
+
return codeVerifier;
|
|
273
|
+
},
|
|
274
|
+
saveDiscoveryState(next) {
|
|
275
|
+
discovery = next;
|
|
276
|
+
},
|
|
277
|
+
discoveryState: () => discovery,
|
|
278
|
+
async invalidateCredentials(scope) {
|
|
279
|
+
if (scope === "all" || scope === "client") {
|
|
280
|
+
clientDecision = undefined;
|
|
281
|
+
forgetExisting = true;
|
|
282
|
+
}
|
|
283
|
+
if (scope === "all" || scope === "discovery")
|
|
284
|
+
discovery = oauth.authServerMetadataUrl !== undefined ? discovery : undefined;
|
|
285
|
+
if (scope === "all" || scope === "verifier")
|
|
286
|
+
codeVerifier = undefined;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
let result;
|
|
290
|
+
try {
|
|
291
|
+
result = await auth(provider, { serverUrl: opts.serverUrl, ...requestedScope !== undefined ? { scope: requestedScope } : {}, fetchFn });
|
|
292
|
+
} catch (err) {
|
|
293
|
+
if (err instanceof McpOAuthError2)
|
|
294
|
+
throw err;
|
|
295
|
+
throw new McpOAuthError2("login_failed", `the sign-in could not start: ${boundedReason(err)}`);
|
|
296
|
+
}
|
|
297
|
+
checkpoint();
|
|
298
|
+
if (result !== "REDIRECT" || authUrl === undefined)
|
|
299
|
+
throw new McpOAuthError2("login_failed", "the authorization server did not produce an authorization URL");
|
|
300
|
+
const verdict = evaluateMcpAuthUrl2(authUrl, { allowLoopback });
|
|
301
|
+
if (!verdict.ok)
|
|
302
|
+
throw new McpOAuthError2("policy_refused", `the authorization URL is refused: ${verdict.reason}`);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
server?.stop(true);
|
|
305
|
+
release();
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
308
|
+
const liveServer = server;
|
|
309
|
+
const authorizeUrl = authUrl;
|
|
310
|
+
const metadata = discovery?.authorizationServerMetadata;
|
|
311
|
+
const authorizationServerUrl = discovery?.authorizationServerUrl !== undefined ? String(discovery.authorizationServerUrl) : new URL("/", opts.serverUrl).toString();
|
|
312
|
+
const issuer = metadata?.issuer ?? authorizationServerUrl;
|
|
313
|
+
const issuerOrigin = new URL(issuer).origin;
|
|
314
|
+
const resource = discovery?.resourceMetadata?.resource;
|
|
315
|
+
const client = clientDecision ?? lastClientInfo;
|
|
316
|
+
const usedDcrRegistration = client !== undefined && oauth.clientId === undefined && client.client_id !== clientMetadataUrl;
|
|
317
|
+
let settle;
|
|
318
|
+
const done = new Promise((resolve) => {
|
|
319
|
+
settle = resolve;
|
|
320
|
+
});
|
|
321
|
+
let finished = false;
|
|
322
|
+
let redeeming = false;
|
|
323
|
+
const timeoutMs = opts.timeoutMs ?? MCP_OAUTH_LOGIN_TIMEOUT_MS2;
|
|
324
|
+
const finish = (outcome) => {
|
|
325
|
+
if (finished)
|
|
326
|
+
return;
|
|
327
|
+
finished = true;
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
release();
|
|
330
|
+
liveServer.stop();
|
|
331
|
+
settle(outcome);
|
|
332
|
+
};
|
|
333
|
+
const timer = setTimeout(() => finish({ ok: false, reason: "login_timeout" }), timeoutMs);
|
|
334
|
+
timer.unref?.();
|
|
335
|
+
finishLive = finish;
|
|
336
|
+
onCallback = async (req) => {
|
|
337
|
+
if (finished || redeeming)
|
|
338
|
+
return page("This sign-in has already finished", 410);
|
|
339
|
+
const params = new URL(req.url).searchParams;
|
|
340
|
+
if (!sameState(params.get("state"), state))
|
|
341
|
+
return page("Sign-in failed: the request did not come from this sign-in", 400);
|
|
342
|
+
const error = params.get("error");
|
|
343
|
+
if (error !== null) {
|
|
344
|
+
const code = /^[a-z_]{1,64}$/.test(error) ? error : "unknown";
|
|
345
|
+
finish({ ok: false, reason: `authorization_denied:${code}` });
|
|
346
|
+
return page("Sign-in was not approved", 400);
|
|
347
|
+
}
|
|
348
|
+
const code = params.get("code");
|
|
349
|
+
if (code === null || code === "")
|
|
350
|
+
return page("Sign-in failed: no authorization code", 400);
|
|
351
|
+
if (client === undefined || codeVerifier === undefined) {
|
|
352
|
+
finish({ ok: false, reason: "token_exchange_failed:login_failed" });
|
|
353
|
+
return page("Sign-in failed", 400);
|
|
354
|
+
}
|
|
355
|
+
redeeming = true;
|
|
356
|
+
try {
|
|
357
|
+
const iss = params.get("iss");
|
|
358
|
+
const tokens = await exchangeAuthorization(authorizationServerUrl, {
|
|
359
|
+
...metadata !== undefined ? { metadata } : {},
|
|
360
|
+
clientInformation: client,
|
|
361
|
+
authorizationCode: code,
|
|
362
|
+
...iss !== null ? { iss } : {},
|
|
363
|
+
codeVerifier,
|
|
364
|
+
redirectUri,
|
|
365
|
+
...resource !== undefined ? { resource } : {},
|
|
366
|
+
fetchFn
|
|
367
|
+
});
|
|
368
|
+
if (finished)
|
|
369
|
+
return page("This sign-in has already finished", 410);
|
|
370
|
+
const previous = await readTokenRecordLenient(opts.store, account).catch(() => null);
|
|
371
|
+
if (finished)
|
|
372
|
+
return page("This sign-in has already finished", 410);
|
|
373
|
+
await writeTokenRecord(opts.store, account, {
|
|
374
|
+
v: 1,
|
|
375
|
+
kind: "mcp-oauth",
|
|
376
|
+
serverUrl: opts.serverUrl,
|
|
377
|
+
issuer,
|
|
378
|
+
accessToken: tokens.access_token,
|
|
379
|
+
...tokens.refresh_token !== undefined ? { refreshToken: tokens.refresh_token } : {},
|
|
380
|
+
...typeof tokens.expires_in === "number" ? { expiresAt: now() + tokens.expires_in * 1000 } : {},
|
|
381
|
+
...tokens.scope !== undefined ? { scope: tokens.scope } : {},
|
|
382
|
+
generation: (previous?.generation ?? 0) + 1
|
|
383
|
+
});
|
|
384
|
+
if (recordFor !== undefined)
|
|
385
|
+
await writeClientRecord(opts.store, clientAccount, recordFor(client, issuer));
|
|
386
|
+
if (finished)
|
|
387
|
+
return page("This sign-in has already finished", 410);
|
|
388
|
+
if (preSecretItem !== undefined && preSecretItem.issuer === undefined && opts.readClientSecret === undefined) {
|
|
389
|
+
await opts.store.write(mcpOAuthClientSecretAccount2(opts.serverUrl), encodeMcpOAuthClientSecretItem2(preSecretItem.secret, issuer)).catch(() => {});
|
|
390
|
+
}
|
|
391
|
+
finish({ ok: true });
|
|
392
|
+
return page("Signed in to the MCP server", 200);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (err instanceof OAuthError && (err.code === OAuthErrorCode.InvalidClient || err.code === OAuthErrorCode.UnauthorizedClient) && usedDcrRegistration) {
|
|
395
|
+
await opts.store.remove(clientAccount).catch(() => {});
|
|
396
|
+
}
|
|
397
|
+
finish({ ok: false, reason: `token_exchange_failed:${reasonOf(err)}` });
|
|
398
|
+
return page("Sign-in failed", 400);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
return {
|
|
402
|
+
authUrl: authorizeUrl.toString(),
|
|
403
|
+
issuer,
|
|
404
|
+
issuerOrigin,
|
|
405
|
+
authorizeOrigin: authorizeUrl.origin,
|
|
406
|
+
done,
|
|
407
|
+
cancel: () => finish({ ok: false, reason: "login_cancelled" })
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
// src/mcp-auth/revoke.ts
|
|
411
|
+
async function revokeMcpOAuth(opts) {
|
|
412
|
+
const clientAccount = clientAccountForTokenAccount2(opts.account);
|
|
413
|
+
try {
|
|
414
|
+
await revokeRemotely(opts, clientAccount);
|
|
415
|
+
} catch {
|
|
416
|
+
console.error(`winter: mcp-auth: revocation for ${opts.account} did not complete; the local sign-out proceeds`);
|
|
417
|
+
}
|
|
418
|
+
await opts.store.remove(opts.account);
|
|
419
|
+
if (opts.forgetClient === true)
|
|
420
|
+
await opts.store.remove(clientAccount);
|
|
421
|
+
}
|
|
422
|
+
async function revokeRemotely(opts, clientAccount) {
|
|
423
|
+
const record = await readTokenRecordLenient(opts.store, opts.account);
|
|
424
|
+
if (record === null)
|
|
425
|
+
return;
|
|
426
|
+
const client = await readClientRecord(opts.store, clientAccount).catch(() => null);
|
|
427
|
+
if (client === null || client.issuer !== record.issuer)
|
|
428
|
+
return;
|
|
429
|
+
const fetchFn = createMcpAuthFetch({ ...opts.fetch !== undefined ? { fetch: opts.fetch } : {}, allowLoopback: isLoopbackMcpServer(record.serverUrl) });
|
|
430
|
+
const server = await loadAuthorizationServer({ authorizationServerUrl: client.authorizationServerUrl ?? record.issuer, expectedIssuer: record.issuer, fetchFn });
|
|
431
|
+
const endpoint = server.metadata?.revocation_endpoint;
|
|
432
|
+
if (typeof endpoint !== "string")
|
|
433
|
+
return;
|
|
434
|
+
const tokens = [];
|
|
435
|
+
if (record.refreshToken !== undefined)
|
|
436
|
+
tokens.push([record.refreshToken, "refresh_token"]);
|
|
437
|
+
tokens.push([record.accessToken, "access_token"]);
|
|
438
|
+
for (const [token, hint] of tokens)
|
|
439
|
+
await revokeOne(fetchFn, endpoint, token, hint, client.clientId, client.clientSecret, opts.account);
|
|
440
|
+
}
|
|
441
|
+
async function revokeOne(fetchFn, endpoint, token, hint, clientId, clientSecret, account) {
|
|
442
|
+
const body = new URLSearchParams({ token, token_type_hint: hint });
|
|
443
|
+
const headers = new Headers({ "content-type": "application/x-www-form-urlencoded" });
|
|
444
|
+
if (clientSecret !== undefined)
|
|
445
|
+
headers.set("authorization", `Basic ${btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`)}`);
|
|
446
|
+
else
|
|
447
|
+
body.set("client_id", clientId);
|
|
448
|
+
const response = await fetchFn(endpoint, { method: "POST", headers, body });
|
|
449
|
+
await response.text().catch(() => {});
|
|
450
|
+
if (!response.ok)
|
|
451
|
+
console.error(`winter: mcp-auth: revocation for ${account} (${hint}) answered HTTP ${response.status}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/mcp-auth.ts
|
|
455
|
+
import { CREDENTIAL_RESOLVE_SUBTYPE, MCP_OAUTH_REFRESH_SUBTYPE } from "@yanlinglabs/winter-agent-sdk";
|
|
456
|
+
var MCP_STATUS_NEEDS_AUTH = "needs-auth";
|
|
457
|
+
export {
|
|
458
|
+
CREDENTIAL_RESOLVE_SUBTYPE,
|
|
459
|
+
MCP_OAUTH_CALLBACK_PATH2 as MCP_OAUTH_CALLBACK_PATH,
|
|
460
|
+
MCP_OAUTH_CLIENT_ACCOUNT_PREFIX2 as MCP_OAUTH_CLIENT_ACCOUNT_PREFIX,
|
|
461
|
+
MCP_OAUTH_EXPIRY_SKEW_MS2 as MCP_OAUTH_EXPIRY_SKEW_MS,
|
|
462
|
+
MCP_OAUTH_HOST_HELD_REFRESH_TOKEN2 as MCP_OAUTH_HOST_HELD_REFRESH_TOKEN,
|
|
463
|
+
MCP_OAUTH_LOGIN_TIMEOUT_MS2 as MCP_OAUTH_LOGIN_TIMEOUT_MS,
|
|
464
|
+
MCP_OAUTH_REFRESH_SUBTYPE,
|
|
465
|
+
MCP_OAUTH_SESSION_CACHE_MS2 as MCP_OAUTH_SESSION_CACHE_MS,
|
|
466
|
+
MCP_OAUTH_TOKEN_ACCOUNT_PREFIX2 as MCP_OAUTH_TOKEN_ACCOUNT_PREFIX,
|
|
467
|
+
MCP_STATUS_NEEDS_AUTH,
|
|
468
|
+
McpOAuthError2 as McpOAuthError,
|
|
469
|
+
WINTER_MCP_CLIENT_METADATA_URL2 as WINTER_MCP_CLIENT_METADATA_URL,
|
|
470
|
+
canonicalMcpServerUrl2 as canonicalMcpServerUrl,
|
|
471
|
+
clientAccountForTokenAccount2 as clientAccountForTokenAccount,
|
|
472
|
+
createKeychainMcpOAuthStore2 as createKeychainMcpOAuthStore,
|
|
473
|
+
createMemoryMcpOAuthStore2 as createMemoryMcpOAuthStore,
|
|
474
|
+
decodeMcpOAuthClientRecord2 as decodeMcpOAuthClientRecord,
|
|
475
|
+
decodeMcpOAuthClientSecretItem2 as decodeMcpOAuthClientSecretItem,
|
|
476
|
+
decodeMcpOAuthTokenRecord2 as decodeMcpOAuthTokenRecord,
|
|
477
|
+
discoverMcpOAuthIssuer2 as discoverMcpOAuthIssuer,
|
|
478
|
+
encodeMcpOAuthClientRecord2 as encodeMcpOAuthClientRecord,
|
|
479
|
+
encodeMcpOAuthClientSecretItem2 as encodeMcpOAuthClientSecretItem,
|
|
480
|
+
encodeMcpOAuthTokenRecord2 as encodeMcpOAuthTokenRecord,
|
|
481
|
+
evaluateMcpAuthUrl2 as evaluateMcpAuthUrl,
|
|
482
|
+
isMcpOAuthTokenAccount2 as isMcpOAuthTokenAccount,
|
|
483
|
+
mcpOAuthAccountId2 as mcpOAuthAccountId,
|
|
484
|
+
mcpOAuthClientAccount2 as mcpOAuthClientAccount,
|
|
485
|
+
mcpOAuthClientSecretAccount2 as mcpOAuthClientSecretAccount,
|
|
486
|
+
mcpOAuthTokenAccount2 as mcpOAuthTokenAccount,
|
|
487
|
+
refreshMcpOAuthToken2 as refreshMcpOAuthToken,
|
|
488
|
+
revokeMcpOAuth,
|
|
489
|
+
sameIssuer2 as sameIssuer,
|
|
490
|
+
startMcpOAuthLogin,
|
|
491
|
+
toSessionMcpTokenRecord2 as toSessionMcpTokenRecord,
|
|
492
|
+
validateMcpOAuthConfig2 as validateMcpOAuthConfig
|
|
493
|
+
};
|