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.
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +18 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +16 -1
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +28 -12
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +5 -1
- package/dist/src/server/app.js.map +1 -1
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +125 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -1
- package/package.json +1 -1
- package/src/anomaly/detector.ts +25 -12
- package/src/auth/routes.ts +40 -0
- package/src/executor/http-executor.ts +21 -2
- package/src/mcp/http-transport.ts +27 -1
- package/src/mcp/internal-auth.ts +14 -0
- package/src/saas/routes.ts +32 -15
- package/src/server/app.ts +5 -1
package/src/auth/routes.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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>();
|
package/src/saas/routes.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
1178
|
-
const 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
|
|
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
|