poe-code 4.0.20 → 4.0.22

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.
@@ -0,0 +1,422 @@
1
+ import { createHmac, generateKeyPairSync, randomBytes } from "node:crypto";
2
+ import { exportJWK } from "jose";
3
+ import { createAuthorizationInteractionSecurity, createInMemoryAuthorizationServerStore, createOAuthAuthorizationServer, verifyAuthorizationInteractionCsrf } from "mcp-oauth-server";
4
+ export function isHostedOAuthConfiguration(value) {
5
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "hosted";
6
+ }
7
+ function normalizePublicUrl(value) {
8
+ const url = new URL(value);
9
+ if (url.hash.length > 0 || url.search.length > 0) {
10
+ throw new Error("hosted OAuth publicUrl must not contain a query or fragment.");
11
+ }
12
+ if (url.username.length > 0 || url.password.length > 0) {
13
+ throw new Error("hosted OAuth publicUrl must not contain credentials.");
14
+ }
15
+ return url;
16
+ }
17
+ function configurationErrors(config, production) {
18
+ const errors = [];
19
+ const scopes = config.advanced?.scopes ?? ["mcp", "offline_access"];
20
+ if (!scopes.includes("mcp"))
21
+ errors.push("scopes containing required mcp scope");
22
+ if (scopes.some((scope) => scope.length === 0 || scope.trim() !== scope || scope.includes(" "))) {
23
+ errors.push("valid space-free scope names");
24
+ }
25
+ if (production) {
26
+ if (normalizePublicUrl(config.publicUrl).protocol !== "https:")
27
+ errors.push("HTTPS publicUrl");
28
+ if (!config.storage.capabilities.durable)
29
+ errors.push("durable storage");
30
+ if (!config.storage.capabilities.encryptedCredentials)
31
+ errors.push("encrypted credentials");
32
+ if (!config.storage.capabilities.stableKeys)
33
+ errors.push("stable signing and subject keys");
34
+ }
35
+ return errors;
36
+ }
37
+ export function hostedOAuth(options) {
38
+ if (options.provider.name.trim().length === 0)
39
+ throw new Error("provider.name is required.");
40
+ if (options.advanced?.interaction === undefined &&
41
+ (options.provider.login === undefined || options.provider.connect === undefined)) {
42
+ throw new Error("provider.login and provider.connect are required without an advanced interaction.");
43
+ }
44
+ if (options.provider.login !== undefined && options.provider.login.fields.length === 0) {
45
+ throw new Error("provider.login.fields must contain at least one field.");
46
+ }
47
+ const fieldNames = (options.provider.login?.fields ?? []).map((field) => typeof field === "string" ? field : field.name);
48
+ if (new Set(fieldNames).size !== fieldNames.length ||
49
+ fieldNames.some((name) => name.length === 0 || name === "signal" || name === "csrf" || name === "transaction")) {
50
+ throw new Error("provider.login.fields must have unique, non-reserved names.");
51
+ }
52
+ if (options.advanced?.interaction !== undefined &&
53
+ (options.advanced.interaction.paths.length === 0 ||
54
+ new Set(options.advanced.interaction.paths).size !==
55
+ options.advanced.interaction.paths.length ||
56
+ options.advanced.interaction.paths.some((path) => !path.startsWith("/") ||
57
+ [
58
+ "/healthz",
59
+ "/oauth/connect",
60
+ "/authorize",
61
+ "/register",
62
+ "/token",
63
+ "/revoke",
64
+ "/.well-known/oauth-authorization-server",
65
+ "/.well-known/jwks.json",
66
+ normalizePublicUrl(options.publicUrl).pathname
67
+ ].includes(path)))) {
68
+ throw new Error("advanced.interaction.paths must contain unique, non-reserved absolute paths.");
69
+ }
70
+ normalizePublicUrl(options.publicUrl);
71
+ return {
72
+ ...options,
73
+ kind: "hosted",
74
+ async prepare({ production = process.env.NODE_ENV === "production" } = {}) {
75
+ const errors = configurationErrors(this, production);
76
+ if (errors.length > 0) {
77
+ throw new Error(`Hosted OAuth configuration requires: ${errors.join(", ")}.`);
78
+ }
79
+ const publicUrl = normalizePublicUrl(this.publicUrl);
80
+ return {
81
+ publicUrl,
82
+ issuer: new URL(publicUrl.origin),
83
+ scopes: this.advanced?.scopes ?? ["mcp", "offline_access"]
84
+ };
85
+ }
86
+ };
87
+ }
88
+ export class HostedOAuthLoginError extends Error {
89
+ constructor(message) {
90
+ super(message);
91
+ this.name = "HostedOAuthLoginError";
92
+ }
93
+ }
94
+ function escapeHtml(value) {
95
+ return value
96
+ .replaceAll("&", "&")
97
+ .replaceAll("<", "&lt;")
98
+ .replaceAll(">", "&gt;")
99
+ .replaceAll('"', "&quot;")
100
+ .replaceAll("'", "&#39;");
101
+ }
102
+ function loginField(field) {
103
+ if (typeof field !== "string")
104
+ return field;
105
+ return {
106
+ name: field,
107
+ label: field === "apiKey" ? "API key" : `${field[0]?.toUpperCase() ?? ""}${field.slice(1)}`,
108
+ type: field === "password" || field === "apiKey" ? "password" : field === "email" ? "email" : "text"
109
+ };
110
+ }
111
+ function renderLogin(providerName, fields, transaction, csrfToken, error) {
112
+ const controls = fields
113
+ .map((field) => {
114
+ const name = escapeHtml(field.name);
115
+ return `<label>${escapeHtml(field.label ?? field.name)}<input name="${name}" type="${field.type ?? "text"}" required autocomplete="${field.type === "password" ? "current-password" : "off"}"></label>`;
116
+ })
117
+ .join("");
118
+ 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
+ }
120
+ function writeWebResponse(response, webResponse) {
121
+ webResponse.headers.forEach((value, name) => response.setHeader(name, value));
122
+ response.statusCode = webResponse.status;
123
+ return webResponse.arrayBuffer().then((body) => {
124
+ response.end(Buffer.from(body));
125
+ });
126
+ }
127
+ async function readBody(request, maxBytes = 65_536) {
128
+ const chunks = [];
129
+ let size = 0;
130
+ for await (const chunk of request) {
131
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
132
+ size += buffer.length;
133
+ if (size > maxBytes)
134
+ throw new Error("Request body is too large.");
135
+ chunks.push(buffer);
136
+ }
137
+ return Buffer.concat(chunks);
138
+ }
139
+ function toWebRequest(request, issuer, body) {
140
+ const headers = new Headers();
141
+ for (const [name, value] of Object.entries(request.headers)) {
142
+ if (value !== undefined)
143
+ headers.set(name, Array.isArray(value) ? value.join(", ") : value);
144
+ }
145
+ return new Request(new URL(request.url ?? "/", issuer), {
146
+ method: request.method,
147
+ headers,
148
+ ...(body !== undefined && body.length > 0 ? { body: body.toString("utf8") } : {})
149
+ });
150
+ }
151
+ function credentialsFor(storage, subject) {
152
+ return {
153
+ async read() {
154
+ const credential = await storage.credentials.get(subject);
155
+ if (credential === undefined)
156
+ throw new Error("Provider credential is missing; reconnect required.");
157
+ return credential;
158
+ },
159
+ update: (update) => storage.credentials.update(subject, update),
160
+ delete: () => storage.credentials.delete(subject)
161
+ };
162
+ }
163
+ /** @internal */
164
+ export async function prepareHostedOAuthRuntime(config) {
165
+ const prepared = await config.prepare();
166
+ const fields = (config.provider.login?.fields ?? []).map(loginField);
167
+ const customInteraction = config.advanced?.interaction;
168
+ const displayName = config.advanced?.branding?.title ?? config.provider.name;
169
+ const interaction = {
170
+ async start({ request, transaction }) {
171
+ await config.storage.interactions.set(transaction);
172
+ if (customInteraction !== undefined) {
173
+ return customInteraction.start({ request, transaction });
174
+ }
175
+ const security = createAuthorizationInteractionSecurity();
176
+ return new Response(renderLogin(displayName, fields, transaction, security.csrfToken), {
177
+ status: 200,
178
+ headers: {
179
+ "content-type": "text/html; charset=utf-8",
180
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
181
+ "cache-control": "no-store",
182
+ "referrer-policy": "no-referrer",
183
+ "x-content-type-options": "nosniff",
184
+ "set-cookie": security.setCookie
185
+ }
186
+ });
187
+ }
188
+ };
189
+ await config.storage.cleanup?.();
190
+ const authorizationServer = createOAuthAuthorizationServer({
191
+ issuer: prepared.issuer.href,
192
+ resources: [prepared.publicUrl.href],
193
+ scopesSupported: prepared.scopes,
194
+ signingKey: await config.storage.signingKey(),
195
+ additionalPublicJwks: config.advanced?.additionalPublicJwks,
196
+ store: config.storage.authorizationServer,
197
+ interaction,
198
+ accessTokenTtlSeconds: config.advanced?.accessTokenTtlSeconds,
199
+ authorizationCodeTtlSeconds: config.advanced?.authorizationCodeTtlSeconds,
200
+ authorizationTransactionTtlSeconds: config.advanced?.authorizationTransactionTtlSeconds,
201
+ refreshTokenTtlSeconds: config.advanced?.refreshTokenTtlSeconds
202
+ });
203
+ const requestHandler = async (request, response) => {
204
+ const url = new URL(request.url ?? "/", prepared.issuer);
205
+ if (request.method === "GET" && url.pathname === "/healthz") {
206
+ response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
207
+ response.end('{"ok":true}');
208
+ return true;
209
+ }
210
+ if (customInteraction?.paths.includes(url.pathname) === true) {
211
+ const body = request.method === "GET" || request.method === "HEAD" ? undefined : await readBody(request);
212
+ const handled = await customInteraction.handle({
213
+ request: toWebRequest(request, prepared.issuer, body),
214
+ async complete({ transactionId, accountId, credential }) {
215
+ const subject = await config.storage.resolveSubject(config.provider.name, accountId);
216
+ const completed = await authorizationServer.completeAuthorization({
217
+ transactionId,
218
+ subject
219
+ });
220
+ await config.storage.credentials.set(subject, credential);
221
+ await config.storage.interactions.delete(transactionId);
222
+ return new Response(null, {
223
+ status: 303,
224
+ headers: {
225
+ location: completed.redirectUrl.href,
226
+ "cache-control": "no-store",
227
+ "content-security-policy": "default-src 'none'",
228
+ "referrer-policy": "no-referrer"
229
+ }
230
+ });
231
+ }
232
+ });
233
+ await writeWebResponse(response, handled);
234
+ return true;
235
+ }
236
+ if (request.method === "POST" && url.pathname === "/oauth/connect") {
237
+ const body = new URLSearchParams((await readBody(request)).toString("utf8"));
238
+ const transactionId = body.get("transaction") ?? "";
239
+ const transaction = await config.storage.interactions.get(transactionId);
240
+ const csrf = body.get("csrf") ?? "";
241
+ if (transaction === undefined ||
242
+ !verifyAuthorizationInteractionCsrf({
243
+ cookieHeader: request.headers.cookie ?? null,
244
+ submittedToken: csrf
245
+ }) ||
246
+ transaction.expiresAt <= Date.now()) {
247
+ response.writeHead(400, {
248
+ "content-type": "text/plain; charset=utf-8",
249
+ "cache-control": "no-store"
250
+ });
251
+ response.end("This connection has expired or was already used. Restart the connection.");
252
+ return true;
253
+ }
254
+ const controller = new AbortController();
255
+ request.once("aborted", () => controller.abort());
256
+ const values = Object.assign(Object.create(null), {
257
+ signal: controller.signal
258
+ });
259
+ for (const field of fields)
260
+ values[field.name] = body.get(field.name) ?? "";
261
+ try {
262
+ const connect = config.provider.connect;
263
+ if (connect === undefined)
264
+ throw new Error("Provider form connection is not configured.");
265
+ const connected = await connect(values);
266
+ if (connected.accountId.trim().length === 0)
267
+ throw new Error("Provider returned an empty accountId.");
268
+ const subject = await config.storage.resolveSubject(config.provider.name, connected.accountId);
269
+ const completed = await authorizationServer.completeAuthorization({
270
+ transactionId,
271
+ subject
272
+ });
273
+ await config.storage.credentials.set(subject, connected.credential);
274
+ await config.storage.interactions.delete(transactionId);
275
+ response.writeHead(303, {
276
+ location: completed.redirectUrl.href,
277
+ "cache-control": "no-store",
278
+ "content-security-policy": "default-src 'none'",
279
+ "referrer-policy": "no-referrer"
280
+ });
281
+ response.end();
282
+ }
283
+ catch (error) {
284
+ const safeError = error instanceof HostedOAuthLoginError
285
+ ? error.message
286
+ : "Sign-in failed. Please try again.";
287
+ response.writeHead(400, {
288
+ "content-type": "text/html; charset=utf-8",
289
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
290
+ "cache-control": "no-store",
291
+ "referrer-policy": "no-referrer"
292
+ });
293
+ response.end(renderLogin(displayName, fields, transaction, csrf, safeError));
294
+ }
295
+ return true;
296
+ }
297
+ const oauthPaths = new Set([
298
+ "/.well-known/oauth-authorization-server",
299
+ "/.well-known/jwks.json",
300
+ "/authorize",
301
+ "/register",
302
+ "/token",
303
+ "/revoke"
304
+ ]);
305
+ if (!oauthPaths.has(url.pathname))
306
+ return false;
307
+ const body = request.method === "POST" ? await readBody(request) : undefined;
308
+ await writeWebResponse(response, await authorizationServer.handle(toWebRequest(request, prepared.issuer, body)));
309
+ return true;
310
+ };
311
+ return {
312
+ mcpPath: prepared.publicUrl.pathname,
313
+ oauth: {
314
+ resource: prepared.publicUrl.href,
315
+ authorizationServers: [authorizationServer.issuer],
316
+ requiredScopes: ["mcp"],
317
+ scopesSupported: [...prepared.scopes],
318
+ verifier: {
319
+ async verify(input) {
320
+ const verified = await authorizationServer.verifyAccessToken(input.token, prepared.publicUrl.href);
321
+ return {
322
+ token: input.token,
323
+ issuer: authorizationServer.issuer,
324
+ audience: [verified.resource],
325
+ scopes: [...verified.scopes],
326
+ expiresAt: verified.expiresAt,
327
+ claims: { sub: verified.subject, client_id: verified.clientId, jti: verified.tokenId },
328
+ subject: verified.subject,
329
+ clientId: verified.clientId
330
+ };
331
+ }
332
+ }
333
+ },
334
+ requestHandler,
335
+ async requestServices(subject) {
336
+ await credentialsFor(config.storage, subject).read();
337
+ return config.provider.services({ credentials: credentialsFor(config.storage, subject) });
338
+ }
339
+ };
340
+ }
341
+ export function createInMemoryHostedOAuthStorage(options) {
342
+ if (options.development !== true) {
343
+ throw new Error("In-memory hosted OAuth storage requires explicit development mode.");
344
+ }
345
+ const credentials = new Map();
346
+ const interactions = new Map();
347
+ const updates = new Map();
348
+ const subjectSalt = randomBytes(32);
349
+ let signingKeyPromise;
350
+ return {
351
+ authorizationServer: createInMemoryAuthorizationServerStore(),
352
+ interactions: {
353
+ async set(transaction) {
354
+ interactions.set(transaction.id, structuredClone(transaction));
355
+ },
356
+ async get(transactionId) {
357
+ const transaction = interactions.get(transactionId);
358
+ return transaction === undefined ? undefined : structuredClone(transaction);
359
+ },
360
+ async delete(transactionId) {
361
+ interactions.delete(transactionId);
362
+ }
363
+ },
364
+ capabilities: {
365
+ durable: false,
366
+ encryptedCredentials: false,
367
+ stableKeys: false,
368
+ shared: false
369
+ },
370
+ credentials: {
371
+ async get(subject) {
372
+ return credentials.get(subject);
373
+ },
374
+ async set(subject, credential) {
375
+ credentials.set(subject, credential);
376
+ },
377
+ async delete(subject) {
378
+ credentials.delete(subject);
379
+ },
380
+ async update(subject, update) {
381
+ const previous = updates.get(subject) ?? Promise.resolve();
382
+ const next = previous.then(async () => {
383
+ const current = credentials.get(subject);
384
+ if (current === undefined)
385
+ throw new Error("Provider credential is missing; reconnect required.");
386
+ const replacement = await update(current);
387
+ credentials.set(subject, replacement);
388
+ return replacement;
389
+ });
390
+ updates.set(subject, next);
391
+ try {
392
+ return await next;
393
+ }
394
+ finally {
395
+ if (updates.get(subject) === next)
396
+ updates.delete(subject);
397
+ }
398
+ }
399
+ },
400
+ async signingKey() {
401
+ signingKeyPromise ??= (async () => {
402
+ const { privateKey, publicKey } = generateKeyPairSync("ec", {
403
+ namedCurve: "prime256v1"
404
+ });
405
+ return {
406
+ algorithm: "ES256",
407
+ keyId: randomBytes(16).toString("base64url"),
408
+ privateKey,
409
+ publicJwk: await exportJWK(publicKey)
410
+ };
411
+ })();
412
+ return await signingKeyPromise;
413
+ },
414
+ async resolveSubject(providerName, accountId) {
415
+ return createHmac("sha256", subjectSalt)
416
+ .update(providerName)
417
+ .update("\0")
418
+ .update(accountId)
419
+ .digest("base64url");
420
+ }
421
+ };
422
+ }
@@ -1,18 +1,20 @@
1
1
  import "./node-require-shim.js";
