poe-code 4.0.22 → 4.0.24
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/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/packages/tiny-http-mcp-server/dist/express-middleware.js +1 -1
- package/packages/tiny-http-mcp-server/dist/http-server.js +4 -1
- package/packages/toolcraft/dist/http-hosted-oauth.compile-check.js +11 -1
- package/packages/toolcraft/dist/http-hosted-oauth.d.ts +10 -1
- package/packages/toolcraft/dist/http-hosted-oauth.js +41 -12
- package/packages/toolcraft/dist/http.js +27 -7
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ export function createProtectedResourceMetadataRouter(options) {
|
|
|
40
40
|
if (path === "/") {
|
|
41
41
|
return [PROTECTED_RESOURCE_METADATA_PATH];
|
|
42
42
|
}
|
|
43
|
-
return [`${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
|
|
43
|
+
return [PROTECTED_RESOURCE_METADATA_PATH, `${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
|
|
44
44
|
})();
|
|
45
45
|
return (req, res, next) => {
|
|
46
46
|
if (req.method !== "GET" || !metadataPaths.includes(req.path)) {
|
|
@@ -19,7 +19,7 @@ function getProtectedResourceMetadataPaths(path) {
|
|
|
19
19
|
if (path === "/") {
|
|
20
20
|
return [PROTECTED_RESOURCE_METADATA_PATH];
|
|
21
21
|
}
|
|
22
|
-
return [`${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
|
|
22
|
+
return [PROTECTED_RESOURCE_METADATA_PATH, `${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
|
|
23
23
|
}
|
|
24
24
|
function formatHostnameForUrl(hostname) {
|
|
25
25
|
if (hostname.includes(":") && !hostname.startsWith("[")) {
|
|
@@ -252,6 +252,9 @@ export function createHttpServer(options) {
|
|
|
252
252
|
};
|
|
253
253
|
};
|
|
254
254
|
httpServer.handleRequest = async (req, res) => {
|
|
255
|
+
if (options.requestHandler !== undefined && (await options.requestHandler(req, res))) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
255
258
|
const { baseUrl } = req;
|
|
256
259
|
const protectedResourcePath = typeof baseUrl === "string" && baseUrl.length > 0 ? baseUrl : "/mcp";
|
|
257
260
|
if (!(await authorizeHttpRequest(req, res, protectedResourcePath))) {
|
|
@@ -14,7 +14,17 @@ const ignoredHostedOAuth = hostedOAuth({
|
|
|
14
14
|
credential: `${ignoredPassword}:${ignoredSignal.aborted}`
|
|
15
15
|
};
|
|
16
16
|
},
|
|
17
|
-
services({ credentials }) {
|
|
17
|
+
services({ credentials, identity }) {
|
|
18
|
+
const ignoredSubject = identity.subject;
|
|
19
|
+
const ignoredClientId = identity.clientId;
|
|
20
|
+
const ignoredScopes = identity.scopes;
|
|
21
|
+
const ignoredResource = identity.resource;
|
|
22
|
+
const ignoredIssuer = identity.issuer;
|
|
23
|
+
void ignoredSubject;
|
|
24
|
+
void ignoredClientId;
|
|
25
|
+
void ignoredScopes;
|
|
26
|
+
void ignoredResource;
|
|
27
|
+
void ignoredIssuer;
|
|
18
28
|
return { skylight: { credential: () => credentials.read() } };
|
|
19
29
|
}
|
|
20
30
|
}
|
|
@@ -31,6 +31,7 @@ export interface HostedOAuthStorage<TCredential = unknown> {
|
|
|
31
31
|
capabilities: HostedOAuthStorageCapabilities;
|
|
32
32
|
signingKey(): Promise<OAuthAuthorizationServerSigningKey>;
|
|
33
33
|
resolveSubject(providerName: string, accountId: string): Promise<string>;
|
|
34
|
+
healthCheck?(): Promise<void>;
|
|
34
35
|
cleanup?(now?: number): Promise<void>;
|
|
35
36
|
}
|
|
36
37
|
export interface HostedOAuthCredentialAccess<TCredential = unknown> {
|
|
@@ -38,6 +39,13 @@ export interface HostedOAuthCredentialAccess<TCredential = unknown> {
|
|
|
38
39
|
update(update: (credential: TCredential) => Promise<TCredential> | TCredential): Promise<TCredential>;
|
|
39
40
|
delete(): Promise<void>;
|
|
40
41
|
}
|
|
42
|
+
export interface HostedOAuthIdentity {
|
|
43
|
+
issuer: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
clientId: string;
|
|
46
|
+
scopes: readonly string[];
|
|
47
|
+
resource: string;
|
|
48
|
+
}
|
|
41
49
|
export interface HostedOAuthProvider<TCredential = unknown, TServices extends object = object> {
|
|
42
50
|
name: string;
|
|
43
51
|
login?: {
|
|
@@ -51,6 +59,7 @@ export interface HostedOAuthProvider<TCredential = unknown, TServices extends ob
|
|
|
51
59
|
}>;
|
|
52
60
|
services(input: {
|
|
53
61
|
credentials: HostedOAuthCredentialAccess<TCredential>;
|
|
62
|
+
identity: HostedOAuthIdentity;
|
|
54
63
|
}): Promise<Partial<TServices>> | Partial<TServices>;
|
|
55
64
|
}
|
|
56
65
|
export interface HostedOAuthInteractionAdapter<TCredential = unknown> {
|
|
@@ -106,7 +115,7 @@ export interface HostedOAuthRuntime<TServices extends object = object> {
|
|
|
106
115
|
mcpPath: string;
|
|
107
116
|
oauth: TinyHttpMcpServerOAuthOptions;
|
|
108
117
|
requestHandler: HttpAdditionalRequestHandler;
|
|
109
|
-
requestServices(
|
|
118
|
+
requestServices(identity: HostedOAuthIdentity): Promise<Partial<TServices>>;
|
|
110
119
|
}
|
|
111
120
|
export declare function createInMemoryHostedOAuthStorage<TCredential = unknown>(options: {
|
|
112
121
|
development: true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHmac, generateKeyPairSync, randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac, generateKeyPairSync, randomBytes } from "node:crypto";
|
|
2
2
|
import { exportJWK } from "jose";
|
|
3
3
|
import { createAuthorizationInteractionSecurity, createInMemoryAuthorizationServerStore, createOAuthAuthorizationServer, verifyAuthorizationInteractionCsrf } from "mcp-oauth-server";
|
|
4
4
|
export function isHostedOAuthConfiguration(value) {
|
|
@@ -12,6 +12,9 @@ function normalizePublicUrl(value) {
|
|
|
12
12
|
if (url.username.length > 0 || url.password.length > 0) {
|
|
13
13
|
throw new Error("hosted OAuth publicUrl must not contain credentials.");
|
|
14
14
|
}
|
|
15
|
+
if (url.pathname === "/" || url.pathname.endsWith("/")) {
|
|
16
|
+
throw new Error("hosted OAuth publicUrl must contain a non-root path without a trailing slash.");
|
|
17
|
+
}
|
|
15
18
|
return url;
|
|
16
19
|
}
|
|
17
20
|
function configurationErrors(config, production) {
|
|
@@ -117,6 +120,14 @@ function renderLogin(providerName, fields, transaction, csrfToken, error) {
|
|
|
117
120
|
.join("");
|
|
118
121
|
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Connect ${escapeHtml(providerName)}</title><style>body{font:16px system-ui;max-width:28rem;margin:10vh auto;padding:1rem;color:#171717}form{display:grid;gap:1rem}label{display:grid;gap:.35rem}input,button{font:inherit;padding:.7rem}button{cursor:pointer}${error === undefined ? "" : ".error{color:#b42318}"}</style></head><body><h1>Connect ${escapeHtml(providerName)}</h1>${error === undefined ? "" : `<p class="error">${escapeHtml(error)}</p>`}<form method="post" action="/oauth/connect"><input type="hidden" name="transaction" value="${escapeHtml(transaction.id)}"><input type="hidden" name="csrf" value="${escapeHtml(csrfToken)}">${controls}<button type="submit">Connect</button></form></body></html>`;
|
|
119
122
|
}
|
|
123
|
+
function loginContentSecurityPolicy(transaction) {
|
|
124
|
+
const callbackOrigin = new URL(transaction.redirectUri).origin;
|
|
125
|
+
return `default-src 'none'; style-src 'unsafe-inline'; form-action 'self' ${callbackOrigin}; base-uri 'none'; frame-ancestors 'none'`;
|
|
126
|
+
}
|
|
127
|
+
function interactionCookieName(transactionId) {
|
|
128
|
+
const suffix = createHash("sha256").update(transactionId).digest("base64url").slice(0, 22);
|
|
129
|
+
return `__Host-mcp_oauth_csrf_${suffix}`;
|
|
130
|
+
}
|
|
120
131
|
function writeWebResponse(response, webResponse) {
|
|
121
132
|
webResponse.headers.forEach((value, name) => response.setHeader(name, value));
|
|
122
133
|
response.statusCode = webResponse.status;
|
|
@@ -172,12 +183,14 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
172
183
|
if (customInteraction !== undefined) {
|
|
173
184
|
return customInteraction.start({ request, transaction });
|
|
174
185
|
}
|
|
175
|
-
const security = createAuthorizationInteractionSecurity(
|
|
186
|
+
const security = createAuthorizationInteractionSecurity({
|
|
187
|
+
cookieName: interactionCookieName(transaction.id)
|
|
188
|
+
});
|
|
176
189
|
return new Response(renderLogin(displayName, fields, transaction, security.csrfToken), {
|
|
177
190
|
status: 200,
|
|
178
191
|
headers: {
|
|
179
192
|
"content-type": "text/html; charset=utf-8",
|
|
180
|
-
"content-security-policy":
|
|
193
|
+
"content-security-policy": loginContentSecurityPolicy(transaction),
|
|
181
194
|
"cache-control": "no-store",
|
|
182
195
|
"referrer-policy": "no-referrer",
|
|
183
196
|
"x-content-type-options": "nosniff",
|
|
@@ -191,6 +204,7 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
191
204
|
issuer: prepared.issuer.href,
|
|
192
205
|
resources: [prepared.publicUrl.href],
|
|
193
206
|
scopesSupported: prepared.scopes,
|
|
207
|
+
defaultScopes: prepared.scopes,
|
|
194
208
|
signingKey: await config.storage.signingKey(),
|
|
195
209
|
additionalPublicJwks: config.advanced?.additionalPublicJwks,
|
|
196
210
|
store: config.storage.authorizationServer,
|
|
@@ -203,8 +217,21 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
203
217
|
const requestHandler = async (request, response) => {
|
|
204
218
|
const url = new URL(request.url ?? "/", prepared.issuer);
|
|
205
219
|
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
try {
|
|
221
|
+
await config.storage.healthCheck?.();
|
|
222
|
+
response.writeHead(200, {
|
|
223
|
+
"content-type": "application/json",
|
|
224
|
+
"cache-control": "no-store"
|
|
225
|
+
});
|
|
226
|
+
response.end('{"ok":true}');
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
response.writeHead(503, {
|
|
230
|
+
"content-type": "application/json",
|
|
231
|
+
"cache-control": "no-store"
|
|
232
|
+
});
|
|
233
|
+
response.end('{"ok":false}');
|
|
234
|
+
}
|
|
208
235
|
return true;
|
|
209
236
|
}
|
|
210
237
|
if (customInteraction?.paths.includes(url.pathname) === true) {
|
|
@@ -213,11 +240,11 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
213
240
|
request: toWebRequest(request, prepared.issuer, body),
|
|
214
241
|
async complete({ transactionId, accountId, credential }) {
|
|
215
242
|
const subject = await config.storage.resolveSubject(config.provider.name, accountId);
|
|
243
|
+
await config.storage.credentials.set(subject, credential);
|
|
216
244
|
const completed = await authorizationServer.completeAuthorization({
|
|
217
245
|
transactionId,
|
|
218
246
|
subject
|
|
219
247
|
});
|
|
220
|
-
await config.storage.credentials.set(subject, credential);
|
|
221
248
|
await config.storage.interactions.delete(transactionId);
|
|
222
249
|
return new Response(null, {
|
|
223
250
|
status: 303,
|
|
@@ -241,7 +268,8 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
241
268
|
if (transaction === undefined ||
|
|
242
269
|
!verifyAuthorizationInteractionCsrf({
|
|
243
270
|
cookieHeader: request.headers.cookie ?? null,
|
|
244
|
-
submittedToken: csrf
|
|
271
|
+
submittedToken: csrf,
|
|
272
|
+
cookieName: interactionCookieName(transactionId)
|
|
245
273
|
}) ||
|
|
246
274
|
transaction.expiresAt <= Date.now()) {
|
|
247
275
|
response.writeHead(400, {
|
|
@@ -266,11 +294,11 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
266
294
|
if (connected.accountId.trim().length === 0)
|
|
267
295
|
throw new Error("Provider returned an empty accountId.");
|
|
268
296
|
const subject = await config.storage.resolveSubject(config.provider.name, connected.accountId);
|
|
297
|
+
await config.storage.credentials.set(subject, connected.credential);
|
|
269
298
|
const completed = await authorizationServer.completeAuthorization({
|
|
270
299
|
transactionId,
|
|
271
300
|
subject
|
|
272
301
|
});
|
|
273
|
-
await config.storage.credentials.set(subject, connected.credential);
|
|
274
302
|
await config.storage.interactions.delete(transactionId);
|
|
275
303
|
response.writeHead(303, {
|
|
276
304
|
location: completed.redirectUrl.href,
|
|
@@ -286,7 +314,7 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
286
314
|
: "Sign-in failed. Please try again.";
|
|
287
315
|
response.writeHead(400, {
|
|
288
316
|
"content-type": "text/html; charset=utf-8",
|
|
289
|
-
"content-security-policy":
|
|
317
|
+
"content-security-policy": loginContentSecurityPolicy(transaction),
|
|
290
318
|
"cache-control": "no-store",
|
|
291
319
|
"referrer-policy": "no-referrer"
|
|
292
320
|
});
|
|
@@ -332,9 +360,10 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
332
360
|
}
|
|
333
361
|
},
|
|
334
362
|
requestHandler,
|
|
335
|
-
async requestServices(
|
|
336
|
-
|
|
337
|
-
|
|
363
|
+
async requestServices(identity) {
|
|
364
|
+
const credentials = credentialsFor(config.storage, identity.subject);
|
|
365
|
+
await credentials.read();
|
|
366
|
+
return config.provider.services({ credentials, identity });
|
|
338
367
|
}
|
|
339
368
|
};
|
|
340
369
|
}
|
|
@@ -91,12 +91,19 @@ export async function createHTTPMCPServer(roots, options) {
|
|
|
91
91
|
sessionIdGenerator: undefined,
|
|
92
92
|
enableJsonResponse: true,
|
|
93
93
|
requestServices: async (context) => {
|
|
94
|
-
const
|
|
95
|
-
|
|
94
|
+
const auth = context.auth;
|
|
95
|
+
const subject = auth?.subject;
|
|
96
|
+
if (auth === undefined || subject === undefined)
|
|
96
97
|
throw new Error("Hosted OAuth request is missing a verified subject.");
|
|
97
98
|
return {
|
|
98
99
|
...(applicationServices === undefined ? {} : await applicationServices(context)),
|
|
99
|
-
...(await runtime.requestServices(
|
|
100
|
+
...(await runtime.requestServices({
|
|
101
|
+
issuer: auth.issuer,
|
|
102
|
+
subject,
|
|
103
|
+
clientId: auth.clientId,
|
|
104
|
+
scopes: auth.scopes,
|
|
105
|
+
resource: auth.resource.href
|
|
106
|
+
}))
|
|
100
107
|
};
|
|
101
108
|
},
|
|
102
109
|
requestHandler: runtime.requestHandler
|
|
@@ -118,10 +125,23 @@ export async function createHTTPMCPServer(roots, options) {
|
|
|
118
125
|
}
|
|
119
126
|
if (hostedMcpPath !== undefined) {
|
|
120
127
|
const listenHttp = server.listenHttp.bind(server);
|
|
121
|
-
server.listenHttp = (listenOptions = {}) =>
|
|
122
|
-
path
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
server.listenHttp = async (listenOptions = {}) => {
|
|
129
|
+
if (listenOptions.path !== undefined) {
|
|
130
|
+
const withLeadingSlash = listenOptions.path.startsWith("/")
|
|
131
|
+
? listenOptions.path
|
|
132
|
+
: `/${listenOptions.path}`;
|
|
133
|
+
const requestedPath = withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/")
|
|
134
|
+
? withLeadingSlash.slice(0, -1)
|
|
135
|
+
: withLeadingSlash;
|
|
136
|
+
if (requestedPath !== hostedMcpPath) {
|
|
137
|
+
throw new Error(`Hosted OAuth MCP path ${JSON.stringify(requestedPath)} conflicts with publicUrl path ${JSON.stringify(hostedMcpPath)}.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return listenHttp({
|
|
141
|
+
...listenOptions,
|
|
142
|
+
path: hostedMcpPath
|
|
143
|
+
});
|
|
144
|
+
};
|
|
125
145
|
}
|
|
126
146
|
return server;
|
|
127
147
|
}
|