palaryn 0.3.0 → 0.3.2

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.
Files changed (38) hide show
  1. package/dist/src/anomaly/detector.d.ts +7 -4
  2. package/dist/src/anomaly/detector.d.ts.map +1 -1
  3. package/dist/src/anomaly/detector.js +22 -12
  4. package/dist/src/anomaly/detector.js.map +1 -1
  5. package/dist/src/auth/routes.d.ts.map +1 -1
  6. package/dist/src/auth/routes.js +35 -0
  7. package/dist/src/auth/routes.js.map +1 -1
  8. package/dist/src/executor/http-executor.d.ts.map +1 -1
  9. package/dist/src/executor/http-executor.js +18 -2
  10. package/dist/src/executor/http-executor.js.map +1 -1
  11. package/dist/src/mcp/http-transport.d.ts +2 -0
  12. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  13. package/dist/src/mcp/http-transport.js +16 -1
  14. package/dist/src/mcp/http-transport.js.map +1 -1
  15. package/dist/src/mcp/internal-auth.d.ts +13 -0
  16. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  17. package/dist/src/mcp/internal-auth.js +12 -0
  18. package/dist/src/mcp/internal-auth.js.map +1 -0
  19. package/dist/src/saas/routes.d.ts.map +1 -1
  20. package/dist/src/saas/routes.js +28 -12
  21. package/dist/src/saas/routes.js.map +1 -1
  22. package/dist/src/server/app.d.ts.map +1 -1
  23. package/dist/src/server/app.js +5 -1
  24. package/dist/src/server/app.js.map +1 -1
  25. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  26. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  27. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  28. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  29. package/dist/tests/unit/saas-routes-branches.test.js +125 -0
  30. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/anomaly/detector.ts +25 -12
  33. package/src/auth/routes.ts +40 -0
  34. package/src/executor/http-executor.ts +21 -2
  35. package/src/mcp/http-transport.ts +27 -1
  36. package/src/mcp/internal-auth.ts +14 -0
  37. package/src/saas/routes.ts +32 -15
  38. package/src/server/app.ts +5 -1
@@ -378,6 +378,46 @@ export function createAuthRouter(deps: AuthRouteDeps): Router {
378
378
  });
379
379
  });
380
380
 
381
+ // ---------------------------------------------------------------------------
382
+ // PUT /auth/password — change password for current user (or admin sets for another user by email)
383
+ // ---------------------------------------------------------------------------
384
+ router.put('/password', async (req: Request, res: Response) => {
385
+ const sessionUser = (req as any).sessionUser;
386
+ if (!sessionUser) {
387
+ res.status(401).json({ error: 'Not authenticated' });
388
+ return;
389
+ }
390
+
391
+ const { new_password, email } = req.body || {};
392
+ if (!new_password || new_password.length < 6) {
393
+ res.status(400).json({ error: 'new_password must be at least 6 characters' });
394
+ return;
395
+ }
396
+
397
+ // If email is provided and differs from session user, check admin
398
+ let targetUser = sessionUser;
399
+ if (email && email !== sessionUser.email) {
400
+ // Only workspace owners can reset other users' passwords
401
+ const memberships = workspaceMemberStore.getByUser(sessionUser.id);
402
+ const isOwner = memberships.some(m => m.role === 'owner');
403
+ if (!isOwner) {
404
+ res.status(403).json({ error: 'Only workspace owners can reset other users passwords' });
405
+ return;
406
+ }
407
+ const found = userStore.getByEmail(email);
408
+ if (!found) {
409
+ res.status(404).json({ error: 'User not found' });
410
+ return;
411
+ }
412
+ targetUser = found;
413
+ }
414
+
415
+ const newHash = await hashPassword(new_password);
416
+ userStore.update(targetUser.id, { password_hash: newHash, updated_at: new Date().toISOString() });
417
+
418
+ res.json({ ok: true, email: targetUser.email });
419
+ });
420
+
381
421
  // ---------------------------------------------------------------------------