2
- import { type HttpListenOptions, type HttpServer, type HttpServerHandle, type HttpToolContext, type HttpTransportOptions } from "../../tiny-http-mcp-server/dist/server.js";
2
+ import { type HttpListenOptions, type HttpServer, type HttpServerHandle, type HttpToolContext, type HttpTransportOptions, type TinyHttpMcpServerOAuthOptions } from "../../tiny-http-mcp-server/dist/server.js";
3
3
  import type { Group } from "./index.js";
4
4
  import type { OAuthAuthorizationServer } from "../../mcp-oauth-server/dist/index.js";
5
5
  import { type RunMCPOptions } from "./mcp.js";
6
+ import { type HostedOAuthConfiguration } from "./http-hosted-oauth.js";
6
7
  export type ToolcraftHTTPContext = HttpToolContext;
7
8
  export type ToolcraftHTTPServer = HttpServer;
8
9
  export type ToolcraftHTTPServerHandle = HttpServerHandle;
9
- type HTTPTransportControls = Omit<HttpTransportOptions, "name" | "version" | "validateToolArguments">;
10
+ type HTTPTransportControls = Omit<HttpTransportOptions, "name" | "version" | "validateToolArguments" | "oauth" | "requestHandler">;
10
11
  type HTTPListenControls = Omit<HttpListenOptions, "port" | "hostname" | "path">;
