poe-code 4.0.22 → 4.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "4.0.22",
3
+ "version": "4.0.23",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
  }
@@ -38,6 +38,13 @@ export interface HostedOAuthCredentialAccess<TCredential = unknown> {
38
38
  update(update: (credential: TCredential) => Promise<TCredential> | TCredential): Promise<TCredential>;
39
39
  delete(): Promise<void>;
40
40
  }
41
+ export interface HostedOAuthIdentity {
42
+ issuer: string;
43
+ subject: string;
44
+ clientId: string;
45
+ scopes: readonly string[];
46
+ resource: string;
47
+ }
41
48
  export interface HostedOAuthProvider<TCredential = unknown, TServices extends object = object> {
42
49
  name: string;
43
50
  login?: {
@@ -51,6 +58,7 @@ export interface HostedOAuthProvider<TCredential = unknown, TServices extends ob
51
58
  }>;
52
59
  services(input: {
53
60
  credentials: HostedOAuthCredentialAccess<TCredential>;
61
+ identity: HostedOAuthIdentity;
54
62
  }): Promise<Partial<TServices>> | Partial<TServices>;
55
63
  }
56
64
  export interface HostedOAuthInteractionAdapter<TCredential = unknown> {
@@ -106,7 +114,7 @@ export interface HostedOAuthRuntime<TServices extends object = object> {
106
114
  mcpPath: string;
107
115
  oauth: TinyHttpMcpServerOAuthOptions;
108
116
  requestHandler: HttpAdditionalRequestHandler;
109
- requestServices(subject: string): Promise<Partial<TServices>>;
117
+ requestServices(identity: HostedOAuthIdentity): Promise<Partial<TServices>>;
110
118
  }
111
119
  export declare function createInMemoryHostedOAuthStorage<TCredential = unknown>(options: {
112
120
  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": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
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,
@@ -241,7 +255,8 @@ export async function prepareHostedOAuthRuntime(config) {
241
255
  if (transaction === undefined ||
242
256
  !verifyAuthorizationInteractionCsrf({
243
257
  cookieHeader: request.headers.cookie ?? null,
244
- submittedToken: csrf
258
+ submittedToken: csrf,
259
+ cookieName: interactionCookieName(transactionId)
245
260
  }) ||
246
261
  transaction.expiresAt <= Date.now()) {
247
262
  response.writeHead(400, {
@@ -286,7 +301,7 @@ export async function prepareHostedOAuthRuntime(config) {
286
301
  : "Sign-in failed. Please try again.";
287
302
  response.writeHead(400, {
288
303
  "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'",
304
+ "content-security-policy": loginContentSecurityPolicy(transaction),
290
305
  "cache-control": "no-store",
291
306
  "referrer-policy": "no-referrer"
292
307
  });
@@ -332,9 +347,10 @@ export async function prepareHostedOAuthRuntime(config) {
332
347
  }
333
348
  },
334
349
  requestHandler,
335
- async requestServices(subject) {
336
- await credentialsFor(config.storage, subject).read();
337
- return config.provider.services({ credentials: credentialsFor(config.storage, subject) });
350
+ async requestServices(identity) {
351
+ const credentials = credentialsFor(config.storage, identity.subject);
352
+ await credentials.read();
353
+ return config.provider.services({ credentials, identity });
338
354
  }
339
355
  };
340
356
  }
@@ -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 subject = context.auth?.subject;
95
- if (subject === undefined)
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(subject))
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 = {}) => listenHttp({
122
- path: hostedMcpPath,
123
- ...listenOptions
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
  }