382
422
  // DELETE /auth/users/:id — delete a user and all related data (self only)
383
423
  // ---------------------------------------------------------------------------
@@ -7,6 +7,7 @@ import { ToolCall } from '../types/tool-call';
7
7
  import { ToolOutput } from '../types/tool-result';
8
8
  import { ExecutorConfig } from '../types/config';
9
9
  import { ToolExecutor } from './interfaces';
10
+ import { internalAuthTokens } from '../mcp/internal-auth';
10
11
 
11
12
  /** Maximum response body size in bytes (10 MB) */
12
13
  const MAX_RESPONSE_BODY_BYTES = 10 * 1024 * 1024;
@@ -111,15 +112,33 @@ export class HttpExecutor implements ToolExecutor {
111
112
 
112
113
  // Execute an HTTP request based on ToolCall args
113
114
  async execute(toolCall: ToolCall): Promise<ToolOutput> {
114
- const { method = 'GET', url, headers = {}, body } = toolCall.args;
115
+ const { method = 'GET', url, body } = toolCall.args;
116
+ const headers: Record<string, string> = (toolCall.args.headers as Record<string, string>) || {};
115
117
 
116
118
  if (!url || typeof url !== 'string') {
117
119
  throw new Error('Missing or invalid URL in tool call args');
118
120
  }
119
121
 
122
+ // Check for internal auth (self-referencing gateway request via MCP)
123
+ const internalAuth = internalAuthTokens.get(toolCall.tool_call_id);
124
+ let skipSsrf = false;
125
+ if (internalAuth) {
126
+ internalAuthTokens.delete(toolCall.tool_call_id);
127
+ if (url.startsWith(internalAuth.gateway_base_url)) {
128
+ // Inject Bearer token unless the user already set an Authorization header
129
+ const hasAuth = headers && Object.keys(headers).some(
130
+ k => k.toLowerCase() === 'authorization',
131
+ );
132
+ if (!hasAuth) {
133
+ headers['Authorization'] = `Bearer ${internalAuth.token}`;
134
+ }
135
+ skipSsrf = true;
136
+ }
137
+ }
138
+
120
139
  // SSRF protection: block requests to private/internal IPs and pin DNS
121
140
  let pinnedIP: string | undefined;
122
- if (this.ssrfProtectionEnabled) {
141
+ if (this.ssrfProtectionEnabled && !skipSsrf) {
123
142
  const parsedUrl = new URL(url);
124
143
  pinnedIP = await validateResolvedIP(parsedUrl.hostname);
125
144
  }
@@ -6,6 +6,7 @@ import { z } from 'zod';
6
6
  import { Gateway } from '../server/gateway';
7
7
  import { ToolCall, ToolCallArgs, ToolInfo } from '../types/tool-call';
8
8
  import { ToolResult } from '../types/tool-result';
9
+ import { internalAuthTokens } from './internal-auth';
9
10
 
10
11
  // ---------------------------------------------------------------------------
11
12
  // Configuration
@@ -14,15 +15,19 @@ import { ToolResult } from '../types/tool-result';
14
15
  export interface MCPHttpConfig {
15
16
  /** Platform identifier (default: 'mcp_http') */
16
17
  platform?: string;
18
+ /** Gateway's own base URL — when set, self-referencing requests get the OAuth token injected */
19
+ gateway_base_url?: string;
17
20
  }
18
21
 
19
22
  interface ResolvedMCPHttpConfig {
20
23
  platform: string;
24
+ gateway_base_url?: string;
21
25
  }
22
26
 
23
27
  function resolveConfig(config?: MCPHttpConfig): ResolvedMCPHttpConfig {
24
28
  return {
25
29
  platform: config?.platform || 'mcp_http',
30
+ gateway_base_url: config?.gateway_base_url,
26
31
  };
27
32
  }
28
33
 
@@ -34,6 +39,8 @@ interface IdentityOverride {
34
39
  workspace_id: string;
35
40
  actor_id: string;
36
41
  api_key_tags?: string[];
42
+ /** Raw OAuth access token — used for self-referencing gateway requests */
43
+ token?: string;
37
44
  }
38
45
 
39
46
  /**
@@ -68,7 +75,8 @@ function resolveIdentity(extra?: { authInfo?: unknown }): IdentityOverride | und
68
75
  }
69
76
 
70
77
  if (typeof wsId === 'string' && wsId && typeof actorId === 'string' && actorId) {
71
- return { workspace_id: wsId, actor_id: actorId, api_key_tags: apiKeyTags };
78
+ const token = typeof authInfo.token === 'string' ? authInfo.token : undefined;
79
+ return { workspace_id: wsId, actor_id: actorId, api_key_tags: apiKeyTags, token };
72
80
  }
73
81
  return undefined;
74
82
  }
@@ -160,6 +168,21 @@ function buildToolCall(
160
168
  return toolCall;
161
169
  }
162
170
 
171
+ /** Store internal auth token for self-referencing gateway requests. */
172
+ function setInternalAuth(
173
+ config: ResolvedMCPHttpConfig,
174
+ identity: IdentityOverride,
175
+ toolCall: ToolCall,
176
+ targetUrl: string,
177
+ ): void {
178
+ if (config.gateway_base_url && identity.token && targetUrl.startsWith(config.gateway_base_url)) {
179
+ internalAuthTokens.set(toolCall.tool_call_id, {
180
+ token: identity.token,
181
+ gateway_base_url: config.gateway_base_url,
182
+ });
183
+ }
184
+ }
185
+
163
186
  function formatResult(result: ToolResult): { content: Array<{ type: 'text'; text: string }>; isError?: boolean } {
164
187
  const isError = result.status === 'error' || result.status === 'blocked';
165
188
 
@@ -393,6 +416,7 @@ function createMCPServerInstance(gateway: Gateway, config: ResolvedMCPHttpConfig
393
416
  context,
394
417
  identity,
395
418
  });
419
+ setInternalAuth(config, identity, toolCall, a.url as string);
396
420
  return formatResult(await executeWithApprovalPolling(gateway, toolCall));
397
421
  },
398
422
  );
@@ -435,6 +459,7 @@ function createMCPServerInstance(gateway: Gateway, config: ResolvedMCPHttpConfig
435
459
  context,
436
460
  identity,
437
461
  });
