@mhingston5/conduit 1.1.4 → 1.1.6

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 @@
1
+ {"version":3,"sources":["../src/executors/pyodide.worker.ts"],"sourcesContent":["import { parentPort, workerData } from 'node:worker_threads';\nimport { loadPyodide, type PyodideInterface } from 'pyodide';\nimport net from 'node:net';\n\nlet pyodide: PyodideInterface | null = null;\nlet currentStdout = '';\nlet currentStderr = '';\nlet totalOutputBytes = 0;\nlet totalLogEntries = 0;\nlet currentLimits: any = null;\n\nasync function init() {\n if (pyodide) return pyodide;\n\n pyodide = await loadPyodide({\n stdout: (text) => {\n if (currentLimits && (totalOutputBytes > (currentLimits.maxOutputBytes || 1024 * 1024) || totalLogEntries > (currentLimits.maxLogEntries || 10000))) {\n return; // Stop processing logs once limit breached\n }\n currentStdout += text + '\\n';\n totalOutputBytes += text.length + 1;\n totalLogEntries++;\n },\n stderr: (text) => {\n if (currentLimits && (totalOutputBytes > (currentLimits.maxOutputBytes || 1024 * 1024) || totalLogEntries > (currentLimits.maxLogEntries || 10000))) {\n return; // Stop processing logs once limit breached\n }\n currentStderr += text + '\\n';\n totalOutputBytes += text.length + 1;\n totalLogEntries++;\n },\n });\n\n return pyodide;\n}\n\nasync function handleTask(data: any) {\n const { code, limits, ipcInfo, shim } = data;\n currentStdout = '';\n currentStderr = '';\n totalOutputBytes = 0;\n totalLogEntries = 0;\n currentLimits = limits;\n\n try {\n const p = await init();\n\n const sendIPCRequest = async (method: string, params: any) => {\n if (!ipcInfo?.ipcAddress) throw new Error('Conduit IPC address not configured');\n\n return new Promise((resolve, reject) => {\n let client: net.Socket;\n\n if (ipcInfo.ipcAddress.includes(':')) {\n const lastColon = ipcInfo.ipcAddress.lastIndexOf(':');\n const host = ipcInfo.ipcAddress.substring(0, lastColon);\n const port = ipcInfo.ipcAddress.substring(lastColon + 1);\n\n let targetHost = host.replace(/[\\[\\]]/g, '');\n if (targetHost === '0.0.0.0' || targetHost === '::' || targetHost === '::1' || targetHost === '') {\n targetHost = '127.0.0.1';\n }\n\n client = net.createConnection({\n host: targetHost,\n port: parseInt(port)\n });\n } else {\n client = net.createConnection({ path: ipcInfo.ipcAddress });\n }\n\n const id = Math.random().toString(36).substring(7);\n const request = {\n jsonrpc: '2.0',\n id,\n method,\n params: params || {},\n auth: { bearerToken: ipcInfo.ipcToken }\n };\n\n client.on('error', (err) => {\n reject(err);\n client.destroy();\n });\n\n client.write(JSON.stringify(request) + '\\n');\n\n let buffer = '';\n client.on('data', (data) => {\n buffer += data.toString();\n // Robust framing: read until we find a complete JSON object on a line\n const lines = buffer.split('\\n');\n buffer = lines.pop() || ''; // Keep the last partial line\n\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const response = JSON.parse(line);\n if (response.id === id) {\n if (response.error) {\n reject(new Error(response.error.message));\n } else {\n resolve(response.result);\n }\n client.end();\n return;\n }\n } catch (e) {\n // If parse fails, it might be a partial line that we haven't seen the end of yet\n // but since we split by \\n, this shouldn't happen unless the \\n was inside the JSON.\n // However, Conduit ensures JSON-RPC is one line.\n }\n }\n });\n\n client.on('end', () => {\n if (buffer.trim()) {\n try {\n const response = JSON.parse(buffer);\n if (response.id === id) {\n if (response.error) {\n reject(new Error(response.error.message));\n } else {\n resolve(response.result);\n }\n }\n } catch (e) { }\n }\n });\n });\n };\n\n (p as any).globals.set('discover_mcp_tools_js', (options: any) => {\n return sendIPCRequest('mcp_discover_tools', options);\n });\n\n (p as any).globals.set('call_mcp_tool_js', (name: string, args: any) => {\n return sendIPCRequest('mcp_call_tool', { name, arguments: args });\n });\n\n if (shim) {\n await p.runPythonAsync(shim);\n }\n\n const result = await p.runPythonAsync(code);\n\n if (totalOutputBytes > (limits.maxOutputBytes || 1024 * 1024)) {\n throw new Error('[LIMIT_OUTPUT]');\n }\n if (totalLogEntries > (limits.maxLogEntries || 10000)) {\n throw new Error('[LIMIT_LOG]');\n }\n\n parentPort?.postMessage({\n stdout: currentStdout,\n stderr: currentStderr,\n result: String(result),\n success: true,\n });\n } catch (err: any) {\n let isOutput = err.message.includes('[LIMIT_OUTPUT]');\n let isLog = err.message.includes('[LIMIT_LOG]');\n\n // Fallback: check counters if message doesn't match (e.g. wrapped in OSError)\n if (!isOutput && !isLog && currentLimits) {\n if (totalOutputBytes > (currentLimits.maxOutputBytes || 1024 * 1024)) {\n isOutput = true;\n }\n // Check specific log limit breach\n if (totalLogEntries > (currentLimits.maxLogEntries || 10000)) {\n isLog = true;\n }\n }\n\n parentPort?.postMessage({\n stdout: currentStdout,\n stderr: currentStderr,\n error: err.message,\n limitBreached: isOutput ? 'output' : (isLog ? 'log' : undefined),\n success: false,\n });\n }\n}\n\nparentPort?.on('message', async (msg) => {\n if (msg.type === 'execute') {\n await handleTask(msg.data);\n } else if (msg.type === 'ping') {\n parentPort?.postMessage({ type: 'pong' });\n }\n});\n\n// Signal ready\nparentPort?.postMessage({ type: 'ready' });\n\n"],"mappings":"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","names":["data"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhingston5/conduit",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "type": "module",
5
5
  "description": "A secure Code Mode execution substrate for MCP agents",
6
6
  "main": "index.js",
package/src/auth.cmd.ts CHANGED
@@ -2,22 +2,143 @@ import Fastify from 'fastify';
2
2
  import axios from 'axios';
3
3
  import open from 'open';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
+ import crypto from 'node:crypto';
5
6
 
6
7
  export interface AuthOptions {
7
- authUrl: string;
8
- tokenUrl: string;
8
+ authUrl?: string;
9
+ tokenUrl?: string;
9
10
  clientId: string;
10
- clientSecret: string;
11
+ clientSecret?: string;
11
12
  scopes?: string;
12
13
  port?: number;
14
+ mcpUrl?: string;
15
+ usePkce?: boolean;
16
+ }
17
+
18
+ type DiscoveredOAuth = {
19
+ authUrl: string;
20
+ tokenUrl: string;
21
+ scopes?: string[];
22
+ resource?: string;
23
+ };
24
+
25
+ const AUTH_REQUEST_PAYLOAD = {
26
+ jsonrpc: '2.0',
27
+ id: 'conduit-auth',
28
+ method: 'initialize',
29
+ params: {
30
+ clientInfo: {
31
+ name: 'conduit-auth',
32
+ version: '1.0.0',
33
+ },
34
+ },
35
+ };
36
+
37
+ function base64UrlEncode(buffer: Buffer): string {
38
+ return buffer
39
+ .toString('base64')
40
+ .replace(/\+/g, '-')
41
+ .replace(/\//g, '_')
42
+ .replace(/=+$/g, '');
43
+ }
44
+
45
+ function createCodeVerifier(): string {
46
+ return base64UrlEncode(crypto.randomBytes(32));
47
+ }
48
+
49
+ function createCodeChallenge(verifier: string): string {
50
+ return base64UrlEncode(crypto.createHash('sha256').update(verifier).digest());
51
+ }
52
+
53
+ function parseResourceMetadataHeader(headerValue: string | string[] | undefined): string | null {
54
+ if (!headerValue) return null;
55
+ const header = Array.isArray(headerValue) ? headerValue.join(',') : headerValue;
56
+ const match = header.match(/resource_metadata="([^"]+)"/i) || header.match(/resource_metadata=([^, ]+)/i);
57
+ return match ? match[1] : null;
58
+ }
59
+
60
+ async function discoverOAuthFromMcp(mcpUrl: string): Promise<DiscoveredOAuth> {
61
+ const attempts = [
62
+ () => axios.get(mcpUrl, { validateStatus: () => true }),
63
+ () => axios.post(mcpUrl, AUTH_REQUEST_PAYLOAD, { validateStatus: () => true }),
64
+ ];
65
+
66
+ let resourceMetadataUrl: string | null = null;
67
+ for (const attempt of attempts) {
68
+ const response = await attempt();
69
+ resourceMetadataUrl = parseResourceMetadataHeader(response.headers['www-authenticate']);
70
+ if (resourceMetadataUrl) break;
71
+ }
72
+
73
+ if (!resourceMetadataUrl) {
74
+ throw new Error('Unable to discover OAuth metadata (missing WWW-Authenticate resource_metadata)');
75
+ }
76
+
77
+ const metadataResponse = await axios.get(resourceMetadataUrl);
78
+ const metadata = metadataResponse.data as Record<string, any>;
79
+
80
+ let authUrl = metadata.authorization_endpoint as string | undefined;
81
+ let tokenUrl = metadata.token_endpoint as string | undefined;
82
+ let scopes = Array.isArray(metadata.scopes_supported) ? metadata.scopes_supported : undefined;
83
+ const resource = typeof metadata.resource === 'string' ? metadata.resource : undefined;
84
+
85
+ if (!authUrl || !tokenUrl) {
86
+ const authServer = (Array.isArray(metadata.authorization_servers) && metadata.authorization_servers[0]) || metadata.issuer;
87
+ if (!authServer) {
88
+ throw new Error('OAuth metadata did not include authorization server info');
89
+ }
90
+
91
+ const asMetadataUrl = new URL('/.well-known/oauth-authorization-server', authServer).toString();
92
+ const asMetadataResponse = await axios.get(asMetadataUrl);
93
+ const asMetadata = asMetadataResponse.data as Record<string, any>;
94
+
95
+ authUrl = authUrl || (asMetadata.authorization_endpoint as string | undefined);
96
+ tokenUrl = tokenUrl || (asMetadata.token_endpoint as string | undefined);
97
+ scopes = scopes || (Array.isArray(asMetadata.scopes_supported) ? asMetadata.scopes_supported : undefined);
98
+ }
99
+
100
+ if (!authUrl || !tokenUrl) {
101
+ throw new Error('OAuth discovery failed: missing authorization or token endpoint');
102
+ }
103
+
104
+ return { authUrl, tokenUrl, scopes, resource };
105
+ }
106
+
107
+ function normalizeScopes(rawScopes?: string): string | undefined {
108
+ if (!rawScopes) return undefined;
109
+ return rawScopes
110
+ .split(',')
111
+ .map(scope => scope.trim())
112
+ .filter(Boolean)
113
+ .join(' ');
13
114
  }
14
115
 
15
116
  export async function handleAuth(options: AuthOptions) {
16
117
  const port = options.port || 3333;
17
118
  const redirectUri = `http://localhost:${port}/callback`;
18
119
  const state = uuidv4();
120
+ const codeVerifier = options.usePkce ? createCodeVerifier() : undefined;
121
+ const codeChallenge = codeVerifier ? createCodeChallenge(codeVerifier) : undefined;
19
122
 
20
123
  const fastify = Fastify();
124
+ let resolvedScopes = normalizeScopes(options.scopes);
125
+ let resolvedAuthUrl = options.authUrl;
126
+ let resolvedTokenUrl = options.tokenUrl;
127
+ let resolvedResource: string | undefined;
128
+
129
+ if (options.mcpUrl) {
130
+ const discovered = await discoverOAuthFromMcp(options.mcpUrl);
131
+ resolvedAuthUrl = discovered.authUrl;
132
+ resolvedTokenUrl = discovered.tokenUrl;
133
+ resolvedResource = discovered.resource;
134
+ if (!resolvedScopes && discovered.scopes && discovered.scopes.length > 0) {
135
+ resolvedScopes = discovered.scopes.join(' ');
136
+ }
137
+ }
138
+
139
+ if (!resolvedAuthUrl || !resolvedTokenUrl) {
140
+ throw new Error('OAuth configuration missing authUrl or tokenUrl (set --mcp-url or provide both)');
141
+ }
21
142
 
22
143
  return new Promise<void>((resolve, reject) => {
23
144
  fastify.get('/callback', async (request, reply) => {
@@ -36,12 +157,26 @@ export async function handleAuth(options: AuthOptions) {
36
157
  }
37
158
 
38
159
  try {
39
- const response = await axios.post(options.tokenUrl, {
40
- grant_type: 'authorization_code',
41
- code,
42
- redirect_uri: redirectUri,
43
- client_id: options.clientId,
44
- client_secret: options.clientSecret,
160
+ const body = new URLSearchParams();
161
+ body.set('grant_type', 'authorization_code');
162
+ body.set('code', code);
163
+ body.set('redirect_uri', redirectUri);
164
+ body.set('client_id', options.clientId);
165
+ if (options.clientSecret) {
166
+ body.set('client_secret', options.clientSecret);
167
+ }
168
+ if (codeVerifier) {
169
+ body.set('code_verifier', codeVerifier);
170
+ }
171
+ if (resolvedResource) {
172
+ body.set('resource', resolvedResource);
173
+ }
174
+
175
+ const response = await axios.post(resolvedTokenUrl, body, {
176
+ headers: {
177
+ 'Content-Type': 'application/x-www-form-urlencoded',
178
+ 'Accept': 'application/json',
179
+ },
45
180
  });
46
181
 
47
182
  const { refresh_token, access_token } = response.data;
@@ -51,9 +186,14 @@ export async function handleAuth(options: AuthOptions) {
51
186
  console.log('credentials:');
52
187
  console.log(' type: oauth2');
53
188
  console.log(` clientId: ${options.clientId}`);
54
- console.log(` clientSecret: ${options.clientSecret}`);
55
- console.log(` tokenUrl: "${options.tokenUrl}"`);
189
+ if (options.clientSecret) {
190
+ console.log(` clientSecret: ${options.clientSecret}`);
191
+ }
192
+ console.log(` tokenUrl: "${resolvedTokenUrl}"`);
56
193
  console.log(` refreshToken: "${refresh_token || 'N/A (No refresh token returned)'}"`);
194
+ if (resolvedScopes) {
195
+ console.log(` scopes: ["${resolvedScopes.split(' ').join('", "')}"]`);
196
+ }
57
197
 
58
198
  if (!refresh_token) {
59
199
  console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
@@ -78,13 +218,20 @@ export async function handleAuth(options: AuthOptions) {
78
218
  return;
79
219
  }
80
220
 
81
- const authUrl = new URL(options.authUrl);
221
+ const authUrl = new URL(resolvedAuthUrl);
82
222
  authUrl.searchParams.append('client_id', options.clientId);
83
223
  authUrl.searchParams.append('redirect_uri', redirectUri);
84
224
  authUrl.searchParams.append('response_type', 'code');
85
225
  authUrl.searchParams.append('state', state);
86
- if (options.scopes) {
87
- authUrl.searchParams.append('scope', options.scopes);
226
+ if (resolvedScopes) {
227
+ authUrl.searchParams.append('scope', resolvedScopes);
228
+ }
229
+ if (codeChallenge) {
230
+ authUrl.searchParams.append('code_challenge', codeChallenge);
231
+ authUrl.searchParams.append('code_challenge_method', 'S256');
232
+ }
233
+ if (resolvedResource) {
234
+ authUrl.searchParams.append('resource', resolvedResource);
88
235
  }
89
236
 
90
237
  console.log(`Opening browser to: ${authUrl.toString()}`);
@@ -134,10 +134,14 @@ export class ConfigService {
134
134
  (fs.existsSync(path.resolve(process.cwd(), 'conduit.yaml')) ? 'conduit.yaml' :
135
135
  (fs.existsSync(path.resolve(process.cwd(), 'conduit.json')) ? 'conduit.json' : null));
136
136
 
137
- if (!configPath) return {};
137
+ if (!configPath) {
138
+ console.warn(`[Conduit] No config file found in ${process.cwd()}. Running with default settings.`);
139
+ return {};
140
+ }
138
141
 
139
142
  try {
140
143
  const fullPath = path.resolve(process.cwd(), configPath);
144
+ console.error(`[Conduit] Loading config from ${fullPath}`);
141
145
  let fileContent = fs.readFileSync(fullPath, 'utf-8');
142
146
 
143
147
  // Env var substitution: ${VAR} or ${VAR:-default}
@@ -137,17 +137,22 @@ export class ExecutionService {
137
137
  const packages = await this.gatewayService.listToolPackages();
138
138
  const allBindings = [];
139
139
 
140
+ this.logger.debug({ packageCount: packages.length, packages: packages.map(p => p.id) }, 'Fetching tool bindings');
141
+
140
142
  for (const pkg of packages) {
141
143
  try {
142
144
  // Determine if we need to fetch tools for this package
143
145
  // Optimization: if allowedTools is strict, we could filter packages here
144
146
 
145
147
  const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
148
+ this.logger.debug({ packageId: pkg.id, stubCount: stubs.length }, 'Got stubs from package');
146
149
  allBindings.push(...stubs.map(s => toToolBinding(s.id, undefined, s.description)));
147
150
  } catch (err: any) {
148
151
  this.logger.warn({ packageId: pkg.id, err: err.message }, 'Failed to list stubs for package');
149
152
  }
150
153
  }
154
+
155
+ this.logger.info({ totalBindings: allBindings.length }, 'Tool bindings ready for SDK generation');
151
156
  return allBindings;
152
157
  }
153
158
 
@@ -66,6 +66,11 @@ export class PolicyService {
66
66
  return true;
67
67
  }
68
68
 
69
+ // Improved matching: if pattern has only one part, match it against the tool's name part
70
+ if (patternParts.length === 1 && toolParts.length > 1) {
71
+ return patternParts[0] === toolParts[toolParts.length - 1];
72
+ }
73
+
69
74
  // Exact match: pattern parts must equal tool parts
70
75
  if (patternParts.length !== toolParts.length) return false;
71
76
  for (let i = 0; i < patternParts.length; i++) {
@@ -104,6 +104,9 @@ export class RequestController {
104
104
  case 'tools/list': // Standard MCP method name
105
105
  case 'mcp_discover_tools':
106
106
  return this.handleDiscoverTools(params, context, id);
107
+ case 'resources/list':
108
+ case 'prompts/list':
109
+ return { jsonrpc: '2.0', id, result: { items: [] } };
107
110
  case 'mcp_list_tool_packages':
108
111
  return this.handleListToolPackages(params, context, id);
109
112
  case 'mcp_list_tool_stubs':
@@ -125,6 +128,8 @@ export class RequestController {
125
128
  return this.handleInitialize(params, context, id);
126
129
  case 'notifications/initialized':
127
130
  return null; // Notifications don't get responses per MCP spec
131
+ case 'mcp_register_upstream':
132
+ return this.handleRegisterUpstream(params, context, id);
128
133
  case 'ping':
129
134
  return { jsonrpc: '2.0', id, result: {} };
130
135
  default:
@@ -135,6 +140,23 @@ export class RequestController {
135
140
  }
136
141
  }
137
142
 
143
+ private async handleRegisterUpstream(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
144
+ if (!params || !params.id || !params.type || (!params.url && !params.command)) {
145
+ return this.errorResponse(id, -32602, 'Missing registration parameters (id, type, url/command)');
146
+ }
147
+
148
+ try {
149
+ this.gatewayService.registerUpstream(params);
150
+ return {
151
+ jsonrpc: '2.0',
152
+ id,
153
+ result: { success: true }
154
+ };
155
+ } catch (err: any) {
156
+ return this.errorResponse(id, -32001, err.message);
157
+ }
158
+ }
159
+
138
160
  private async handleDiscoverTools(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
139
161
  const tools = await this.gatewayService.discoverTools(context);
140
162
 
@@ -212,20 +234,71 @@ export class RequestController {
212
234
  if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
213
235
  const { name, arguments: toolArgs } = params;
214
236
 
237
+ const toolId = this.gatewayService.policyService.parseToolName(name);
238
+ const baseName = toolId.name;
239
+ const isConduit = toolId.namespace === 'conduit' || toolId.namespace === '';
240
+
215
241
  // Route built-in tools to their specific handlers
216
- switch (name) {
217
- case 'mcp_execute_typescript':
218
- return this.handleExecuteTypeScript(toolArgs, context, id);
219
- case 'mcp_execute_python':
220
- return this.handleExecutePython(toolArgs, context, id);
221
- case 'mcp_execute_isolate':
222
- return this.handleExecuteIsolate(toolArgs, context, id);
242
+ if (isConduit) {
243
+ switch (baseName) {
244
+ case 'mcp_execute_typescript':
245
+ return this.handleExecuteToolCall('typescript', toolArgs, context, id);
246
+ case 'mcp_execute_python':
247
+ return this.handleExecuteToolCall('python', toolArgs, context, id);
248
+ case 'mcp_execute_isolate':
249
+ return this.handleExecuteToolCall('isolate', toolArgs, context, id);
250
+ }
223
251
  }
224
252
 
225
253
  const response = await this.gatewayService.callTool(name, toolArgs, context);
226
254
  return { ...response, id };
227
255
  }
228
256
 
257
+ private formatExecutionResult(result: { stdout: string; stderr: string; exitCode: number | null }) {
258
+ const structured = {
259
+ stdout: result.stdout,
260
+ stderr: result.stderr,
261
+ exitCode: result.exitCode,
262
+ };
263
+ return {
264
+ content: [{
265
+ type: 'text',
266
+ text: JSON.stringify(structured),
267
+ }],
268
+ structuredContent: structured,
269
+ };
270
+ }
271
+
272
+ private async handleExecuteToolCall(
273
+ mode: 'typescript' | 'python' | 'isolate',
274
+ params: any,
275
+ context: ExecutionContext,
276
+ id: string | number
277
+ ): Promise<JSONRPCResponse> {
278
+ if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
279
+ const { code, limits, allowedTools } = params;
280
+
281
+ if (Array.isArray(allowedTools)) {
282
+ context.allowedTools = allowedTools;
283
+ }
284
+
285
+ const result = mode === 'typescript'
286
+ ? await this.executionService.executeTypeScript(code, limits, context, allowedTools)
287
+ : mode === 'python'
288
+ ? await this.executionService.executePython(code, limits, context, allowedTools)
289
+ : await this.executionService.executeIsolate(code, limits, context, allowedTools);
290
+
291
+ if (result.error) {
292
+ return this.errorResponse(id, result.error.code, result.error.message);
293
+ }
294
+
295
+ return {
296
+ jsonrpc: '2.0',
297
+ id,
298
+ result: this.formatExecutionResult(result),
299
+ };
300
+ }
301
+
229
302
  private async handleExecuteTypeScript(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
230
303
  if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
231
304
  const { code, limits, allowedTools } = params;
@@ -94,9 +94,15 @@ export class PyodideExecutor implements Executor {
94
94
  }
95
95
 
96
96
  private createWorker(limits?: ConduitResourceLimits): Worker {
97
- let workerPath = path.resolve(__dirname, './pyodide.worker.js');
98
- if (!fs.existsSync(workerPath)) {
99
- workerPath = path.resolve(__dirname, './pyodide.worker.ts');
97
+ const candidates = [
98
+ path.resolve(__dirname, './pyodide.worker.js'),
99
+ path.resolve(__dirname, './pyodide.worker.ts'),
100
+ path.resolve(__dirname, './executors/pyodide.worker.js'),
101
+ path.resolve(__dirname, './executors/pyodide.worker.ts'),
102
+ ];
103
+ const workerPath = candidates.find(p => fs.existsSync(p));
104
+ if (!workerPath) {
105
+ throw new Error(`Pyodide worker not found. Tried: ${candidates.join(', ')}`);
100
106
  }
101
107
 
102
108
  return new Worker(workerPath, {
@@ -324,4 +330,3 @@ export class PyodideExecutor implements Executor {
324
330
  }
325
331
  }
326
332
  }
327
-
@@ -11,6 +11,7 @@ export interface UpstreamCredentials {
11
11
  clientSecret?: string;
12
12
  tokenUrl?: string;
13
13
  refreshToken?: string;
14
+ scopes?: string[];
14
15
  }
15
16
 
16
17
  interface CachedToken {
@@ -73,26 +74,35 @@ export class AuthService {
73
74
  }
74
75
 
75
76
  private async doRefresh(creds: UpstreamCredentials, cacheKey: string): Promise<string> {
76
- if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId || !creds.clientSecret) {
77
+ if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId) {
77
78
  throw new Error('OAuth2 credentials missing required fields for refresh');
78
79
  }
79
80
 
80
81
  this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, 'Refreshing OAuth2 token');
81
82
 
82
83
  try {
83
- const response = await axios.post(creds.tokenUrl, {
84
- grant_type: 'refresh_token',
85
- refresh_token: creds.refreshToken,
86
- client_id: creds.clientId,
87
- client_secret: creds.clientSecret,
84
+ const body = new URLSearchParams();
85
+ body.set('grant_type', 'refresh_token');
86
+ body.set('refresh_token', creds.refreshToken);
87
+ body.set('client_id', creds.clientId);
88
+ if (creds.clientSecret) {
89
+ body.set('client_secret', creds.clientSecret);
90
+ }
91
+
92
+ const response = await axios.post(creds.tokenUrl, body, {
93
+ headers: {
94
+ 'Content-Type': 'application/x-www-form-urlencoded',
95
+ 'Accept': 'application/json',
96
+ },
88
97
  });
89
98
 
90
99
  const { access_token, expires_in } = response.data;
100
+ const expiresInSeconds = Number(expires_in) || 3600;
91
101
 
92
102
  // Cache the token (don't mutate the input credentials)
93
103
  this.tokenCache.set(cacheKey, {
94
104
  accessToken: access_token,
95
- expiresAt: Date.now() + (expires_in * 1000),
105
+ expiresAt: Date.now() + (expiresInSeconds * 1000),
96
106
  });
97
107
 
98
108
  return `Bearer ${access_token}`;