11
12
  export interface RunHTTPMCPOptions<TServices extends object = Record<string, unknown>> extends RunMCPOptions<TServices>, HTTPTransportControls, HTTPListenControls {
12
13
  hostname?: string;
13
14
  port?: number;
14
15
  path?: string;
15
16
  requestServices?(context: ToolcraftHTTPContext): Partial<TServices> | Promise<Partial<TServices>>;
17
+ oauth?: TinyHttpMcpServerOAuthOptions | HostedOAuthConfiguration<unknown, TServices>;
16
18
  }
17
19
  export interface HTTPMCPAuthorizationOptions {
18
20
  authorizationServer: Pick<OAuthAuthorizationServer, "issuer" | "verifyAccessToken">;
@@ -2,6 +2,7 @@ import "./node-require-shim.js";
2
2
  import { createHttpServer } from "tiny-http-mcp-server/server";
3
3
  import { createMCPServerForTransport } from "./mcp.js";
4
4
  import { enableSourceMaps } from "./stack-trim.js";
5
+ import { isHostedOAuthConfiguration, prepareHostedOAuthRuntime } from "./http-hosted-oauth.js";
5
6
  function toVerifiedAccessToken(token, issuer, verified) {
6
7
  return {
7
8
  token,
@@ -62,7 +63,8 @@ function createTransportOptions(options, serverOptions) {
62
63
  requestIdGenerator: options.requestIdGenerator,
63
64
  observability: options.observability,
64
65
  trustedProxy: options.trustedProxy,
65
- oauth: options.oauth
66
+ oauth: options.oauth,
67
+ requestHandler: options.requestHandler
66
68
  };
67
69
  }
68
70
  function createListenOptions(options) {
@@ -77,20 +79,50 @@ function createListenOptions(options) {
77
79
  };
78
80
  }
79
81
  export async function createHTTPMCPServer(roots, options) {
82
+ let resolvedOptions = options;
83
+ let hostedMcpPath;
84
+ if (isHostedOAuthConfiguration(options.oauth)) {
85
+ const runtime = await prepareHostedOAuthRuntime(options.oauth);
86
+ const applicationServices = options.requestServices;
87
+ hostedMcpPath = runtime.mcpPath;
88
+ resolvedOptions = {
89
+ ...options,
90
+ oauth: runtime.oauth,
91
+ sessionIdGenerator: undefined,
92
+ enableJsonResponse: true,
93
+ requestServices: async (context) => {
94
+ const subject = context.auth?.subject;
95
+ if (subject === undefined)
96
+ throw new Error("Hosted OAuth request is missing a verified subject.");
97
+ return {
98
+ ...(applicationServices === undefined ? {} : await applicationServices(context)),
99
+ ...(await runtime.requestServices(subject))
100
+ };
101
+ },
102
+ requestHandler: runtime.requestHandler
103
+ };
104
+ }
80
105
  let server;
81
- await createMCPServerForTransport(roots, options, {
106
+ await createMCPServerForTransport(roots, resolvedOptions, {
82
107
  createServer(serverOptions) {
83
- server = createHttpServer(createTransportOptions(options, serverOptions));
108
+ server = createHttpServer(createTransportOptions(resolvedOptions, serverOptions));
84
109
  return server;
85
110
  },
86
111
  getRequestContext() {
87
112
  return server?.getRequestContext();
88
113
  },
89
- requestServices: options.requestServices
114
+ requestServices: resolvedOptions.requestServices
90
115
  });
91
116
  if (server === undefined) {
92
117
  throw new Error("Toolcraft HTTP MCP server was not created.");
93
118
  }
119
+ if (hostedMcpPath !== undefined) {
120
+ const listenHttp = server.listenHttp.bind(server);
121
+ server.listenHttp = (listenOptions = {}) => listenHttp({
122
+ path: hostedMcpPath,
123
+ ...listenOptions
124
+ });
125
+ }
94
126
  return server;
95
127
  }
96
128
  export async function runHTTPMCP(roots, options) {