462
+ setInternalAuth(config, identity, toolCall, a.url as string);
438
463
  return formatResult(await executeWithApprovalPolling(gateway, toolCall));
439
464
  },
440
465
  );
@@ -479,6 +504,7 @@ function createMCPServerInstance(gateway: Gateway, config: ResolvedMCPHttpConfig
479
504
  context,
480
505
  identity,
481
506
  });
507
+ setInternalAuth(config, identity, toolCall, a.url as string);
482
508
  return formatResult(await executeWithApprovalPolling(gateway, toolCall));
483
509
  },
484
510
  );
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Side-channel for passing MCP OAuth tokens to the HttpExecutor without
3
+ * polluting ToolCall.args (which would trigger DLP and leak into audit logs).
4
+ *
5
+ * Keyed by `tool_call_id` — the MCP handler sets an entry before gateway.execute(),
6
+ * and the HttpExecutor consumes (and deletes) it during request execution.
7
+ */
8
+
9
+ export interface InternalAuthEntry {
10
+ token: string;
11
+ gateway_base_url: string;
12
+ }
13
+
14
+ export const internalAuthTokens = new Map<string, InternalAuthEntry>();
@@ -1149,19 +1149,34 @@ Output: {"name":"approve-slack-writes","description":"Require approval for write
1149
1149
  const controller = new AbortController();
1150
1150
  const timeout = setTimeout(() => controller.abort(), 15000);
1151
1151
 
1152
- const llmRes = await fetch('https://api.anthropic.com/v1/messages', {
1152
+ // Detect provider from API key prefix
1153
+ const isOpenAI = apiKey.startsWith('sk-proj-') || apiKey.startsWith('sk-');
1154
+ const llmUrl = isOpenAI
1155
+ ? 'https://api.openai.com/v1/chat/completions'
1156
+ : 'https://api.anthropic.com/v1/messages';
1157
+ const llmHeaders: Record<string, string> = isOpenAI
1158
+ ? { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }
1159
+ : { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
1160
+ const llmBody = isOpenAI
1161
+ ? JSON.stringify({
1162
+ model: 'gpt-4.1-mini',
1163
+ max_tokens: 1024,
1164
+ messages: [
1165
+ { role: 'system', content: systemPrompt },
1166
+ { role: 'user', content: trimmed },
1167
+ ],
1168
+ })
1169
+ : JSON.stringify({
1170
+ model: 'claude-sonnet-4-5-20241022',
1171
+ max_tokens: 1024,
1172
+ system: systemPrompt,
1173
+ messages: [{ role: 'user', content: trimmed }],
1174
+ });
1175
+
1176
+ const llmRes = await fetch(llmUrl, {
1153
1177
  method: 'POST',
1154
- headers: {
1155
- 'Content-Type': 'application/json',
1156
- 'x-api-key': apiKey,
1157
- 'anthropic-version': '2023-06-01',
1158
- },
1159
- body: JSON.stringify({
1160
- model: 'claude-sonnet-4-5-20250929',
1161
- max_tokens: 1024,
1162
- system: systemPrompt,
1163
- messages: [{ role: 'user', content: trimmed }],
1164
- }),
1178
+ headers: llmHeaders,
1179
+ body: llmBody,
1165
1180
  signal: controller.signal,
1166
1181
  });
1167
1182
 
@@ -1174,8 +1189,10 @@ Output: {"name":"approve-slack-writes","description":"Require approval for write
1174
1189
  return;
1175
1190
  }
1176
1191
 
1177
- const llmData = await llmRes.json() as { content: { type: string; text: string }[] };
1178
- const text = llmData.content?.[0]?.text || '';
1192
+ const llmData = await llmRes.json() as any;
1193
+ const text = isOpenAI
1194
+ ? (llmData.choices?.[0]?.message?.content || '')
1195
+ : (llmData.content?.[0]?.text || '');
1179
1196
 
1180
1197
  // Strip markdown code fences if present
1181
1198
  const cleaned = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
@@ -1621,7 +1638,7 @@ Output: {"name":"approve-slack-writes","description":"Require approval for write
1621
1638
  return;
1622
1639
  }
1623
1640
 
1624
- const report = detector.getBaselineReport();
1641
+ const report = detector.getBaselineReport(workspaceId);
1625
1642
  res.json(report);
1626
1643
  });
1627
1644
 
package/src/server/app.ts CHANGED
@@ -843,7 +843,11 @@ export function createApp(config: GatewayConfig, externalSaaSStores?: Partial<Sa
843
843
  // MCP HTTP transport with OAuth 2.0 support.
844
844
  // When mcp_oauth is enabled, uses the MCP SDK's OAuth router + bearer auth.
845
845
  // When disabled, falls back to existing auth middleware + RBAC.
846
- const mcpHandler = createMCPHttpHandler(gateway);
846
+ const mcpBaseUrl = config.mcp_oauth?.base_url
847
+ || (isProduction ? 'https://app.palaryn.com' : `http://localhost:${config.port}`);
848
+ const mcpHandler = createMCPHttpHandler(gateway, {
849
+ gateway_base_url: config.mcp_oauth?.enabled ? mcpBaseUrl : undefined,
850
+ });
847
851
 
848
852
  if (config.mcp_oauth?.enabled) {
849
853
  // Create OAuth stores — prefer Postgres-backed stores when available