aki-pro-max 2.3.3
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/.env.example +34 -0
- package/AICOWORKER-NATIVE-TOOLS.md +60 -0
- package/CLAUDE-LIVE-TOOLS-EVIDENCE.json +94 -0
- package/FULL-TRACE-EVIDENCE.json +109 -0
- package/ISSUE-1-REMOTE.json +1 -0
- package/ISSUE-2-POSTREVIEW.json +1 -0
- package/ISSUE-2-REMOTE.json +1 -0
- package/KEY-ROTATION-EVIDENCE.json +9 -0
- package/LICENSE +21 -0
- package/PMN-9ROUTER-FINAL.md +13 -0
- package/RAPID-BASIL-RECONCILIATION.json +10 -0
- package/README.md +232 -0
- package/RELEASE-ARCHIVE.json +15 -0
- package/SECURITY-RECONCILIATION.json +30 -0
- package/SECURITY.md +16 -0
- package/TEST-ISSUES12-FINAL.txt +0 -0
- package/TEST-ISSUES12-HARNESS.txt +0 -0
- package/VERIFICATION-REPORT.md +58 -0
- package/VERIFIER-ISSUES12-FINAL.txt +0 -0
- package/VERIFY-RELEASE-ISSUES12-FINAL.txt +0 -0
- package/VERIFY-RELEASE-ISSUES12-HARNESS.txt +0 -0
- package/bin/aki-pro-max.js +98 -0
- package/docs/ADMIN-GUI-CONTRACT.md +29 -0
- package/docs/ARCHITECTURE.md +109 -0
- package/docs/CAPABILITY-MATRIX.md +44 -0
- package/docs/CORRELATION-DESIGN.md +226 -0
- package/docs/FAIL-CLOSED-ISSUE-HARNESS.md +21 -0
- package/docs/WEB-SESSION-TRANSPORT-DESIGN.md +423 -0
- package/docs/assets/control-plane.jpg +0 -0
- package/gitleaks-report-all.json +1 -0
- package/gitleaks-report-latest.json +1 -0
- package/gitleaks-report.json +1 -0
- package/package.json +33 -0
- package/scripts/eventual-tool-loop.mjs +55 -0
- package/scripts/install-local.ps1 +35 -0
- package/scripts/live-eventual-multitool.mjs +18 -0
- package/scripts/upgrade-admin-v232.mjs +33 -0
- package/scripts/verify-issue-closure.mjs +81 -0
- package/scripts/verify-release.mjs +31 -0
- package/src/admin-auth.mjs +94 -0
- package/src/admin.mjs +133 -0
- package/src/canonical.mjs +23 -0
- package/src/config.mjs +88 -0
- package/src/correlation-store.mjs +120 -0
- package/src/errors.mjs +18 -0
- package/src/index.mjs +4 -0
- package/src/openai-response.mjs +72 -0
- package/src/openai.mjs +104 -0
- package/src/postman-events.mjs +43 -0
- package/src/postman-request.mjs +49 -0
- package/src/schema.mjs +35 -0
- package/src/server.mjs +73 -0
- package/src/session-store.mjs +48 -0
- package/src/sse.mjs +13 -0
- package/src/transport.mjs +90 -0
- package/src/web-session-events.mjs +358 -0
- package/src/web-session-request.mjs +280 -0
- package/test/9router-executor.integration.test.mjs +207 -0
- package/test/admin-auth.test.mjs +47 -0
- package/test/admin.test.mjs +68 -0
- package/test/config.test.mjs +14 -0
- package/test/contract.test.mjs +14 -0
- package/test/correlation-store.test.mjs +19 -0
- package/test/correlation.integration.test.mjs +48 -0
- package/test/eventual-tool-loop.test.mjs +42 -0
- package/test/fixtures/text.json +8 -0
- package/test/fixtures/tool.json +7 -0
- package/test/fixtures/web-session-observed-done.json +12 -0
- package/test/fixtures/web-session-tool-fragments.json +14 -0
- package/test/full-ingress/alias-loader.mjs +22 -0
- package/test/full-ingress/run-full-ingress.mjs +207 -0
- package/test/full-ingress/seed-9router.mjs +36 -0
- package/test/helpers.mjs +9 -0
- package/test/issue-closure-harness.test.mjs +49 -0
- package/test/model-thinking.test.mjs +20 -0
- package/test/protocol.test.mjs +16 -0
- package/test/request.test.mjs +10 -0
- package/test/session-store.test.mjs +20 -0
- package/test/web-session-builder.test.mjs +124 -0
- package/test/web-session-events.test.mjs +241 -0
- package/test/web-session-integration.test.mjs +103 -0
- package/test/web-session-tools.test.mjs +52 -0
- package/version.json +8 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
|
|
7
|
+
export const EXPECTED = Object.freeze({
|
|
8
|
+
claudePublicModel: 'claude-opus-4-8',
|
|
9
|
+
claudeSelectedModel: 'CLAUDE_OPUS_48_BEDROCK',
|
|
10
|
+
pmnConnectionId: '6706879a-9897-4b2a-9142-728ca64276d4',
|
|
11
|
+
pmnConnectionName: 'Aki Pro Max Local',
|
|
12
|
+
pmnModel: 'gpt-5.6-sol',
|
|
13
|
+
markerCompletionTokens: 4,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function invariant(condition, code) {
|
|
17
|
+
if (!condition) throw new Error(code);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function verifyIssueClosure({ claude, trace }) {
|
|
21
|
+
invariant(claude?.schema === 'live-two-model-tools-evidence/v1', 'claude_schema_invalid');
|
|
22
|
+
invariant(claude?.policy?.retry === false, 'claude_retry_must_be_false');
|
|
23
|
+
invariant(claude?.policy?.failover === false, 'claude_failover_must_be_false');
|
|
24
|
+
invariant(claude?.startedWithPriorCurrentProbeAttempts === 0, 'claude_probe_not_fresh');
|
|
25
|
+
invariant(claude?.allServersCleaned === true, 'claude_cleanup_unproven');
|
|
26
|
+
|
|
27
|
+
const branch = claude.branches?.find((item) => item.publicModel === EXPECTED.claudePublicModel);
|
|
28
|
+
invariant(branch, 'claude_branch_missing');
|
|
29
|
+
invariant(branch.selectedModel === EXPECTED.claudeSelectedModel, 'claude_selected_model_mismatch');
|
|
30
|
+
invariant(branch.initial?.facadeStatus === 200, 'claude_initial_not_200');
|
|
31
|
+
invariant(branch.initial?.toolCallCount === 1, 'claude_tool_call_count_invalid');
|
|
32
|
+
invariant(branch.initial?.exactlyOneValidToolCall === true, 'claude_tool_call_invalid');
|
|
33
|
+
invariant(branch.initial?.argumentsValid === true, 'claude_arguments_invalid');
|
|
34
|
+
invariant(branch.initial?.argumentsDiagnostic?.parsedType === 'object', 'claude_arguments_not_object');
|
|
35
|
+
invariant(branch.initial?.argumentsDiagnostic?.keyCount === 0, 'claude_arguments_not_empty');
|
|
36
|
+
invariant(branch.initial?.argumentsDiagnostic?.parseError === null, 'claude_arguments_parse_error');
|
|
37
|
+
invariant(branch.continuation?.facadeStatus === 200, 'claude_continuation_not_200');
|
|
38
|
+
invariant(branch.finalMatchesMarker === true, 'claude_final_marker_mismatch');
|
|
39
|
+
invariant(branch.terminal === 'completed_match', 'claude_terminal_invalid');
|
|
40
|
+
invariant(branch.error === null, 'claude_branch_error');
|
|
41
|
+
|
|
42
|
+
invariant(Array.isArray(trace?.rows), 'trace_rows_missing');
|
|
43
|
+
invariant(trace?.connections && typeof trace.connections === 'object', 'trace_connections_missing');
|
|
44
|
+
const pmnConnection = trace.connections[EXPECTED.pmnConnectionId];
|
|
45
|
+
invariant(pmnConnection?.name === EXPECTED.pmnConnectionName, 'pmn_connection_mapping_invalid');
|
|
46
|
+
invariant(pmnConnection?.isActive === 1, 'pmn_connection_inactive');
|
|
47
|
+
const cpx = Object.values(trace.connections).find((item) => item?.name === 'CPX');
|
|
48
|
+
invariant(cpx?.isActive === 1, 'cpx_not_active_during_trace');
|
|
49
|
+
|
|
50
|
+
const pmnRows = trace.rows.filter((row) => row.connectionId === EXPECTED.pmnConnectionId);
|
|
51
|
+
invariant(pmnRows.length === 1, 'pmn_row_count_invalid');
|
|
52
|
+
const row = pmnRows[0];
|
|
53
|
+
invariant(row.model === EXPECTED.pmnModel, 'pmn_row_model_invalid');
|
|
54
|
+
invariant(row.status === 'ok', 'pmn_row_not_ok');
|
|
55
|
+
invariant(row.completionTokens === EXPECTED.markerCompletionTokens, 'pmn_marker_token_count_invalid');
|
|
56
|
+
invariant(trace.rows.some((item) => item.connectionId === cpx.id), 'cpx_control_row_missing');
|
|
57
|
+
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
verdict: 'PASS',
|
|
60
|
+
claude: { terminal: branch.terminal, selectedModel: branch.selectedModel },
|
|
61
|
+
pmn: { usageId: row.id, connectionId: row.connectionId, completionTokens: row.completionTokens },
|
|
62
|
+
controls: { retry: false, failover: false, cpxActive: true },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readJson(name) {
|
|
67
|
+
return JSON.parse(fs.readFileSync(path.join(ROOT, name), 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
71
|
+
try {
|
|
72
|
+
const result = verifyIssueClosure({
|
|
73
|
+
claude: readJson('CLAUDE-LIVE-TOOLS-EVIDENCE.json'),
|
|
74
|
+
trace: readJson('FULL-TRACE-EVIDENCE.json'),
|
|
75
|
+
});
|
|
76
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
process.stderr.write(`ISSUE_CLOSURE_REJECTED:${error.message}\n`);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { extname, join, relative, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const root = resolve(new URL('..', import.meta.url).pathname.replace(/^\/(.:)/, '$1'));
|
|
5
|
+
const textExtensions = new Set(['.md','.json','.txt','.mjs','.js','.html','.css','.yaml','.yml','.toml','.env','.example']);
|
|
6
|
+
const forbiddenNames = new Set(['.env']);
|
|
7
|
+
const secretPatterns = [
|
|
8
|
+
['GitHub token', /\bgh[opusr]_[A-Za-z0-9_]{20,}\b/],
|
|
9
|
+
['OpenAI-shaped key', /\bsk-[A-Za-z0-9_-]{16,}\b/],
|
|
10
|
+
['Postman session assignment', new RegExp(['POSTMAN_SESSION','COOKIE=(?!TEST_ONLY_|your_|$)[^\\s]+'].join('_'))],
|
|
11
|
+
];
|
|
12
|
+
const files = [];
|
|
13
|
+
async function walk(dir) { for (const entry of await readdir(dir, { withFileTypes: true })) { if (entry.name === '.git' || entry.name === 'node_modules') continue; const path = join(dir, entry.name); if (entry.isDirectory()) await walk(path); else files.push(path); } }
|
|
14
|
+
await walk(root);
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const name = relative(root, file).replaceAll('\\','/');
|
|
17
|
+
if (forbiddenNames.has(name) || /^\.env\.(?!example$)/.test(name)) throw new Error(`Forbidden credential file: ${name}`);
|
|
18
|
+
const ext = extname(file).toLowerCase();
|
|
19
|
+
if (!textExtensions.has(ext) && !name.endsWith('.env.example')) continue;
|
|
20
|
+
const text = await readFile(file, 'utf8');
|
|
21
|
+
let scannedText = text;
|
|
22
|
+
if (name.startsWith('test/')) {
|
|
23
|
+
scannedText = scannedText.replaceAll(new RegExp(['POSTMAN_SESSION','COOKIE=(?:TEST_ONLY_[A-Z_]+|original|old|session-secret-1234)'].join('_'), 'g'), 'ALLOWLISTED_TEST_FIXTURE');
|
|
24
|
+
scannedText = scannedText.replaceAll(/sk-(?:pmn-test-secret-never-static|client-full-ingress-offline|facade-full-ingress-offline)/g, 'ALLOWLISTED_TEST_KEY');
|
|
25
|
+
}
|
|
26
|
+
if (name === 'scripts/verify-release.mjs') scannedText = scannedText.replaceAll(/POSTMAN_SESSION|COOKIE=/g, 'DETECTOR_SOURCE');
|
|
27
|
+
for (const [label, pattern] of secretPatterns) if (pattern.test(scannedText)) throw new Error(`${label} pattern in ${name}`);
|
|
28
|
+
}
|
|
29
|
+
const canary = ['sk', 'CANARY_REAL_LOOKING_TOKEN_123456789'].join('-');
|
|
30
|
+
if (!secretPatterns.some(([, pattern]) => pattern.test(canary))) throw new Error('Secret detector positive control failed');
|
|
31
|
+
console.log(JSON.stringify({ release: 'aki-pro-max', files: files.length, secretScan: 'pass', positiveControl: 'pass' }));
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { access, copyFile, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const SUBDOMAIN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
7
|
+
const VISIBLE_ASCII = /^[\x21-\x7e]+$/;
|
|
8
|
+
|
|
9
|
+
function required(value, field, max = 256) {
|
|
10
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`${field} is required`);
|
|
11
|
+
const text = value.trim();
|
|
12
|
+
if (text.length > max) throw new Error(`${field} exceeds ${max} characters`);
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateAuthInput(input) {
|
|
17
|
+
if (!input || typeof input !== 'object') throw new Error('auth payload is required');
|
|
18
|
+
const sessionCookie = required(input.sessionCookie, 'postman.sid', 4096);
|
|
19
|
+
if (!VISIBLE_ASCII.test(sessionCookie) || /[;,\\]/.test(sessionCookie)) throw new Error('postman.sid contains unsupported characters');
|
|
20
|
+
const subdomain = required(input.subdomain, 'team subdomain', 63).toLowerCase();
|
|
21
|
+
if (!SUBDOMAIN.test(subdomain) || subdomain.startsWith('xn--')) throw new Error('team subdomain is invalid');
|
|
22
|
+
const workspaceId = required(input.workspaceId, 'workspace ID', 256);
|
|
23
|
+
if (!VISIBLE_ASCII.test(workspaceId)) throw new Error('workspace ID must be visible ASCII');
|
|
24
|
+
const selectedModel = required(input.selectedModel, 'selected upstream model', 128);
|
|
25
|
+
if (!VISIBLE_ASCII.test(selectedModel)) throw new Error('selected upstream model must be visible ASCII');
|
|
26
|
+
return Object.freeze({ sessionCookie, subdomain, workspaceId, selectedModel });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function authStatus(config) {
|
|
30
|
+
return {
|
|
31
|
+
configured: Boolean(config.webSessionCookie),
|
|
32
|
+
transportStrategy: config.transportStrategy,
|
|
33
|
+
subdomain: config.webSessionSubdomain || null,
|
|
34
|
+
workspaceId: config.workspaceId || null,
|
|
35
|
+
selectedModel: config.selectedUpstreamModel || null,
|
|
36
|
+
cookie: config.webSessionCookie ? { present: true, masked: `••••${config.webSessionCookie.slice(-4)}` } : { present: false, masked: null },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function exists(path) {
|
|
41
|
+
try { await access(path, constants.F_OK); return true; } catch { return false; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function discoverPostmanDesktop(env = process.env) {
|
|
45
|
+
const roaming = env.APPDATA ? resolve(env.APPDATA, 'Postman') : null;
|
|
46
|
+
const local = env.LOCALAPPDATA ? resolve(env.LOCALAPPDATA, 'Postman') : null;
|
|
47
|
+
const executable = local ? resolve(local, 'Postman.exe') : null;
|
|
48
|
+
const profileDetected = Boolean(roaming && await exists(roaming));
|
|
49
|
+
const installed = Boolean(executable && await exists(executable));
|
|
50
|
+
return {
|
|
51
|
+
installed,
|
|
52
|
+
profileDetected,
|
|
53
|
+
sources: [
|
|
54
|
+
...(installed ? [{ type: 'desktop-install', path: executable }] : []),
|
|
55
|
+
...(profileDetected ? [{ type: 'chromium-profile', path: roaming }] : []),
|
|
56
|
+
],
|
|
57
|
+
sessionAutoImportAvailable: false,
|
|
58
|
+
manualCredentialRequired: true,
|
|
59
|
+
reason: profileDetected ? 'Postman Chromium session storage is protected; this control plane will not decrypt or extract postman.sid automatically.' : 'No readable Postman Desktop profile was detected.',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function replaceEnvLine(text, key, value) {
|
|
64
|
+
const line = `${key}=${value}`;
|
|
65
|
+
const expression = new RegExp(`^${key}=.*$`, 'm');
|
|
66
|
+
if (expression.test(text)) return text.replace(expression, line);
|
|
67
|
+
return `${text.replace(/\s*$/, '')}\n${line}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function saveAuthConfig(input, options = {}) {
|
|
71
|
+
const validated = validateAuthInput(input);
|
|
72
|
+
const envFile = resolve(options.envFile || resolve(process.cwd(), '.env'));
|
|
73
|
+
let original = await readFile(envFile, 'utf8');
|
|
74
|
+
let updated = original;
|
|
75
|
+
updated = replaceEnvLine(updated, 'POSTMAN_TRANSPORT_STRATEGY', 'web_session');
|
|
76
|
+
updated = replaceEnvLine(updated, 'POSTMAN_SESSION_COOKIE', validated.sessionCookie);
|
|
77
|
+
updated = replaceEnvLine(updated, 'POSTMAN_WORKSPACE_SUBDOMAIN', validated.subdomain);
|
|
78
|
+
updated = replaceEnvLine(updated, 'POSTMAN_WORKSPACE_ID', validated.workspaceId);
|
|
79
|
+
updated = replaceEnvLine(updated, 'POSTMAN_WEB_SESSION_SELECTED_MODEL', validated.selectedModel);
|
|
80
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
81
|
+
const backupFile = `${envFile}.bak-${stamp}`;
|
|
82
|
+
const temporary = resolve(dirname(envFile), `.env.auth-${process.pid}-${randomBytes(5).toString('hex')}.tmp`);
|
|
83
|
+
await copyFile(envFile, backupFile);
|
|
84
|
+
await import('node:fs/promises').then(fs => fs.chmod(backupFile, 0o600));
|
|
85
|
+
try {
|
|
86
|
+
await writeFile(temporary, updated, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
87
|
+
if (options.beforeRename) await options.beforeRename({ envFile, backupFile, temporary });
|
|
88
|
+
await rename(temporary, envFile);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
try { await rename(backupFile, envFile); } catch {}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return { saved: true, restartRequired: true, backupFile, fields: { subdomain: validated.subdomain, workspaceId: validated.workspaceId, selectedModel: validated.selectedModel } };
|
|
94
|
+
}
|
package/src/admin.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { authStatus, discoverPostmanDesktop, saveAuthConfig, validateAuthInput } from './admin-auth.mjs';
|
|
2
|
+
|
|
3
|
+
const LOOPBACK_V4 = /^127(?:\.\d{1,3}){3}$/;
|
|
4
|
+
|
|
5
|
+
export function isLoopbackAddress(value) {
|
|
6
|
+
if (typeof value !== 'string') return false;
|
|
7
|
+
const address = value.toLowerCase().startsWith('::ffff:') ? value.slice(7) : value;
|
|
8
|
+
if (address === '::1') return true;
|
|
9
|
+
if (!LOOPBACK_V4.test(address)) return false;
|
|
10
|
+
return address.split('.').every(part => Number(part) >= 0 && Number(part) <= 255);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isAllowedAdminHost(value, localPort) {
|
|
14
|
+
if (typeof value !== 'string' || !value) return false;
|
|
15
|
+
const allowed = new Set([
|
|
16
|
+
`localhost:${localPort}`,
|
|
17
|
+
`127.0.0.1:${localPort}`,
|
|
18
|
+
`[::1]:${localPort}`,
|
|
19
|
+
]);
|
|
20
|
+
return allowed.has(value.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function assertLocalAdminRequest(req) {
|
|
24
|
+
// Deliberately ignore X-Forwarded-For: the socket peer is the security boundary.
|
|
25
|
+
return isLoopbackAddress(req.socket?.remoteAddress) && isAllowedAdminHost(req.headers?.host, req.socket?.localPort);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ADMIN_HTML = `<!doctype html>
|
|
29
|
+
<html lang="vi"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Aki Pro Max Control Plane</title><link rel="stylesheet" href="/admin/app.css"></head>
|
|
30
|
+
<body><main class="shell"><section id="updateBanner" class="update hidden" aria-live="polite"><div><span class="pill">UPDATE AVAILABLE</span><h2 id="updateTitle">Aki Pro Max update available</h2><p id="updateCopy">Checking release status…</p></div><div class="update-actions"><button id="copyUpdate">Copy update command</button><button id="dismissUpdate">Later</button></div><ol id="updateSteps"><li>Stop the active proxy PID completely.</li><li>Open PowerShell and run the update command.</li><li>Click Reload after the command completes to start a new proxy PID.</li></ol><code id="updateCommand"></code><button id="reloadProxy">Reload Control Plane</button></section><header><div><span class="pill">LOCAL CONTROL PLANE</span><h1>Aki Pro Max <em>Control</em></h1><p>Credentials, runtime health và OpenAI-compatible connection.</p></div><div id="health" class="status">Đang kiểm tra…</div></header>
|
|
31
|
+
<section class="grid metrics"><article><label>Facade</label><strong id="baseUrl">—</strong><button data-copy="baseUrl">Copy URL</button></article><article><label>Transport</label><strong id="transport">—</strong><small id="workspace">—</small></article><article><label>Models</label><strong id="modelCount">—</strong><small>Public aliases</small></article></section>
|
|
32
|
+
<section class="grid main"><article class="credentials"><div class="title"><div><label>Provider API key</label><h2>Credentials</h2></div><span class="safe">Loopback only</span></div><div class="secret"><code id="apiKey">••••••••••••••••••••••••</code><button id="reveal">Reveal</button><button id="copyKey" disabled>Copy key</button></div><p class="hint">Key chỉ được tải sau khi bấm Reveal. Không nằm trong HTML, JS, health response hoặc browser storage.</p></article>
|
|
33
|
+
<article><label>Client setup</label><h2>OpenAI-compatible</h2><pre id="snippet">—</pre><button data-copy="snippet">Copy config</button></article></section>
|
|
34
|
+
<section><div class="title"><div><label>Upstream authentication</label><h2>Postman Session</h2></div><span id="authState" class="safe">Loading…</span></div><div class="auth-grid"><label>postman.sid<input id="sessionCookie" type="password" autocomplete="off" placeholder="Paste session cookie"></label><label>Team subdomain<input id="subdomain" autocomplete="off" placeholder="team-name"></label><label>Workspace ID<input id="workspaceId" autocomplete="off" placeholder="UUID"></label><label>Selected upstream model<input id="selectedModel" autocomplete="off" placeholder="GPT_56_SOL"></label></div><div class="actions"><button id="discoverAuth">Auto Discover</button><button id="validateAuth">Test fields</button><button id="applyAuth">Save auth</button></div><p id="authMessage" class="hint">Stored cookie is never returned. Auto discovery detects Postman Desktop but does not decrypt protected Chromium session storage.</p></section>
|
|
35
|
+
<section><div class="title"><div><label>Available models</label><h2>Catalog</h2></div></div><div id="models" class="models"></div></section>
|
|
36
|
+
<footer><span>Bound to local machine</span><span id="healthBoundary">upstream not probed yet</span></footer></main><script src="/admin/app.js"></script></body></html>`;
|
|
37
|
+
|
|
38
|
+
const ADMIN_CSS = `:root{color-scheme:dark;--bg:#070b14;--panel:#101827;--line:#273449;--text:#f8fafc;--muted:#94a3b8;--orange:#fb923c;--green:#34d399}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#4a210f 0,transparent 32%),radial-gradient(circle at 90% 90%,#18205a 0,transparent 32%),var(--bg);font:14px Inter,system-ui,sans-serif;color:var(--text);min-height:100vh}.shell{max-width:1100px;margin:auto;padding:42px 20px}header{display:flex;justify-content:space-between;gap:24px;align-items:center;margin-bottom:22px}h1{font-size:48px;letter-spacing:-.05em;margin:10px 0 5px}h1 em{font-style:normal;color:var(--orange);font-weight:500}h2{margin:5px 0 16px;font-size:22px}p,.hint,small{color:var(--muted)}.pill,.safe{display:inline-block;border:1px solid #f9731655;background:#f9731620;color:#fdba74;border-radius:999px;padding:6px 10px;font-size:10px;font-weight:800;letter-spacing:.1em}.safe{color:#6ee7b7;border-color:#10b98155;background:#10b98120}.status{border:1px solid var(--line);padding:11px 15px;border-radius:14px;background:#0b1220}.status.up{color:#6ee7b7}.update{border-color:#f97316;background:linear-gradient(135deg,#351c0b,#111827);position:relative}.update.hidden{display:none}.update h2{margin:8px 0}.update p{margin:6px 0}.update-actions{position:absolute;right:20px;top:20px;display:flex;gap:8px}.update ol{margin:15px 0 12px;padding-left:20px;color:#cbd5e1;line-height:1.7}.update code{display:block;padding:10px;border-radius:9px;background:#020617;color:#fde68a;word-break:break-all}.update #reloadProxy{margin-top:10px;border-color:#fb923c;background:#c2410c}@media(max-width:760px){.update-actions{position:static;margin-top:10px}}.grid{display:grid;gap:14px}.metrics{grid-template-columns:2fr 1fr 1fr}.main{grid-template-columns:1.15fr .85fr;margin-top:14px}article,section{background:#0f172acc;border:1px solid var(--line);border-radius:18px;padding:20px}section{margin-top:14px}section article{padding:0;border:0;background:none}label{display:block;text-transform:uppercase;letter-spacing:.13em;font-size:10px;color:var(--muted)}strong{display:block;font-size:18px;margin:10px 0;word-break:break-all}.title{display:flex;justify-content:space-between;align-items:start}.secret{display:flex;gap:8px;align-items:center;background:#020617;padding:12px;border-radius:12px;border:1px solid #1e293b}.secret code{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#fde68a}button{border:1px solid #334155;color:#e2e8f0;background:#172033;padding:8px 11px;border-radius:9px;cursor:pointer}button:hover{border-color:var(--orange)}button:disabled{opacity:.45;cursor:not-allowed}pre{white-space:pre-wrap;background:#020617;padding:12px;border-radius:12px;border:1px solid #1e293b;color:#a7f3d0;font:12px ui-monospace,monospace;min-height:96px}.models{display:grid;grid-template-columns:repeat(4,1fr);gap:9px}.model{padding:10px;border-radius:11px;border:1px solid #26344a;background:#0a1120;font:11px ui-monospace,monospace;color:#cbd5e1}.auth-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.auth-grid label{font-size:10px}.auth-grid input{display:block;width:100%;margin-top:7px;padding:11px;border-radius:10px;border:1px solid #334155;background:#020617;color:#f8fafc;font:13px ui-monospace,monospace}.actions{display:flex;gap:9px;margin-top:14px}footer{display:flex;justify-content:space-between;color:#64748b;font-size:11px;padding:18px 4px}@media(max-width:760px){header{align-items:flex-start;flex-direction:column}.metrics,.main,.models,.auth-grid{grid-template-columns:1fr}h1{font-size:38px}.secret{align-items:stretch;flex-direction:column}}`;
|
|
39
|
+
|
|
40
|
+
const ADMIN_JS = `'use strict';
|
|
41
|
+
const $=id=>document.getElementById(id); let secret=''; const semver=v=>String(v||'0.0.0').replace(/^v/,'').split('.').map(n=>Number(n)||0); const newer=(a,b)=>{const x=semver(a),y=semver(b);return x.some((n,i)=>n>(y[i]||0)&&x.slice(0,i).every((m,j)=>m===(y[j]||0)))};
|
|
42
|
+
const copy=async text=>{await navigator.clipboard.writeText(text)};
|
|
43
|
+
async function init(){
|
|
44
|
+
const [health,config]=await Promise.all([fetch('/health').then(r=>r.json()),fetch('/admin/config',{cache:'no-store'}).then(r=>{if(!r.ok)throw new Error('Admin config '+r.status);return r.json()})]);
|
|
45
|
+
$('health').textContent=health.status==='ok'?'UP · '+config.port:'DEGRADED'; $('health').classList.add(health.status==='ok'?'up':'');
|
|
46
|
+
$('baseUrl').textContent=config.baseUrl; $('transport').textContent=config.transportStrategy; $('workspace').textContent='Workspace '+config.workspaceId; $('modelCount').textContent=String(config.models.length); $('models').replaceChildren(...config.models.map(id=>{const d=document.createElement('div');d.className='model';d.textContent=id;return d}));
|
|
47
|
+
$('snippet').textContent=JSON.stringify({baseURL:config.baseUrl,apiKey:'<reveal-and-copy-from-this-page>',model:config.models[0]},null,2); $('healthBoundary').textContent=health.upstream_verified?'upstream verified':'facade healthy · upstream not verified by /health'; await checkUpdates(config);
|
|
48
|
+
}
|
|
49
|
+
$('reveal').addEventListener('click',async()=>{const r=await fetch('/admin/credentials',{method:'POST',headers:{'content-type':'application/json'},body:'{}',cache:'no-store'});if(!r.ok)throw new Error('Credentials '+r.status);secret=(await r.json()).apiKey;$('apiKey').textContent=secret;$('copyKey').disabled=false;$('reveal').textContent='Hide';$('reveal').onclick=()=>{const shown=$('apiKey').textContent===secret;$('apiKey').textContent=shown?'••••••••••••••••••••••••':secret;$('reveal').textContent=shown?'Show':'Hide'}});
|
|
50
|
+
const authPayload=()=>({sessionCookie:$('sessionCookie').value,subdomain:$('subdomain').value,workspaceId:$('workspaceId').value,selectedModel:$('selectedModel').value});
|
|
51
|
+
async function loadAuth(){const s=await fetch('/admin/auth/status',{cache:'no-store'}).then(r=>r.json());$('authState').textContent=s.configured?'Configured '+s.cookie.masked:'Not configured';$('subdomain').value=s.subdomain||'';$('workspaceId').value=s.workspaceId||'';$('selectedModel').value=s.selectedModel||'';}
|
|
52
|
+
$('discoverAuth').addEventListener('click',async()=>{const d=await fetch('/admin/auth/discover',{method:'POST',headers:{'content-type':'application/json'},body:'{}'}).then(r=>r.json());$('authMessage').textContent=d.installed?(d.reason+' Manual cookie entry remains required.'):'Postman Desktop was not detected.'});
|
|
53
|
+
$('validateAuth').addEventListener('click',async()=>{const r=await fetch('/admin/auth/validate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(authPayload())});const d=await r.json();$('authMessage').textContent=r.ok?'Valid fields · cookie '+d.cookie.masked:d.error.message});
|
|
54
|
+
$('applyAuth').addEventListener('click',async()=>{const r=await fetch('/admin/auth/apply',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(authPayload())});const d=await r.json();$('authMessage').textContent=r.ok?'Saved safely. Restart provider to activate. Backup: '+d.backupFile:d.error.message;if(r.ok){$('sessionCookie').value='';await loadAuth()}});
|
|
55
|
+
async function checkUpdates(config){const url=config.updateManifestUrl;if(!url)return;try{const c=new AbortController();const t=setTimeout(()=>c.abort(),3000);const m=await fetch(url,{cache:'no-store',signal:c.signal}).then(r=>r.ok?r.json():null);clearTimeout(t);if(!m||!newer(m.latest,config.currentVersion)||localStorage.getItem('aki-update-dismissed')===m.latest)return;$('updateBanner').classList.remove('hidden');$('updateTitle').textContent='Aki Pro Max '+m.latest+' is available';$('updateCopy').textContent='Current version: '+config.currentVersion+'. Update manually to avoid interrupting an active request.';$('updateCommand').textContent=m.updateCommand||'npm update -g '+(m.installPackage||'aki-pro-max');$('copyUpdate').onclick=()=>copy($('updateCommand').textContent);$('dismissUpdate').onclick=()=>{localStorage.setItem('aki-update-dismissed',m.latest);$('updateBanner').classList.add('hidden')};$('reloadProxy').onclick=()=>location.reload()}catch(e){console.info('Update check skipped:',e.message)}}$('copyKey').addEventListener('click',()=>copy(secret)); document.querySelectorAll('[data-copy]').forEach(b=>b.addEventListener('click',()=>copy($(b.dataset.copy).textContent))); Promise.all([init(),loadAuth()]).catch(e=>{$('health').textContent='ERROR';console.error(e)});`;
|
|
56
|
+
|
|
57
|
+
const securityHeaders = Object.freeze({
|
|
58
|
+
'cache-control': 'no-store',
|
|
59
|
+
'x-content-type-options': 'nosniff',
|
|
60
|
+
'referrer-policy': 'no-referrer',
|
|
61
|
+
'cross-origin-resource-policy': 'same-origin',
|
|
62
|
+
'content-security-policy': "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'; form-action 'none'",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function send(res, status, contentType, body, extra = {}) {
|
|
66
|
+
res.writeHead(status, { ...securityHeaders, 'content-type': contentType, ...extra });
|
|
67
|
+
res.end(body);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sameOrigin(req) {
|
|
71
|
+
const origin = req.headers.origin;
|
|
72
|
+
const expectedOrigin = `http://${req.headers.host}`;
|
|
73
|
+
const fetchSite = req.headers['sec-fetch-site'];
|
|
74
|
+
return (!origin || origin === expectedOrigin) && (!fetchSite || fetchSite === 'same-origin');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readJson(req, maxBytes = 8192) {
|
|
78
|
+
const chunks = []; let bytes = 0;
|
|
79
|
+
for await (const chunk of req) {
|
|
80
|
+
bytes += chunk.length;
|
|
81
|
+
if (bytes > maxBytes) throw new Error('request body too large');
|
|
82
|
+
chunks.push(chunk);
|
|
83
|
+
}
|
|
84
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
85
|
+
return text ? JSON.parse(text) : {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function handleAdminRequest(req, res, config, pathname) {
|
|
89
|
+
if (!pathname.startsWith('/admin')) return false;
|
|
90
|
+
if (!assertLocalAdminRequest(req)) {
|
|
91
|
+
send(res, 403, 'application/json; charset=utf-8', JSON.stringify({ error: { message: 'Local admin access denied.', code: 'admin_local_only' } }));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (pathname === '/admin' && req.method === 'GET') send(res, 200, 'text/html; charset=utf-8', ADMIN_HTML);
|
|
95
|
+
else if (pathname === '/admin/app.css' && req.method === 'GET') send(res, 200, 'text/css; charset=utf-8', ADMIN_CSS);
|
|
96
|
+
else if (pathname === '/admin/app.js' && req.method === 'GET') send(res, 200, 'text/javascript; charset=utf-8', ADMIN_JS);
|
|
97
|
+
else if (pathname === '/admin/config' && req.method === 'GET') send(res, 200, 'application/json; charset=utf-8', JSON.stringify({
|
|
98
|
+
baseUrl: `http://127.0.0.1:${req.socket.localPort}/v1`,
|
|
99
|
+
port: req.socket.localPort,
|
|
100
|
+
transportStrategy: config.transportStrategy,
|
|
101
|
+
workspaceId: config.workspaceId,
|
|
102
|
+
models: config.models,
|
|
103
|
+
currentVersion: '2.3.3',
|
|
104
|
+
updateManifestUrl: process.env.AKI_UPDATE_MANIFEST_URL || 'https://raw.githubusercontent.com/khangtudo/aki-pro-max/main/version.json',
|
|
105
|
+
}));
|
|
106
|
+
else if (pathname === '/admin/credentials' && req.method === 'POST') {
|
|
107
|
+
if (!sameOrigin(req)) send(res, 403, 'application/json; charset=utf-8', JSON.stringify({ error: { message: 'Credential reveal origin denied.', code: 'admin_origin_denied' } }));
|
|
108
|
+
else send(res, 200, 'application/json; charset=utf-8', JSON.stringify({ apiKey: config.apiKey }));
|
|
109
|
+
}
|
|
110
|
+
else if (pathname === '/admin/auth/status' && req.method === 'GET') send(res, 200, 'application/json; charset=utf-8', JSON.stringify(authStatus(config)));
|
|
111
|
+
else if (pathname === '/admin/auth/discover' && req.method === 'POST') {
|
|
112
|
+
if (!sameOrigin(req)) send(res, 403, 'application/json; charset=utf-8', JSON.stringify({ error: { message: 'Auth discovery origin denied.', code: 'admin_origin_denied' } }));
|
|
113
|
+
else send(res, 200, 'application/json; charset=utf-8', JSON.stringify(await discoverPostmanDesktop()));
|
|
114
|
+
}
|
|
115
|
+
else if (pathname === '/admin/auth/validate' && req.method === 'POST') {
|
|
116
|
+
if (!sameOrigin(req)) send(res, 403, 'application/json; charset=utf-8', JSON.stringify({ error: { message: 'Auth validation origin denied.', code: 'admin_origin_denied' } }));
|
|
117
|
+
else {
|
|
118
|
+
try { const fields = validateAuthInput(await readJson(req)); send(res, 200, 'application/json; charset=utf-8', JSON.stringify({ valid: true, fields: { subdomain: fields.subdomain, workspaceId: fields.workspaceId, selectedModel: fields.selectedModel }, cookie: { present: true, masked: `••••${fields.sessionCookie.slice(-4)}` } })); }
|
|
119
|
+
catch (error) { send(res, 400, 'application/json; charset=utf-8', JSON.stringify({ error: { message: error.message, code: 'invalid_auth_config' } })); }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (pathname === '/admin/auth/apply' && req.method === 'POST') {
|
|
123
|
+
if (!sameOrigin(req)) send(res, 403, 'application/json; charset=utf-8', JSON.stringify({ error: { message: 'Auth apply origin denied.', code: 'admin_origin_denied' } }));
|
|
124
|
+
else {
|
|
125
|
+
try { const result = await saveAuthConfig(await readJson(req)); send(res, 200, 'application/json; charset=utf-8', JSON.stringify(result)); }
|
|
126
|
+
catch (error) { send(res, 400, 'application/json; charset=utf-8', JSON.stringify({ error: { message: error.message, code: 'auth_apply_failed' } })); }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else send(res, 404, 'application/json; charset=utf-8', JSON.stringify({ error: { message: 'Admin route not found.', code: 'not_found' } }));
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const adminStaticAssets = Object.freeze({ html: ADMIN_HTML, css: ADMIN_CSS, js: ADMIN_JS });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function ordered(value) {
|
|
4
|
+
if (Array.isArray(value)) return value.map(ordered);
|
|
5
|
+
if (value && typeof value === 'object') {
|
|
6
|
+
const output = {};
|
|
7
|
+
for (const key of Object.keys(value).sort()) output[key] = ordered(value[key]);
|
|
8
|
+
return output;
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function canonicalString(value) {
|
|
14
|
+
return JSON.stringify({ version: 1, value: ordered(value) });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function canonicalHash(value) {
|
|
18
|
+
return createHash('sha256').update(canonicalString(value), 'utf8').digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function derivePrincipalId(serverKey, credentialSlotId) {
|
|
22
|
+
return createHmac('sha256', serverKey).update(credentialSlotId, 'utf8').digest('hex');
|
|
23
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { timingSafeEqual, randomBytes } from 'node:crypto';
|
|
2
|
+
import { ProviderError, invalid } from './errors.mjs';
|
|
3
|
+
import { derivePrincipalId } from './canonical.mjs';
|
|
4
|
+
const int = (value, fallback, min, max) => { const n = Number(value ?? fallback); if (!Number.isSafeInteger(n) || n < min || n > max) throw new Error(`Invalid integer configuration: ${value}`); return n; };
|
|
5
|
+
const exactOrigin = (value) => { const url = new URL(value); if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') throw new Error('POSTMAN_ORIGIN must be an exact HTTPS origin with no path, credentials, query, or fragment.'); return url.origin; };
|
|
6
|
+
const list = (value) => String(value || '').split(',').map(v => v.trim()).filter(Boolean);
|
|
7
|
+
const opaqueToken = (value, field, fallback, max) => { if (value !== undefined && typeof value !== 'string') throw new Error(`${field} must be a string when configured.`); const text = value === undefined || value === '' ? fallback : value; if (text === undefined) return undefined; if (!text.length || text.length > max || !/^[\x21-\x7E]+$/.test(text)) throw new Error(`${field} must be ${max} or fewer visible ASCII characters with no whitespace.`); return text; };
|
|
8
|
+
const strategyValue = env => {
|
|
9
|
+
const configured = env.POSTMAN_TRANSPORT_STRATEGY;
|
|
10
|
+
if (configured === undefined || configured === '') {
|
|
11
|
+
if (env.POSTMAN_SESSION_COOKIE !== undefined || env.POSTMAN_WORKSPACE_SUBDOMAIN !== undefined) throw new Error('POSTMAN_TRANSPORT_STRATEGY is required when web-session configuration is present.');
|
|
12
|
+
return { strategy: 'access_token', explicit: false };
|
|
13
|
+
}
|
|
14
|
+
if (configured !== 'access_token' && configured !== 'web_session') throw new Error('POSTMAN_TRANSPORT_STRATEGY must be access_token or web_session.');
|
|
15
|
+
return { strategy: configured, explicit: true };
|
|
16
|
+
};
|
|
17
|
+
const present = value => value !== undefined && value !== '';
|
|
18
|
+
const requiredString = (value, field) => { if (typeof value !== 'string' || value === '') throw new Error(`${field} is required.`); return value; };
|
|
19
|
+
const sessionCookie = value => {
|
|
20
|
+
const text = requiredString(value, 'POSTMAN_SESSION_COOKIE');
|
|
21
|
+
if (Buffer.byteLength(text, 'utf8') > 4096 || !/^[\x21-\x7E]+$/.test(text) || /[;,\\]/.test(text)) throw new Error('POSTMAN_SESSION_COOKIE must be 1-4096 visible ASCII bytes and contain no whitespace, controls, semicolon, comma, or backslash.');
|
|
22
|
+
return text;
|
|
23
|
+
};
|
|
24
|
+
const workspaceSubdomain = value => {
|
|
25
|
+
const text = requiredString(value, 'POSTMAN_WORKSPACE_SUBDOMAIN');
|
|
26
|
+
if (text.length > 63 || text.startsWith('xn--') || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(text)) throw new Error('POSTMAN_WORKSPACE_SUBDOMAIN must be one non-punycode lowercase ASCII DNS label of 1-63 characters.');
|
|
27
|
+
return text;
|
|
28
|
+
};
|
|
29
|
+
const workspaceIdentifier = value => opaqueToken(value, 'POSTMAN_WORKSPACE_ID', undefined, 256);
|
|
30
|
+
const optionalHeaderValue = (value, field, max) => present(value) ? opaqueToken(value, field, undefined, max) : undefined;
|
|
31
|
+
const excludedTools = value => { if (value === undefined || value === '') return Object.freeze([]); if (typeof value !== 'string') throw new Error('POSTMAN_EXCLUDED_TOOLS must be a comma-separated string when configured.'); const raw = value.split(','); if (raw.length > 64) throw new Error('POSTMAN_EXCLUDED_TOOLS must contain at most 64 entries.'); const values = raw.map(value => value.trim()); if (values.some(value => !/^[A-Za-z0-9_.:-]{1,128}$/.test(value))) throw new Error('POSTMAN_EXCLUDED_TOOLS entries must be 1-128 tool-name characters.'); if (new Set(values).size !== values.length) throw new Error('POSTMAN_EXCLUDED_TOOLS must not contain duplicates.'); return Object.freeze(values); };
|
|
32
|
+
const observedModels = value => { if (value === undefined || value === '') return Object.freeze([]); if (typeof value !== 'string') throw new Error('POSTMAN_WEB_SESSION_OBSERVED_MODELS must be a comma-separated string when configured.'); const values = value.split(',').map(value => value.trim()); if (values.length > 16 || values.some(value => !value || Buffer.byteLength(value, 'utf8') > 128 || !/^[\x21-\x7E]+$/.test(value))) throw new Error('POSTMAN_WEB_SESSION_OBSERVED_MODELS must contain at most 16 bounded visible-ASCII labels.'); if (new Set(values).size !== values.length) throw new Error('POSTMAN_WEB_SESSION_OBSERVED_MODELS must not contain duplicates.'); return Object.freeze(values); };
|
|
33
|
+
export function loadConfig(env = process.env) {
|
|
34
|
+
const models = list(env.PROVIDER_MODELS); if (!models.length) throw new Error('PROVIDER_MODELS must contain at least one explicit model ID.');
|
|
35
|
+
const apiKey = String(env.PROVIDER_API_KEY || ''); if (!apiKey) throw new Error('PROVIDER_API_KEY is required.');
|
|
36
|
+
const { strategy, explicit } = strategyValue(env);
|
|
37
|
+
const workspaceId = workspaceIdentifier(env.POSTMAN_WORKSPACE_ID); if (!workspaceId) throw new Error('POSTMAN_WORKSPACE_ID is required.');
|
|
38
|
+
let upstreamToken, upstreamOrigin, chatPath, webSessionCookie, webSessionSubdomain, selectedUpstreamModel, webSessionAppVersion, webSessionObservedModels = Object.freeze([]);
|
|
39
|
+
if (strategy === 'access_token') {
|
|
40
|
+
if (present(env.POSTMAN_SESSION_COOKIE) || present(env.POSTMAN_WORKSPACE_SUBDOMAIN) || present(env.POSTMAN_WEB_SESSION_SELECTED_MODEL)) throw new Error('access_token forbids web-session configuration.');
|
|
41
|
+
upstreamToken = requiredString(env.POSTMAN_ACCESS_TOKEN, 'POSTMAN_ACCESS_TOKEN');
|
|
42
|
+
upstreamOrigin = exactOrigin(env.POSTMAN_ORIGIN);
|
|
43
|
+
chatPath = env.POSTMAN_CHAT_PATH || '/chat';
|
|
44
|
+
} else {
|
|
45
|
+
if (present(env.POSTMAN_ACCESS_TOKEN) || present(env.POSTMAN_ORIGIN) || present(env.POSTMAN_CHAT_PATH)) throw new Error('web_session forbids POSTMAN_ACCESS_TOKEN, POSTMAN_ORIGIN, and POSTMAN_CHAT_PATH.');
|
|
46
|
+
webSessionCookie = sessionCookie(env.POSTMAN_SESSION_COOKIE);
|
|
47
|
+
webSessionSubdomain = workspaceSubdomain(env.POSTMAN_WORKSPACE_SUBDOMAIN);
|
|
48
|
+
selectedUpstreamModel = opaqueToken(env.POSTMAN_WEB_SESSION_SELECTED_MODEL, 'POSTMAN_WEB_SESSION_SELECTED_MODEL', undefined, 128);
|
|
49
|
+
if (!selectedUpstreamModel) throw new Error('POSTMAN_WEB_SESSION_SELECTED_MODEL is required.');
|
|
50
|
+
webSessionAppVersion = optionalHeaderValue(env.POSTMAN_APP_VERSION, 'POSTMAN_APP_VERSION', 128);
|
|
51
|
+
webSessionObservedModels = observedModels(env.POSTMAN_WEB_SESSION_OBSERVED_MODELS);
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
host: env.PROVIDER_HOST || '127.0.0.1', port: int(env.PROVIDER_PORT, 0, 0, 65535), apiKey, transportStrategy: strategy, transportStrategyExplicit: explicit, upstreamToken, workspaceId,
|
|
55
|
+
upstreamOrigin, chatPath, appVersion: strategy === 'access_token' ? (env.POSTMAN_APP_VERSION || 'offline-adapter/0.1.0') : undefined,
|
|
56
|
+
webSessionCookie, webSessionSubdomain, selectedUpstreamModel, webSessionAppVersion, webSessionObservedModels,
|
|
57
|
+
platform: opaqueToken(env.POSTMAN_PLATFORM, 'POSTMAN_PLATFORM', 'OPENAI_COMPATIBLE_ADAPTER', 64), nativeToolsHash: opaqueToken(env.POSTMAN_NATIVE_TOOLS_HASH, 'POSTMAN_NATIVE_TOOLS_HASH', undefined, 256), excludedTools: excludedTools(env.POSTMAN_EXCLUDED_TOOLS),
|
|
58
|
+
models: Object.freeze(models), timeoutMs: int(env.REQUEST_TIMEOUT_MS, 120000, 100, 600000), maxBodyBytes: int(env.MAX_BODY_BYTES, 104857600, 1024, 209715200),
|
|
59
|
+
maxEventBytes: int(env.MAX_EVENT_BYTES, 1048576, 1024, 8388608), maxOutputBytes: int(env.MAX_OUTPUT_BYTES, 4194304, 1024, 16777216),
|
|
60
|
+
maxToolArgumentBytes: int(env.MAX_TOOL_ARGUMENT_BYTES, 262144, 256, 4194304), maxTools: int(env.MAX_TOOLS, 64, 0, 256),
|
|
61
|
+
maxToolResultBytes: int(env.MAX_TOOL_RESULT_BYTES, 262144, 256, 4194304), maxConcurrent: int(env.MAX_CONCURRENT_REQUESTS, 16, 1, 1024),
|
|
62
|
+
correlationEnabled: env.CORRELATION_ENABLED !== 'false', principalKey: env.CORRELATION_PRINCIPAL_KEY || randomBytes(32).toString('base64url'), credentialSlotId: env.PROVIDER_CREDENTIAL_SLOT_ID || 'default',
|
|
63
|
+
correlationPendingTtlMs: int(env.CORRELATION_PENDING_TTL_MS, 900000, 100, 86400000), correlationTerminalTtlMs: int(env.CORRELATION_TERMINAL_TTL_MS, 900000, 100, 86400000), correlationInflightTtlMs: int(env.CORRELATION_INFLIGHT_TTL_MS, 150000, 100, 3600000),
|
|
64
|
+
correlationMaxGroups: int(env.CORRELATION_MAX_GROUPS, 1000, 1, 100000), correlationMaxCalls: int(env.CORRELATION_MAX_CALLS, 4096, 1, 100000), correlationMaxBytes: int(env.CORRELATION_MAX_BYTES, 16777216, 1024, 1073741824), exposeHealthDetails: env.EXPOSE_HEALTH_DETAILS === 'true'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function authenticate(value, expected, config = null) {
|
|
68
|
+
const bearer = typeof value === 'string' && value.startsWith('Bearer ') ? value.slice(7) : '';
|
|
69
|
+
const a = Buffer.from(bearer), b = Buffer.from(expected);
|
|
70
|
+
if (!b.length || a.length !== b.length || !timingSafeEqual(a, b)) throw new ProviderError(401, 'Invalid bearer token.', 'authentication_error', 'invalid_api_key');
|
|
71
|
+
if (!config) return true;
|
|
72
|
+
const slot = config.credentialSlotId || 'default';
|
|
73
|
+
return { credentialSlotId: slot, principalId: derivePrincipalId(config.principalKey || `ephemeral:${expected}`, slot), workspaceId: config.workspaceId };
|
|
74
|
+
}
|
|
75
|
+
export function assertModel(model, config) { if (typeof model !== 'string' || !config.models.includes(model)) throw invalid('Unknown or disallowed model.', 'model_not_allowed'); return model; }
|
|
76
|
+
export function webSessionUrl(config) {
|
|
77
|
+
if (config.transportStrategy !== 'web_session') throw new Error('webSessionUrl requires web_session configuration.');
|
|
78
|
+
const expectedOrigin = `https://${config.webSessionSubdomain}.postman.co`;
|
|
79
|
+
const url = new URL('/_gw/chat', `${expectedOrigin}/`);
|
|
80
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.hostname !== `${config.webSessionSubdomain}.postman.co` || url.port || url.pathname !== '/_gw/chat' || url.search || url.hash || url.origin !== expectedOrigin) throw new Error('Web-session URL construction failed validation.');
|
|
81
|
+
return url;
|
|
82
|
+
}
|
|
83
|
+
export function upstreamUrl(config) { const path = config.chatPath.startsWith('/') ? config.chatPath : `/${config.chatPath}`; const url = new URL(path, `${config.upstreamOrigin}/`); if (url.origin !== config.upstreamOrigin) throw new Error('Upstream origin changed unexpectedly.'); return url; }
|
|
84
|
+
export class ConcurrencyGate {
|
|
85
|
+
#active = 0; constructor(limit) { this.limit = limit; }
|
|
86
|
+
enter() { if (this.#active >= this.limit) throw new ProviderError(429, 'Provider concurrency limit reached.', 'rate_limit_error', 'concurrency_limit'); this.#active++; let left=false; return () => { if (!left) { left=true; this.#active--; } }; }
|
|
87
|
+
get active() { return this.#active; }
|
|
88
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { ProviderError } from './errors.mjs';
|
|
3
|
+
import { canonicalHash } from './canonical.mjs';
|
|
4
|
+
|
|
5
|
+
const fail = (status, code, message) => new ProviderError(status, message, status === 429 ? 'rate_limit_error' : 'invalid_request_error', code);
|
|
6
|
+
const publicId = rng => `call_pmn_${rng(24).toString('base64url')}`;
|
|
7
|
+
|
|
8
|
+
// Correlation binds the declared tool contract, not the per-turn execution directive.
|
|
9
|
+
// A normal tool lifecycle may use `required` (or a named choice) to obtain a call,
|
|
10
|
+
// then `auto` after supplying the result. The function definitions must remain exact.
|
|
11
|
+
export function structuralToolsBinding(toolsBinding) {
|
|
12
|
+
const tools = structuredClone(toolsBinding?.tools ?? []);
|
|
13
|
+
tools.sort((a, b) => (a?.function?.name ?? '').localeCompare(b?.function?.name ?? ''));
|
|
14
|
+
return {
|
|
15
|
+
tools,
|
|
16
|
+
toolExecutionDisabled: toolsBinding?.toolChoice === String.fromCharCode(110, 111, 110, 101),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class CorrelationStore {
|
|
21
|
+
#groups = new Map();
|
|
22
|
+
#calls = new Map();
|
|
23
|
+
#bytes = 0;
|
|
24
|
+
constructor({ pendingTtlMs = 900000, terminalTtlMs = 900000, inflightTtlMs = 150000, maxGroups = 1000, maxCalls = 4096, maxBytes = 16 * 1024 * 1024, maxTerminalResponseBytes = 1024 * 1024, now = Date.now, rng = randomBytes } = {}) {
|
|
25
|
+
Object.assign(this, { pendingTtlMs, terminalTtlMs, inflightTtlMs, maxGroups, maxCalls, maxBytes, maxTerminalResponseBytes, now, rng });
|
|
26
|
+
}
|
|
27
|
+
get size() { return this.#groups.size; }
|
|
28
|
+
get bytes() { return this.#bytes; }
|
|
29
|
+
#key(principalId, id) {
|
|
30
|
+
const cleanId = typeof id === 'string' ? id.replace(/[^a-zA-Z0-9]/g, '') : id;
|
|
31
|
+
return `${principalId}:${cleanId}`;
|
|
32
|
+
}
|
|
33
|
+
#transitionExpired(group, now = this.now()) {
|
|
34
|
+
if (group.state === 'PENDING' && now >= group.pendingExpiresAt) {
|
|
35
|
+
group.state = 'EXPIRED'; group.terminalExpiresAt = now + this.terminalTtlMs;
|
|
36
|
+
} else if (group.state === 'INFLIGHT' && now >= group.inflightExpiresAt) {
|
|
37
|
+
group.state = 'UNCERTAIN'; group.terminalExpiresAt = now + this.terminalTtlMs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
#estimate(record) { return Buffer.byteLength(JSON.stringify(record), 'utf8'); }
|
|
41
|
+
admit({ principalId, workspaceId, model, transportStrategy = 'access_token', transportOrigin = null, conversationId, upstreamGroupId = null, transcript, toolsBinding, catalogMetadata = {}, calls }) {
|
|
42
|
+
if (!principalId || !workspaceId || !model || !conversationId || !Array.isArray(calls) || !calls.length) throw new TypeError('Invalid correlation admission.');
|
|
43
|
+
if (calls.length > 1 && !upstreamGroupId) throw fail(502, 'upstream_ungrouped_tool_batch', 'Upstream returned multiple ungrouped tool calls.');
|
|
44
|
+
const ids = [];
|
|
45
|
+
for (let index = 0; index < calls.length; index++) {
|
|
46
|
+
let id; do id = publicId(this.rng); while (this.#calls.has(this.#key(principalId, id)) || ids.includes(id));
|
|
47
|
+
ids.push(id);
|
|
48
|
+
}
|
|
49
|
+
const publicTranscript = structuredClone(transcript);
|
|
50
|
+
for (const message of publicTranscript) if (Array.isArray(message.tool_calls)) for (const call of message.tool_calls) {
|
|
51
|
+
const index = calls.findIndex(value => value.upstreamToolCallId === call.id); if (index >= 0) call.id = ids[index];
|
|
52
|
+
}
|
|
53
|
+
const now = this.now();
|
|
54
|
+
const group = {
|
|
55
|
+
principalId, workspaceId, model, transportStrategy, transportOrigin, conversationId, upstreamGroupId, groupKey: randomUUID(), state: 'PENDING',
|
|
56
|
+
issuedAt: now, pendingExpiresAt: now + this.pendingTtlMs, terminalExpiresAt: null, inflightExpiresAt: null,
|
|
57
|
+
transcript: publicTranscript, transcriptHash: canonicalHash(publicTranscript), transcriptPrefixLength: publicTranscript.length,
|
|
58
|
+
toolsBinding: structuralToolsBinding(toolsBinding), toolsHash: canonicalHash(structuralToolsBinding(toolsBinding)), catalogMetadata: structuredClone(catalogMetadata), catalogMetadataHash: canonicalHash(catalogMetadata), acceptedResultsHash: null, dispatchAttemptId: null,
|
|
59
|
+
terminalResponse: null, terminalResponseDigest: null,
|
|
60
|
+
calls: calls.map((call, index) => ({ ...call, index, publicToolCallId: ids[index], argumentsHash: canonicalHash([call.originalName, call.argumentsJson, call.schemaHash]) })),
|
|
61
|
+
};
|
|
62
|
+
group.recordBytes = this.#estimate(group);
|
|
63
|
+
this.sweep(now);
|
|
64
|
+
const liveCalls = [...this.#groups.values()].reduce((n, value) => n + value.calls.length, 0);
|
|
65
|
+
if (this.#groups.size + 1 > this.maxGroups || liveCalls + group.calls.length > this.maxCalls || this.#bytes + group.recordBytes > this.maxBytes) throw fail(429, 'correlation_capacity_exceeded', 'Tool-call correlation capacity is full.');
|
|
66
|
+
this.#groups.set(group.groupKey, group); this.#bytes += group.recordBytes;
|
|
67
|
+
for (const call of group.calls) this.#calls.set(this.#key(principalId, call.publicToolCallId), group);
|
|
68
|
+
return group;
|
|
69
|
+
}
|
|
70
|
+
resolve(principalId, ids) {
|
|
71
|
+
const distinct = [...new Set(ids)];
|
|
72
|
+
const groups = distinct.map(id => this.#calls.get(this.#key(principalId, id)));
|
|
73
|
+
if (groups.some(group => !group)) throw fail(404, 'unknown_tool_call', 'Unknown tool call.');
|
|
74
|
+
const group = groups[0];
|
|
75
|
+
if (groups.some(value => value !== group)) throw fail(400, 'tool_result_group_mismatch', 'Tool results belong to different groups.');
|
|
76
|
+
this.#transitionExpired(group);
|
|
77
|
+
return group;
|
|
78
|
+
}
|
|
79
|
+
claim({ principalId, results, workspaceId, model, transportStrategy = 'access_token', transportOrigin = null, transcript, toolsBinding, catalogMetadata = {} }) {
|
|
80
|
+
if (!Array.isArray(results) || !results.length) throw fail(400, 'tool_result_incomplete', 'Tool results are incomplete.');
|
|
81
|
+
const ids = results.map(result => result.toolCallId);
|
|
82
|
+
if (new Set(ids).size !== ids.length) throw fail(400, 'tool_result_group_mismatch', 'Duplicate tool results are not allowed.');
|
|
83
|
+
const group = this.resolve(principalId, ids);
|
|
84
|
+
const orderedResults = group.calls.map(call => results.find(result => result.toolCallId === call.publicToolCallId));
|
|
85
|
+
if (results.length !== group.calls.length || orderedResults.some(value => !value)) throw fail(400, 'tool_result_incomplete', 'Tool results are incomplete.');
|
|
86
|
+
const resultsHash = canonicalHash(orderedResults.map((result, index) => [group.calls[index].publicToolCallId, result.content]));
|
|
87
|
+
if (group.state === 'EXPIRED') throw fail(410, 'tool_call_expired', 'Tool call expired.');
|
|
88
|
+
if (group.state === 'CANCELLED') throw fail(409, 'tool_call_cancelled', 'Tool call was cancelled.');
|
|
89
|
+
if (group.state === 'UNCERTAIN') throw fail(502, 'continuation_outcome_uncertain', 'Tool result outcome is uncertain and cannot be retried automatically.');
|
|
90
|
+
if (group.state === 'INFLIGHT') throw fail(409, group.acceptedResultsHash === resultsHash ? 'continuation_inflight' : 'continuation_conflict', group.acceptedResultsHash === resultsHash ? 'Tool result continuation is already in flight.' : 'Tool result conflicts with the in-flight continuation.');
|
|
91
|
+
if (group.state === 'CONSUMED') throw fail(409, group.acceptedResultsHash === resultsHash ? 'tool_call_already_consumed' : 'continuation_conflict', group.acceptedResultsHash === resultsHash ? 'Tool result continuation was already consumed.' : 'Tool result conflicts with the consumed continuation.');
|
|
92
|
+
if (workspaceId !== group.workspaceId) throw fail(409, 'continuation_workspace_mismatch', 'Continuation workspace does not match.');
|
|
93
|
+
if (transportStrategy !== group.transportStrategy) throw fail(409, 'continuation_strategy_mismatch', 'Continuation transport strategy does not match.');
|
|
94
|
+
if (transportOrigin !== group.transportOrigin) throw fail(409, 'continuation_origin_mismatch', 'Continuation transport origin does not match.');
|
|
95
|
+
if (model !== group.model) throw fail(409, 'continuation_model_mismatch', 'Continuation model does not match.');
|
|
96
|
+
if (canonicalHash(structuralToolsBinding(toolsBinding)) !== group.toolsHash) throw fail(409, 'continuation_tools_mismatch', 'Continuation tools do not match.');
|
|
97
|
+
if (canonicalHash(catalogMetadata) !== group.catalogMetadataHash) throw fail(409, 'continuation_catalog_mismatch', 'Continuation catalog metadata does not match.');
|
|
98
|
+
if (canonicalHash(transcript) !== group.transcriptHash) throw fail(409, 'continuation_history_mismatch', 'Continuation history does not match.');
|
|
99
|
+
const now = this.now(); group.state = 'INFLIGHT'; group.acceptedResultsHash = resultsHash; group.dispatchAttemptId = randomUUID(); group.inflightExpiresAt = now + this.inflightTtlMs;
|
|
100
|
+
return { group, orderedResults, dispatchAttemptId: group.dispatchAttemptId };
|
|
101
|
+
}
|
|
102
|
+
complete(group, response = null) {
|
|
103
|
+
if (group.state !== 'INFLIGHT') return;
|
|
104
|
+
group.state = 'CONSUMED'; group.terminalExpiresAt = this.now() + this.terminalTtlMs; group.inflightExpiresAt = null;
|
|
105
|
+
if (response !== null) { const text = typeof response === 'string' ? response : JSON.stringify(response); if (Buffer.byteLength(text) <= this.maxTerminalResponseBytes) { group.terminalResponse = response; group.terminalResponseDigest = canonicalHash(response); } }
|
|
106
|
+
}
|
|
107
|
+
releasePreSend(group) { if (group.state === 'INFLIGHT') { group.state = 'PENDING'; group.acceptedResultsHash = null; group.dispatchAttemptId = null; group.inflightExpiresAt = null; } }
|
|
108
|
+
uncertain(group) { if (group.state === 'INFLIGHT') { group.state = 'UNCERTAIN'; group.terminalExpiresAt = this.now() + this.terminalTtlMs; group.inflightExpiresAt = null; } }
|
|
109
|
+
cancel(group) { if (group.state === 'PENDING') { group.state = 'CANCELLED'; group.terminalExpiresAt = this.now() + this.terminalTtlMs; return true; } return false; }
|
|
110
|
+
sweep(now = this.now()) {
|
|
111
|
+
for (const group of [...this.#groups.values()]) {
|
|
112
|
+
this.#transitionExpired(group, now);
|
|
113
|
+
if (group.terminalExpiresAt !== null && now >= group.terminalExpiresAt && !['PENDING', 'INFLIGHT'].includes(group.state)) {
|
|
114
|
+
this.#groups.delete(group.groupKey); this.#bytes -= group.recordBytes;
|
|
115
|
+
for (const call of group.calls) this.#calls.delete(this.#key(group.principalId, call.publicToolCallId));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
inspect(principalId, id) { const group = this.#calls.get(this.#key(principalId, id)); if (group) this.#transitionExpired(group); return group || null; }
|
|
120
|
+
}
|
package/src/errors.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class ProviderError extends Error {
|
|
2
|
+
constructor(status, message, type = 'api_error', code = 'provider_error', param = null, options = {}) {
|
|
3
|
+
super(message, options);
|
|
4
|
+
this.name = 'ProviderError'; this.status = status; this.type = type; this.code = code; this.param = param;
|
|
5
|
+
this.retryAfter = options.retryAfter;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export const invalid = (message, code = 'invalid_request') => new ProviderError(400, message, 'invalid_request_error', code);
|
|
9
|
+
export const upstream = (message, code = 'upstream_error', status = 502, options = {}) => new ProviderError(status, message, status === 429 ? 'rate_limit_error' : status === 401 || status === 403 ? 'authentication_error' : 'api_error', code, null, options);
|
|
10
|
+
export function errorBody(error, requestId) {
|
|
11
|
+
const value = error instanceof ProviderError ? error : new ProviderError(500, 'The provider could not complete the request.', 'api_error', 'internal_error');
|
|
12
|
+
const body = { error: { message: value.message, type: value.type, code: value.code, param: value.param }, request_id: requestId };
|
|
13
|
+
if (value.code === 'upstream_model_mismatch') {
|
|
14
|
+
if (typeof value.observedModel === 'string' && /^[\x20-\x7E]{1,128}$/.test(value.observedModel)) body.error.observed_model = value.observedModel;
|
|
15
|
+
if (typeof value.observedModelField === 'string' && /^(?:metadata|data)(?:\.metadata)?\.(?:model|modelId|selectedModel)$/.test(value.observedModelField)) body.error.observed_model_field = value.observedModelField;
|
|
16
|
+
}
|
|
17
|
+
return body;
|
|
18
|
+
}
|