Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
473999c
feat(#249): add custom REST API endpoints backend foundation
radaghastly Feb 18, 2026
09d3d7e
feat(#249): add custom REST API endpoints backend foundation
radaghastly Feb 18, 2026
d89447a
feat(#249): add custom REST API endpoints backend foundation
radaghastly Feb 18, 2026
d7e9c33
feat(#249): add custom REST API endpoints backend foundation
radaghastly Feb 18, 2026
188b692
feat(#249): add custom REST API endpoints backend foundation
radaghastly Feb 18, 2026
c9e0eea
feat(#249): add custom REST API endpoints backend foundation
radaghastly Feb 18, 2026
d44288c
fix: address review feedback on PR #283 (#249)
radaghastly Feb 18, 2026
fd6d9f3
fix: address review feedback on PR #283 (#249)
radaghastly Feb 18, 2026
7d9b638
fix: address review feedback on PR #283 (#249)
radaghastly Feb 18, 2026
7e57a5f
fix: address review feedback on PR #283 (#249)
radaghastly Feb 18, 2026
a4b2971
fix: address review feedback on PR #283 (#249)
radaghastly Feb 18, 2026
4e407d9
fix: address review feedback on PR #283 (#249)
radaghastly Feb 18, 2026
7fdaeb9
fix: add custom service mocks to ui.test.js
radaghastly Feb 18, 2026
a3f7406
feat: add credential encryption at rest (AES-256-GCM) for custom serv…
radaghastly Feb 22, 2026
a0c37ff
feat: encrypt/decrypt credentials in custom service account CRUD
radaghastly Feb 22, 2026
60d6d75
feat: add URL validation and SSRF protection for custom services
radaghastly Feb 22, 2026
274acc8
fix: SSRF proxy protection, DNS pinning, expanded private IP ranges, …
radaghastly Feb 22, 2026
4672fd9
fix: SSRF proxy protection, DNS pinning, expanded private IP ranges, …
radaghastly Feb 22, 2026
24a4f18
fix: SSRF proxy protection, DNS pinning, expanded private IP ranges, …
radaghastly Feb 22, 2026
60c37d1
fix: skip DNS pinning for HTTPS (TLS compat), keep SSRF check
radaghastly Feb 22, 2026
9b63b14
fix: skip DNS pinning for HTTPS (TLS compat), keep SSRF check
radaghastly Feb 22, 2026
985824c
fix: add @eslint/js to devDependencies (fixes CI lint)
radaghastly Feb 22, 2026
0c7c99e
fix: add @eslint/js to devDependencies (fixes CI lint)
radaghastly Feb 22, 2026
795287b
chore: trigger CI
radaghastly Feb 22, 2026
5a7347c
fix: import readOnlyEnforce in index.js after merge with main
radaghastly Feb 24, 2026
c25e380
fix: sync package.json with main (resolve merge conflict)
radaghastly Feb 24, 2026
8d74829
fix: sync package-lock.json with main (resolve merge conflict)
radaghastly Feb 24, 2026
7752023
chore: trigger CI on latest commit
radaghastly Mar 6, 2026
9c9c9dc
fix: sync with main — resolve merge conflicts, add sidecarSecret/rate…
radaghastly Mar 9, 2026
a28da9f
fix: sync with main — resolve merge conflicts, add sidecarSecret/rate…
radaghastly Mar 9, 2026
3d9ca97
fix: sync with main — resolve merge conflicts, add sidecarSecret/rate…
radaghastly Mar 9, 2026
0b743c2
fix: sync with main — resolve merge conflicts, add sidecarSecret/rate…
radaghastly Mar 9, 2026
c030808
fix: sanitize upstream error messages, add design decision comments (…
radaghastly Mar 9, 2026
82b3f91
fix: add TODO comment for HTTPS DNS rebinding TOCTOU gap (PR #283 rev…
radaghastly Mar 9, 2026
3420b56
fix: resolve merge conflict with main in src/index.js
radaghastly Mar 9, 2026
772a9e9
fix: resolve merge conflict with main in package-lock.json
radaghastly Mar 9, 2026
b1c1a81
fix: resolve merge conflict - revert middleware import to ancestor ve…
radaghastly Mar 9, 2026
fc41b2f
fix: import readOnlyEnforce directly in custom-proxy router
radaghastly Mar 9, 2026
a865380
fix: revert package-lock.json version to ancestor to resolve merge co…
radaghastly Mar 9, 2026
24bb13b
fix: reset index.js to ancestor + branch-only changes to resolve merg…
radaghastly Mar 9, 2026
0db04ba
fix(#340): Eliminate HTTPS DNS rebinding TOCTOU gap with pinned agent
luthien-m Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ Configure your agent with the base URL and API key. Agents can use REST or MCP.
## License

ISC

20 changes: 14 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentgate",
"version": "0.12.2",
"version": "0.16.0",
"type": "module",
"description": "API gateway for AI agents with human-in-the-loop write approval",
"main": "src/index.js",
Expand All @@ -12,7 +12,8 @@
},
"files": [
"src/",
"public/"
"public/",
"views/"
],
"repository": {
"type": "git",
Expand Down Expand Up @@ -61,8 +62,10 @@
"ejs": "^3.1.10"
},
"devDependencies": {
"@eslint/js": "^9.0.0",
"eslint": "^9.0.0",
"jest": "^29.7.0",
"supertest": "^7.0.0"
}
}

4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { setupHumanChannelProxy, setAdminTokenValidator } from './routes/channel
import { setupAgentChannelProxy } from './routes/channel-agent.js';
import { validateAdminChatToken } from './routes/ui/keys.js';
import llmRoutes from './routes/llm.js';
import customProxyRoutes from './routes/custom-proxy.js';
import { createMCPPostHandler, createMCPGetHandler, createMCPDeleteHandler } from './routes/mcp.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -100,6 +101,9 @@ app.use('/api/agents/memento', apiKeyAuth, (req, res, next) => {
// LLM proxy - require auth, no read-only enforcement (POST for completions)
app.use('/api/llm', apiKeyAuth, llmRoutes);

// Custom service proxy - require auth
app.use('/api/custom', apiKeyAuth, customProxyRoutes);

// MCP server - Streamable HTTP transport (requires auth)
// POST handles initialization + messages, GET opens optional SSE stream, DELETE terminates session
app.post('/mcp', apiKeyAuth, createMCPPostHandler());
Expand Down
62 changes: 62 additions & 0 deletions src/lib/credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Credential encryption utilities for custom service accounts (#283)
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const IV_LENGTH = 16;
const TAG_LENGTH = 16;
const SALT = 'agentgate-credentials-v1'; // Static salt; key uniqueness comes from the secret

let _cachedKey = null;

function getEncryptionKey() {
if (_cachedKey) return _cachedKey;
const secret = process.env.CREDENTIALS_SECRET || process.env.AGENTGATE_SECRET;
if (!secret) {
throw new Error('CREDENTIALS_SECRET or AGENTGATE_SECRET environment variable must be set to use credential encryption');
}
_cachedKey = scryptSync(secret, SALT, KEY_LENGTH);
return _cachedKey;
}

/**
* Encrypt a plaintext string. Returns a hex-encoded string: iv + tag + ciphertext.
*/
export function encrypt(plaintext) {
const key = getEncryptionKey();
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
// Pack as: iv (16) + tag (16) + ciphertext
return Buffer.concat([iv, tag, encrypted]).toString('hex');
}

/**
* Decrypt a hex-encoded string produced by encrypt().
* Returns the original plaintext.
*/
export function decrypt(hexString) {
const key = getEncryptionKey();
const data = Buffer.from(hexString, 'hex');
const iv = data.subarray(0, IV_LENGTH);
const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
const ciphertext = data.subarray(IV_LENGTH + TAG_LENGTH);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
return decipher.update(ciphertext) + decipher.final('utf8');
}

/**
* Encrypt a credentials object (JSON-serializes then encrypts).
*/
export function encryptCredentials(credentials) {
return encrypt(JSON.stringify(credentials));
}

/**
* Decrypt a credentials string back to an object.
*/
export function decryptCredentials(encryptedHex) {
return JSON.parse(decrypt(encryptedHex));
}
Loading
Loading