ssh-mcp-pro 1.2.1 → 1.4.0
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/CHANGELOG.md +72 -0
- package/README.md +12 -2
- package/dist/agent-bin.js +5 -2
- package/dist/agent-bin.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -47
- package/dist/cli.js.map +1 -1
- package/dist/config-security-env.d.ts +4 -0
- package/dist/config-security-env.d.ts.map +1 -0
- package/dist/config-security-env.js +64 -0
- package/dist/config-security-env.js.map +1 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -67
- package/dist/config.js.map +1 -1
- package/dist/connector-profile.d.ts +17 -3
- package/dist/connector-profile.d.ts.map +1 -1
- package/dist/connector-profile.js.map +1 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +226 -192
- package/dist/detect.js.map +1 -1
- package/dist/ensure-package-service.d.ts +9 -0
- package/dist/ensure-package-service.d.ts.map +1 -0
- package/dist/ensure-package-service.js +146 -0
- package/dist/ensure-package-service.js.map +1 -0
- package/dist/ensure-pkg.js +4 -4
- package/dist/ensure-pkg.js.map +1 -1
- package/dist/ensure.d.ts.map +1 -1
- package/dist/ensure.js +15 -144
- package/dist/ensure.js.map +1 -1
- package/dist/fs-sftp.js +1 -1
- package/dist/fs-sftp.js.map +1 -1
- package/dist/fs-stat.d.ts +10 -0
- package/dist/fs-stat.d.ts.map +1 -0
- package/dist/fs-stat.js +62 -0
- package/dist/fs-stat.js.map +1 -0
- package/dist/fs-tools.d.ts.map +1 -1
- package/dist/fs-tools.js +6 -58
- package/dist/fs-tools.js.map +1 -1
- package/dist/http-security.d.ts.map +1 -1
- package/dist/http-security.js +6 -6
- package/dist/http-security.js.map +1 -1
- package/dist/http-server-runtime.d.ts.map +1 -1
- package/dist/http-server-runtime.js +41 -7
- package/dist/http-server-runtime.js.map +1 -1
- package/dist/index.js +6 -23
- package/dist/index.js.map +1 -1
- package/dist/mcp-http-web.d.ts +8 -0
- package/dist/mcp-http-web.d.ts.map +1 -0
- package/dist/mcp-http-web.js +35 -0
- package/dist/mcp-http-web.js.map +1 -0
- package/dist/mcp.d.ts +6 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +22 -14
- package/dist/mcp.js.map +1 -1
- package/dist/policy.d.ts +8 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +147 -105
- package/dist/policy.js.map +1 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +11 -7
- package/dist/process.js.map +1 -1
- package/dist/prompts.d.ts +2 -11
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js.map +1 -1
- package/dist/remote/agent-activity-log.d.ts +26 -0
- package/dist/remote/agent-activity-log.d.ts.map +1 -0
- package/dist/remote/agent-activity-log.js +211 -0
- package/dist/remote/agent-activity-log.js.map +1 -0
- package/dist/remote/agent-cli.d.ts.map +1 -1
- package/dist/remote/agent-cli.js +92 -25
- package/dist/remote/agent-cli.js.map +1 -1
- package/dist/remote/agent-executor.d.ts.map +1 -1
- package/dist/remote/agent-executor.js +25 -16
- package/dist/remote/agent-executor.js.map +1 -1
- package/dist/remote/agent-handler.d.ts +2 -0
- package/dist/remote/agent-handler.d.ts.map +1 -1
- package/dist/remote/agent-handler.js +4 -0
- package/dist/remote/agent-handler.js.map +1 -1
- package/dist/remote/config.d.ts.map +1 -1
- package/dist/remote/config.js +12 -1
- package/dist/remote/config.js.map +1 -1
- package/dist/remote/control-plane.d.ts +12 -0
- package/dist/remote/control-plane.d.ts.map +1 -1
- package/dist/remote/control-plane.js +174 -53
- package/dist/remote/control-plane.js.map +1 -1
- package/dist/remote/crypto.d.ts +9 -4
- package/dist/remote/crypto.d.ts.map +1 -1
- package/dist/remote/crypto.js +41 -9
- package/dist/remote/crypto.js.map +1 -1
- package/dist/remote/http-util.d.ts.map +1 -1
- package/dist/remote/http-util.js +14 -7
- package/dist/remote/http-util.js.map +1 -1
- package/dist/remote/mcp-tools.d.ts +7 -0
- package/dist/remote/mcp-tools.d.ts.map +1 -1
- package/dist/remote/mcp-tools.js +28 -0
- package/dist/remote/mcp-tools.js.map +1 -1
- package/dist/remote/oauth-handler.d.ts +36 -3
- package/dist/remote/oauth-handler.d.ts.map +1 -1
- package/dist/remote/oauth-handler.js +287 -13
- package/dist/remote/oauth-handler.js.map +1 -1
- package/dist/remote/policy.js +2 -2
- package/dist/remote/policy.js.map +1 -1
- package/dist/remote/safe-file.d.ts +2 -1
- package/dist/remote/safe-file.d.ts.map +1 -1
- package/dist/remote/safe-file.js +51 -53
- package/dist/remote/safe-file.js.map +1 -1
- package/dist/remote/schemas.d.ts +9 -9
- package/dist/remote/schemas.js +6 -6
- package/dist/remote/schemas.js.map +1 -1
- package/dist/remote/store.d.ts +38 -1
- package/dist/remote/store.d.ts.map +1 -1
- package/dist/remote/store.js +149 -11
- package/dist/remote/store.js.map +1 -1
- package/dist/remote/types.d.ts +14 -0
- package/dist/remote/types.d.ts.map +1 -1
- package/dist/remote/types.js.map +1 -1
- package/dist/remote/web-dashboard-client-config.d.ts +77 -0
- package/dist/remote/web-dashboard-client-config.d.ts.map +1 -0
- package/dist/remote/web-dashboard-client-config.js +81 -0
- package/dist/remote/web-dashboard-client-config.js.map +1 -0
- package/dist/remote/web-dashboard-html.d.ts +2 -0
- package/dist/remote/web-dashboard-html.d.ts.map +1 -0
- package/dist/remote/web-dashboard-html.js +433 -0
- package/dist/remote/web-dashboard-html.js.map +1 -0
- package/dist/remote/web-dashboard-oauth-users.d.ts +4 -0
- package/dist/remote/web-dashboard-oauth-users.d.ts.map +1 -0
- package/dist/remote/web-dashboard-oauth-users.js +78 -0
- package/dist/remote/web-dashboard-oauth-users.js.map +1 -0
- package/dist/remote/web-dashboard-static.d.ts +4 -0
- package/dist/remote/web-dashboard-static.d.ts.map +1 -0
- package/dist/remote/web-dashboard-static.js +19 -0
- package/dist/remote/web-dashboard-static.js.map +1 -0
- package/dist/remote/web-dashboard-styles.d.ts +2 -0
- package/dist/remote/web-dashboard-styles.d.ts.map +1 -0
- package/dist/remote/web-dashboard-styles.js +187 -0
- package/dist/remote/web-dashboard-styles.js.map +1 -0
- package/dist/remote/web-dashboard.d.ts +62 -0
- package/dist/remote/web-dashboard.d.ts.map +1 -0
- package/dist/remote/web-dashboard.js +467 -0
- package/dist/remote/web-dashboard.js.map +1 -0
- package/dist/resources.d.ts +2 -7
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js.map +1 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +6 -3
- package/dist/retry.js.map +1 -1
- package/dist/safety.d.ts.map +1 -1
- package/dist/safety.js +56 -6
- package/dist/safety.js.map +1 -1
- package/dist/session-auth.js +7 -7
- package/dist/session-auth.js.map +1 -1
- package/dist/session.d.ts +5 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +81 -54
- package/dist/session.js.map +1 -1
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +5 -4
- package/dist/shell.js.map +1 -1
- package/dist/ssh-config.d.ts +2 -2
- package/dist/ssh-config.d.ts.map +1 -1
- package/dist/ssh-config.js +24 -20
- package/dist/ssh-config.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +9 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/connector.provider.d.ts +1 -1
- package/dist/tools/connector.provider.d.ts.map +1 -1
- package/dist/tools/ensure.provider.d.ts +1 -1
- package/dist/tools/ensure.provider.d.ts.map +1 -1
- package/dist/tools/fs.provider.d.ts +1 -1
- package/dist/tools/fs.provider.d.ts.map +1 -1
- package/dist/tools/metadata.d.ts +1 -1
- package/dist/tools/metadata.d.ts.map +1 -1
- package/dist/tools/output-schemas.d.ts +27 -135
- package/dist/tools/output-schemas.d.ts.map +1 -1
- package/dist/tools/process.provider.d.ts +1 -1
- package/dist/tools/process.provider.d.ts.map +1 -1
- package/dist/tools/registry.d.ts +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +16 -13
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/session.provider.d.ts +1 -1
- package/dist/tools/session.provider.d.ts.map +1 -1
- package/dist/tools/system.provider.d.ts +1 -1
- package/dist/tools/system.provider.d.ts.map +1 -1
- package/dist/tools/transfer.provider.d.ts +1 -1
- package/dist/tools/transfer.provider.d.ts.map +1 -1
- package/dist/tools/tunnel.provider.d.ts +1 -1
- package/dist/tools/tunnel.provider.d.ts.map +1 -1
- package/dist/tools/types.d.ts +1 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/transfer.js +2 -2
- package/dist/transfer.js.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/docs/adding-a-device.md +39 -17
- package/docs/agent-activity-monitor.md +34 -0
- package/docs/coolify.md +29 -0
- package/docs/development/coding-standards.md +8 -2
- package/docs/development/dependency-management.md +1 -1
- package/docs/development/mcp-protocol-2026.md +33 -0
- package/docs/development/release-process.md +29 -55
- package/docs/doppler-guide.md +34 -0
- package/docs/reference/compatibility.md +1 -1
- package/docs/remote-mcp-hardening.md +17 -0
- package/docs/security/dependency-audit-exceptions.md +3 -7
- package/docs/security/hosted-data-inventory.md +81 -0
- package/docs/sonarcloud-analysis.md +9 -0
- package/mcp.json +1 -1
- package/package.json +31 -24
- package/registry/ssh-mcp-pro/mcp.json +1 -1
- package/server.json +3 -3
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { DASHBOARD_STYLES } from "./web-dashboard-styles.js";
|
|
2
|
+
export function renderDashboardHtml(initialUserBase64, mcpUrlJson, publicUrlJson) {
|
|
3
|
+
return `<!doctype html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<title>ssh-mcp-pro | Secure SSH MCP Platform</title>
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9
|
+
<style>${DASHBOARD_STYLES}</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<header>
|
|
13
|
+
<div class="logo-area">
|
|
14
|
+
<div class="logo-icon">⚡</div>
|
|
15
|
+
<div>
|
|
16
|
+
<div class="brand-title">ssh-mcp-pro</div>
|
|
17
|
+
<div class="brand-sub">Model Context Protocol SSH Gateway</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="user-area" id="headerUserArea"></div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<div class="container" id="appRoot">
|
|
24
|
+
<!-- Rendered by client script based on currentUser -->
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<script>
|
|
28
|
+
const INITIAL_USER = JSON.parse(atob("${initialUserBase64}"));
|
|
29
|
+
const MCP_URL = ${mcpUrlJson};
|
|
30
|
+
const PUBLIC_URL = ${publicUrlJson};
|
|
31
|
+
|
|
32
|
+
let currentUser = INITIAL_USER;
|
|
33
|
+
let activeTab = 'devices';
|
|
34
|
+
let devicesList = [];
|
|
35
|
+
let auditList = [];
|
|
36
|
+
|
|
37
|
+
function escapeHtml(value) {
|
|
38
|
+
return String(value ?? '').replace(/[&<>"']/g, char => ({
|
|
39
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
40
|
+
})[char]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderHeader() {
|
|
44
|
+
const container = document.getElementById('headerUserArea');
|
|
45
|
+
if (currentUser) {
|
|
46
|
+
container.innerHTML = \`
|
|
47
|
+
<span class="badge badge-green">Connected</span>
|
|
48
|
+
<span style="font-size: 14px; font-weight: 600;">\${escapeHtml(currentUser.displayName || currentUser.id)}</span>
|
|
49
|
+
<span class="badge badge-blue">\${escapeHtml(currentUser.provider)}</span>
|
|
50
|
+
<a href="/auth/logout" class="btn btn-outline btn-sm">Sign out</a>
|
|
51
|
+
\`;
|
|
52
|
+
} else {
|
|
53
|
+
container.innerHTML = \`
|
|
54
|
+
<a href="/auth/login/google" class="btn btn-outline btn-sm">Sign In</a>
|
|
55
|
+
\`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderApp() {
|
|
60
|
+
const root = document.getElementById('appRoot');
|
|
61
|
+
if (!currentUser) {
|
|
62
|
+
root.innerHTML = renderLanding();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
root.innerHTML = \`
|
|
67
|
+
<div class="card" style="margin-bottom: 24px; background: linear-gradient(90deg, #161b22 0%, #1f242c 100%);">
|
|
68
|
+
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:16px;">
|
|
69
|
+
<div>
|
|
70
|
+
<div style="font-size:12px; text-transform:uppercase; font-weight:700; color:var(--blue); margin-bottom:4px;">Remote MCP Endpoint</div>
|
|
71
|
+
<div style="font-family:var(--font-mono); font-size:15px; color:#fff; font-weight:600;">\${MCP_URL}</div>
|
|
72
|
+
<div style="font-size:13px; color:var(--text-muted); margin-top:4px;">Transport: Streamable HTTP • Auth: OAuth 2.1 PKCE (RFC 9728 & RFC 8414)</div>
|
|
73
|
+
</div>
|
|
74
|
+
<button class="btn btn-primary" onclick="switchTab('add-device')">➕ Add New Device</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<nav class="tabs" aria-label="Dashboard sections">
|
|
79
|
+
<button type="button" data-dashboard-tab="devices" aria-pressed="\${activeTab === 'devices'}" class="tab-item \${activeTab === 'devices' ? 'active' : ''}" onclick="switchTab('devices')">🖥️ Enrolled Devices</button>
|
|
80
|
+
<button type="button" data-dashboard-tab="add-device" aria-pressed="\${activeTab === 'add-device'}" class="tab-item \${activeTab === 'add-device' ? 'active' : ''}" onclick="switchTab('add-device')">➕ Add Device</button>
|
|
81
|
+
<button type="button" data-dashboard-tab="clients" aria-pressed="\${activeTab === 'clients'}" class="tab-item \${activeTab === 'clients' ? 'active' : ''}" onclick="switchTab('clients')">🤖 AI Integrations</button>
|
|
82
|
+
<button type="button" data-dashboard-tab="audit" aria-pressed="\${activeTab === 'audit'}" class="tab-item \${activeTab === 'audit' ? 'active' : ''}" onclick="switchTab('audit')">📜 Audit Log</button>
|
|
83
|
+
</nav>
|
|
84
|
+
|
|
85
|
+
<div id="tabContent">
|
|
86
|
+
\${renderTabContent()}
|
|
87
|
+
</div>
|
|
88
|
+
\`;
|
|
89
|
+
|
|
90
|
+
if (activeTab === 'devices') loadDevices();
|
|
91
|
+
if (activeTab === 'audit') loadAudit();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function switchTab(tab) {
|
|
95
|
+
activeTab = tab;
|
|
96
|
+
renderApp();
|
|
97
|
+
document.querySelector('[data-dashboard-tab="' + tab + '"]')?.focus({ preventScroll: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderLanding() {
|
|
101
|
+
return \`
|
|
102
|
+
<div class="hero">
|
|
103
|
+
<div style="font-size: 48px; margin-bottom: 12px;">🛡️⚡</div>
|
|
104
|
+
<h1>ssh-mcp-pro Gateway</h1>
|
|
105
|
+
<p>Zero-Trust Model Context Protocol SSH Gateway with policy controls, biometric & OAuth 2.1 authentication, and dual local/remote agent support.</p>
|
|
106
|
+
<div class="auth-buttons">
|
|
107
|
+
<a href="/auth/login/google" class="auth-btn auth-btn-google">
|
|
108
|
+
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="#EA4335" d="M12 5c1.7 0 3 .7 3.9 1.5l2.9-2.9C17 2 14.7 1 12 1 7.5 1 3.7 3.6 1.9 7.3l3.7 2.9C6.5 7.4 9 5 12 5z"/><path fill="#4285F4" d="M23.5 12.3c0-.8-.1-1.6-.2-2.3H12v4.5h6.5c-.3 1.5-1.1 2.8-2.4 3.7l3.7 2.9c2.2-2 3.7-5 3.7-8.8z"/><path fill="#FBBC05" d="M5.6 14.8c-.2-.7-.4-1.5-.4-2.3s.2-1.6.4-2.3L1.9 7.3C.7 9.7 0 12.3 0 15.2c0 2.8.7 5.5 1.9 7.8l3.7-2.9z"/><path fill="#34A853" d="M12 23.5c3.2 0 6-1.1 8-3l-3.7-2.9c-1.1.7-2.5 1.2-4.3 1.2-3 0-5.5-2.4-6.4-5.2L1.9 16.5C3.7 20.3 7.5 23.5 12 23.5z"/></svg>
|
|
109
|
+
Sign in with Google
|
|
110
|
+
</a>
|
|
111
|
+
<a href="/auth/login/github" class="auth-btn auth-btn-github">
|
|
112
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
|
113
|
+
Sign in with GitHub
|
|
114
|
+
</a>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="grid-2">
|
|
119
|
+
<div class="card">
|
|
120
|
+
<h3 style="margin-bottom:8px; color:var(--blue);">🌐 Remote Streamable HTTP (OAuth 2.1)</h3>
|
|
121
|
+
<p style="font-size:14px; color:var(--text-muted); margin-bottom:12px;">Connect supported MCP clients (ChatGPT plugins, Claude connectors, Gemini) through the HTTPS endpoint with OAuth policy enforcement.</p>
|
|
122
|
+
<div class="code-box">\${MCP_URL}</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="card">
|
|
125
|
+
<h3 style="margin-bottom:8px; color:#7ee787;">💻 Local Stdio Mode (Unrestricted)</h3>
|
|
126
|
+
<p style="font-size:14px; color:var(--text-muted); margin-bottom:12px;">No cloud dependencies, zero limits. Run ssh-mcp-pro locally with your own OpenSSH keys and config.</p>
|
|
127
|
+
<div class="code-box">npx ssh-mcp-pro</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
\`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderTabContent() {
|
|
134
|
+
if (activeTab === 'devices') {
|
|
135
|
+
if (devicesList.length === 0) {
|
|
136
|
+
return \`
|
|
137
|
+
<div class="card empty-box">
|
|
138
|
+
<div style="font-size:40px; margin-bottom:12px;">🖥️</div>
|
|
139
|
+
<h3 style="margin-bottom:8px;">No devices enrolled yet</h3>
|
|
140
|
+
<p style="margin-bottom:20px;">Enroll your first server, VM, or local workstation to control it safely through MCP.</p>
|
|
141
|
+
<button class="btn btn-primary" onclick="switchTab('add-device')">Enroll a Device</button>
|
|
142
|
+
</div>
|
|
143
|
+
\`;
|
|
144
|
+
}
|
|
145
|
+
return devicesList.map(dev => \`
|
|
146
|
+
<div class="card">
|
|
147
|
+
<div class="device-row">
|
|
148
|
+
<div class="device-info">
|
|
149
|
+
<div class="device-icon">\${dev.host_metadata?.platform === 'win32' ? '🪟' : (dev.host_metadata?.platform === 'darwin' ? '🍎' : '🐧')}</div>
|
|
150
|
+
<div>
|
|
151
|
+
<div class="device-name">
|
|
152
|
+
\${escapeHtml(dev.alias)}
|
|
153
|
+
<span class="badge \${dev.isLiveConnected ? 'badge-green' : 'badge-gray'}">\${escapeHtml(dev.isLiveConnected ? '🟢 Live Online' : dev.status)}</span>
|
|
154
|
+
<span class="badge badge-blue">\${escapeHtml(dev.profile)}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="device-meta">
|
|
157
|
+
Host: \${escapeHtml(dev.host_metadata?.hostname || 'Not reported')} | OS: \${escapeHtml(dev.host_metadata?.os || 'Not reported')} | Arch: \${escapeHtml(dev.host_metadata?.arch || 'Not reported')} | Policy v\${escapeHtml(dev.policy_version ?? '—')}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
162
|
+
<select aria-label="Policy for \${escapeHtml(dev.alias)}" class="btn btn-outline btn-sm" onchange="changePolicy('\${dev.id}', this.value)" \${dev.status === 'revoked' ? 'disabled' : ''}>
|
|
163
|
+
<option value="read-only" \${dev.profile === 'read-only' ? 'selected' : ''}>Profile: Read-Only</option>
|
|
164
|
+
<option value="operations" \${dev.profile === 'operations' ? 'selected' : ''}>Profile: Operations</option>
|
|
165
|
+
<option value="full-admin" \${dev.profile === 'full-admin' ? 'selected' : ''}>Profile: Full Admin</option>
|
|
166
|
+
</select>
|
|
167
|
+
<button class="btn btn-outline btn-sm" onclick="revokeDevice('\${dev.id}')" \${dev.status === 'revoked' ? 'disabled' : ''}>\${dev.status === 'revoked' ? 'Revoked' : 'Revoke access'}</button>
|
|
168
|
+
<button class="btn btn-danger btn-sm" onclick="removeDevice('\${dev.id}')">Remove device</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
\`).join('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (activeTab === 'add-device') {
|
|
176
|
+
return \`
|
|
177
|
+
<div class="card">
|
|
178
|
+
<h2 style="margin-bottom:12px;">Enroll New Device</h2>
|
|
179
|
+
<p style="color:var(--text-muted); font-size:14px; margin-bottom:24px;">Generate an enrollment token and install the outbound agent on your target server.</p>
|
|
180
|
+
<div style="display:flex; gap:16px; margin-bottom:24px; flex-wrap:wrap;">
|
|
181
|
+
<div style="flex:1; min-width:240px;">
|
|
182
|
+
<label for="enrollAlias" style="display:block; font-size:13px; font-weight:600; margin-bottom:6px;">Device Alias</label>
|
|
183
|
+
<input id="enrollAlias" type="text" placeholder="e.g. production-bastion, home-server" style="width:100%; padding:10px; border-radius:6px; background:#090d13; border:1px solid var(--card-border); color:#fff; font-size:14px;">
|
|
184
|
+
</div>
|
|
185
|
+
<div style="flex:1; min-width:240px;">
|
|
186
|
+
<label for="enrollProfile" style="display:block; font-size:13px; font-weight:600; margin-bottom:6px;">Initial Policy Profile</label>
|
|
187
|
+
<select id="enrollProfile" style="width:100%; padding:10px; border-radius:6px; background:#090d13; border:1px solid var(--card-border); color:#fff; font-size:14px;">
|
|
188
|
+
<option value="read-only" selected>Read-Only (Status, Logs, Read Files)</option>
|
|
189
|
+
<option value="operations">Operations (Restart Services, Docker, Safe Shell)</option>
|
|
190
|
+
<option value="full-admin">Full Admin (Root sudo, File Write, Service Admin)</option>
|
|
191
|
+
</select>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<button class="btn btn-primary" onclick="generateEnrollment()">Generate Install Command</button>
|
|
195
|
+
|
|
196
|
+
<div id="enrollOutput" style="margin-top:24px; display:none;">
|
|
197
|
+
<h3 style="margin-bottom:12px; color:#fff;">One-Liner Installation Commands:</h3>
|
|
198
|
+
|
|
199
|
+
<div style="margin-bottom:16px;">
|
|
200
|
+
<div style="font-weight:600; font-size:13px; color:var(--blue); margin-bottom:4px;">🐧 Linux (systemd)</div>
|
|
201
|
+
<div class="code-box">
|
|
202
|
+
<button class="copy-btn" onclick="copySnippet('cmdLinux')">Copy</button>
|
|
203
|
+
<span id="cmdLinux"></span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div style="margin-bottom:16px;">
|
|
208
|
+
<div style="font-weight:600; font-size:13px; color:var(--blue); margin-bottom:4px;">🪟 Windows (PowerShell)</div>
|
|
209
|
+
<div class="code-box">
|
|
210
|
+
<button class="copy-btn" onclick="copySnippet('cmdWindows')">Copy</button>
|
|
211
|
+
<span id="cmdWindows"></span>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div>
|
|
216
|
+
<div style="font-weight:600; font-size:13px; color:var(--blue); margin-bottom:4px;">📦 Node CLI (Linux/macOS)</div>
|
|
217
|
+
<div class="code-box">
|
|
218
|
+
<button class="copy-btn" onclick="copySnippet('cmdNpm')">Copy</button>
|
|
219
|
+
<span id="cmdNpm"></span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
\`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (activeTab === 'clients') {
|
|
228
|
+
return \`
|
|
229
|
+
<div class="grid-2">
|
|
230
|
+
<div class="card">
|
|
231
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
|
232
|
+
<span style="font-size:24px;">🤖</span>
|
|
233
|
+
<h3>Claude Desktop & Claude Code</h3>
|
|
234
|
+
</div>
|
|
235
|
+
<p style="font-size:13px; color:var(--text-muted); margin-bottom:12px;">For remote access in Claude or Claude Desktop: <strong>Customize > Connectors > Add custom connector</strong>, enter this URL, then sign in. Local Desktop servers still use <code>claude_desktop_config.json</code>.</p>
|
|
236
|
+
<div style="font-size:12px; font-weight:700; color:var(--blue);">Claude remote custom connector URL:</div>
|
|
237
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgClaudeRemote')">Copy</button><span id="cfgClaudeRemote">\${MCP_URL}</span></div>
|
|
238
|
+
<div style="font-size:12px; font-weight:700; color:var(--blue); margin-top:12px;">Claude Code remote (run in a terminal):</div>
|
|
239
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgClaudeCode')">Copy</button><span id="cfgClaudeCode">claude mcp add --transport http ssh-mcp-pro \${MCP_URL}</span></div>
|
|
240
|
+
<div style="font-size:12px; font-weight:700; color:#7ee787; margin-top:12px;">Claude Desktop local stdio:</div>
|
|
241
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgClaudeLocal')">Copy</button><span id="cfgClaudeLocal">{\\n "mcpServers": {\\n "ssh-mcp-pro": {\\n "command": "ssh-mcp-pro",\\n "args": []\\n }\\n }\\n}</span></div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div class="card">
|
|
245
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
|
246
|
+
<span style="font-size:24px;">💬</span>
|
|
247
|
+
<h3>ChatGPT MCP Plugin</h3>
|
|
248
|
+
</div>
|
|
249
|
+
<p style="font-size:13px; color:var(--text-muted); margin-bottom:12px;">In supported ChatGPT workspaces, open <strong>Plugins > Add custom MCP server</strong>, paste the URL below and configure OAuth. Custom GPT Actions are a separate OpenAPI-based feature.</p>
|
|
250
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgChatGptMcp')">Copy</button><span id="cfgChatGptMcp">\${MCP_URL}</span></div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div class="card">
|
|
254
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
|
255
|
+
<span style="font-size:24px;">✨</span>
|
|
256
|
+
<h3>Gemini API (Interactions)</h3>
|
|
257
|
+
</div>
|
|
258
|
+
<p style="font-size:13px; color:var(--text-muted); margin-bottom:12px;">Use this Remote MCP tool definition with the Gemini API Interactions endpoint. Configure a valid OAuth bearer credential separately (for example via managed agent credentials); the URL alone does not authenticate. This is not a Gemini web-extension manifest. <a href="https://ai.google.dev/gemini-api/docs/function-calling" target="_blank" rel="noopener noreferrer">Gemini Remote MCP docs</a>.</p>
|
|
259
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgGemini')">Copy</button><span id="cfgGemini">{\\n "type": "mcp_server",\\n "name": "ssh_mcp_pro",\\n "url": "\${MCP_URL}"\\n}</span></div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="card">
|
|
263
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
|
264
|
+
<span style="font-size:24px;">💻</span>
|
|
265
|
+
<h3>VS Code / Cursor / Windsurf</h3>
|
|
266
|
+
</div>
|
|
267
|
+
<p style="font-size:13px; color:var(--text-muted); margin-bottom:12px;">Copy the configuration for your chosen client. VS Code/Copilot portable files use <code>mcpServers</code>; the VS Code workspace format uses <code>servers</code>. Sign in through your client's OAuth flow.</p>
|
|
268
|
+
<div style="font-size:12px; font-weight:700; color:var(--blue); margin-top:12px;">Copilot portable — <code>.mcp.json</code></div>
|
|
269
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgVsCodePortable')">Copy</button><span id="cfgVsCodePortable">{\\n "mcpServers": {\\n "ssh-mcp-pro": {\\n "type": "http",\\n "url": "\${MCP_URL}"\\n }\\n }\\n}</span></div>
|
|
270
|
+
<div style="font-size:12px; font-weight:700; color:var(--blue); margin-top:12px;">VS Code workspace (compatible) — <code>.vscode/mcp.json</code></div>
|
|
271
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgVsCode')">Copy</button><span id="cfgVsCode">{\\n "servers": {\\n "ssh-mcp-pro": {\\n "type": "http",\\n "url": "\${MCP_URL}"\\n }\\n }\\n}</span></div>
|
|
272
|
+
<div style="font-size:12px; font-weight:700; color:var(--blue); margin-top:12px;">Cursor — <code>.cursor/mcp.json</code></div>
|
|
273
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgCursor')">Copy</button><span id="cfgCursor">{\\n "mcpServers": {\\n "ssh-mcp-pro": {\\n "url": "\${MCP_URL}"\\n }\\n }\\n}</span></div>
|
|
274
|
+
<div style="font-size:12px; font-weight:700; color:var(--blue); margin-top:12px;">Windsurf — <code>mcp_config.json</code></div>
|
|
275
|
+
<div class="code-box"><button class="copy-btn" onclick="copySnippet('cfgWindsurf')">Copy</button><span id="cfgWindsurf">{\\n "mcpServers": {\\n "ssh-mcp-pro": {\\n "serverUrl": "\${MCP_URL}"\\n }\\n }\\n}</span></div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
\`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (activeTab === 'audit') {
|
|
282
|
+
if (auditList.length === 0) {
|
|
283
|
+
return '<div class="card empty-box">No audit events recorded yet.</div>';
|
|
284
|
+
}
|
|
285
|
+
return \`
|
|
286
|
+
<div class="card" style="padding:0; overflow:hidden;">
|
|
287
|
+
<table style="width:100%; border-collapse:collapse; font-size:13px; text-align:left;">
|
|
288
|
+
<thead style="background:#21262d; border-bottom:1px solid var(--card-border);">
|
|
289
|
+
<tr>
|
|
290
|
+
<th style="padding:12px 16px;">Timestamp</th>
|
|
291
|
+
<th style="padding:12px 16px;">Event</th>
|
|
292
|
+
<th style="padding:12px 16px;">Severity</th>
|
|
293
|
+
<th style="padding:12px 16px;">Target</th>
|
|
294
|
+
</tr>
|
|
295
|
+
</thead>
|
|
296
|
+
<tbody>
|
|
297
|
+
\${auditList.map(e => \`
|
|
298
|
+
<tr style="border-bottom:1px solid #21262d;">
|
|
299
|
+
<td style="padding:12px 16px; font-family:var(--font-mono); color:var(--text-muted);">\${new Date(e.createdAt).toLocaleTimeString()}</td>
|
|
300
|
+
<td style="padding:12px 16px; font-weight:600;">\${escapeHtml(e.eventType)}</td>
|
|
301
|
+
<td style="padding:12px 16px;"><span class="badge \${e.severity === 'info' ? 'badge-blue' : (e.severity === 'warn' ? 'badge-green' : 'badge-gray')}">\${e.severity}</span></td>
|
|
302
|
+
<td style="padding:12px 16px; font-family:var(--font-mono); color:var(--text-muted);">\${escapeHtml(e.agentId || e.actionId || '-')}</td>
|
|
303
|
+
</tr>
|
|
304
|
+
\`).join('')}
|
|
305
|
+
</tbody>
|
|
306
|
+
</table>
|
|
307
|
+
</div>
|
|
308
|
+
\`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function loadDevices() {
|
|
313
|
+
try {
|
|
314
|
+
const res = await fetch('/api/dashboard/devices');
|
|
315
|
+
if (res.ok) {
|
|
316
|
+
const data = await res.json();
|
|
317
|
+
devicesList = data.devices || [];
|
|
318
|
+
const tabContent = document.getElementById('tabContent');
|
|
319
|
+
if (activeTab === 'devices' && tabContent) {
|
|
320
|
+
tabContent.innerHTML = renderTabContent();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (e) {
|
|
324
|
+
console.error('Failed to load devices', e);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function loadAudit() {
|
|
329
|
+
try {
|
|
330
|
+
const res = await fetch('/api/dashboard/audit');
|
|
331
|
+
if (res.ok) {
|
|
332
|
+
const data = await res.json();
|
|
333
|
+
auditList = data.events || [];
|
|
334
|
+
const tabContent = document.getElementById('tabContent');
|
|
335
|
+
if (activeTab === 'audit' && tabContent) {
|
|
336
|
+
tabContent.innerHTML = renderTabContent();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.error('Failed to load audit events', e);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function generateEnrollment() {
|
|
345
|
+
const alias = document.getElementById('enrollAlias').value.trim() || 'server-' + Math.floor(Math.random() * 1000);
|
|
346
|
+
const profile = document.getElementById('enrollProfile').value;
|
|
347
|
+
try {
|
|
348
|
+
const res = await fetch('/api/dashboard/devices/enroll', {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: { 'Content-Type': 'application/json', 'X-SSH-MCP-Dashboard': '1' },
|
|
351
|
+
body: JSON.stringify({ alias, requested_profile: profile })
|
|
352
|
+
});
|
|
353
|
+
if (res.ok) {
|
|
354
|
+
const data = await res.json();
|
|
355
|
+
document.getElementById('cmdLinux').textContent = data.commands.linux;
|
|
356
|
+
document.getElementById('cmdWindows').textContent = data.commands.windows;
|
|
357
|
+
document.getElementById('cmdNpm').textContent = data.commands.npm;
|
|
358
|
+
document.getElementById('enrollOutput').style.display = 'block';
|
|
359
|
+
} else {
|
|
360
|
+
alert('Failed to generate enrollment token.');
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
alert('Error: ' + e.message);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function changePolicy(id, profile) {
|
|
368
|
+
try {
|
|
369
|
+
const res = await fetch('/api/dashboard/devices/' + encodeURIComponent(id), {
|
|
370
|
+
method: 'PATCH',
|
|
371
|
+
headers: { 'Content-Type': 'application/json', 'X-SSH-MCP-Dashboard': '1' },
|
|
372
|
+
body: JSON.stringify({ policy: { profile } })
|
|
373
|
+
});
|
|
374
|
+
if (res.ok) {
|
|
375
|
+
loadDevices();
|
|
376
|
+
} else {
|
|
377
|
+
alert('Failed to update policy');
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {
|
|
380
|
+
alert('Error: ' + e.message);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function revokeDevice(id) {
|
|
385
|
+
if (!confirm('Revoke access for this device? The agent will disconnect, but its record will remain until removed.')) return;
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetch('/api/dashboard/devices/' + encodeURIComponent(id) + '/revoke', {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
headers: { 'X-SSH-MCP-Dashboard': '1' }
|
|
390
|
+
});
|
|
391
|
+
if (res.ok) {
|
|
392
|
+
await loadDevices();
|
|
393
|
+
} else {
|
|
394
|
+
const detail = await res.json().catch(() => ({}));
|
|
395
|
+
alert(detail.error || 'Failed to revoke device');
|
|
396
|
+
}
|
|
397
|
+
} catch (e) {
|
|
398
|
+
alert('Error: ' + e.message);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function removeDevice(id) {
|
|
403
|
+
if (!confirm('Remove this device? Access will be revoked and its device record permanently deleted. Audit history is retained.')) return;
|
|
404
|
+
try {
|
|
405
|
+
const res = await fetch('/api/dashboard/devices/' + encodeURIComponent(id), {
|
|
406
|
+
method: 'DELETE',
|
|
407
|
+
headers: { 'X-SSH-MCP-Dashboard': '1' }
|
|
408
|
+
});
|
|
409
|
+
if (res.ok) {
|
|
410
|
+
loadDevices();
|
|
411
|
+
} else {
|
|
412
|
+
const detail = await res.json().catch(() => ({}));
|
|
413
|
+
alert(detail.error || 'Failed to remove device');
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
alert('Error: ' + e.message);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function copySnippet(id) {
|
|
421
|
+
const text = document.getElementById(id).textContent;
|
|
422
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
423
|
+
alert('Copied to clipboard!');
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
renderHeader();
|
|
428
|
+
renderApp();
|
|
429
|
+
</script>
|
|
430
|
+
</body>
|
|
431
|
+
</html>`;
|
|
432
|
+
}
|
|
433
|
+
//# sourceMappingURL=web-dashboard-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-dashboard-html.js","sourceRoot":"","sources":["../../src/remote/web-dashboard-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,MAAM,UAAU,mBAAmB,CACjC,iBAAyB,EACzB,UAAkB,EAClB,aAAqB;IAErB,OAAO;;;;;;WAME,gBAAgB;;;;;;;;;;;;;;;;;;;4CAmBiB,iBAAiB;sBACvC,UAAU;yBACP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAiZ9B,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GitHubUser, GoogleUser, RemoteConfig } from "./types.js";
|
|
2
|
+
export declare function fetchGitHubUserDirect(config: RemoteConfig, code: string): Promise<GitHubUser>;
|
|
3
|
+
export declare function fetchGoogleUserDirect(config: RemoteConfig, code: string, nonce: string): Promise<GoogleUser>;
|
|
4
|
+
//# sourceMappingURL=web-dashboard-oauth-users.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-dashboard-oauth-users.d.ts","sourceRoot":"","sources":["../../src/remote/web-dashboard-oauth-users.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAIvE,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,UAAU,CAAC,CA2BrB;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,UAAU,CAAC,CAiCrB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
+
import { asString, safeError } from "./http-util.js";
|
|
3
|
+
const googleJwks = createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs"));
|
|
4
|
+
export async function fetchGitHubUserDirect(config, code) {
|
|
5
|
+
const callbackUrl = `${config.publicBaseUrl}/auth/callback/github`;
|
|
6
|
+
const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
9
|
+
body: JSON.stringify({
|
|
10
|
+
client_id: config.githubClientId,
|
|
11
|
+
client_secret: config.githubClientSecret,
|
|
12
|
+
code,
|
|
13
|
+
redirect_uri: callbackUrl,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
const tokenPayload = (await tokenResponse.json());
|
|
17
|
+
const accessToken = asString(tokenPayload.access_token);
|
|
18
|
+
if (!accessToken) {
|
|
19
|
+
throw safeError("INVALID_TOKEN", "GitHub OAuth token exchange failed", 502);
|
|
20
|
+
}
|
|
21
|
+
const userResponse = await fetch("https://api.github.com/user", {
|
|
22
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/vnd.github+json" },
|
|
23
|
+
});
|
|
24
|
+
const userPayload = (await userResponse.json());
|
|
25
|
+
const githubId = userPayload.id;
|
|
26
|
+
const githubLogin = userPayload.login;
|
|
27
|
+
if (typeof githubId !== "number" || typeof githubLogin !== "string" || !githubLogin) {
|
|
28
|
+
throw safeError("INVALID_TOKEN", "GitHub user response missing valid identity", 502);
|
|
29
|
+
}
|
|
30
|
+
return { id: String(githubId), login: githubLogin };
|
|
31
|
+
}
|
|
32
|
+
export async function fetchGoogleUserDirect(config, code, nonce) {
|
|
33
|
+
const callbackUrl = config.googleCallbackUrl;
|
|
34
|
+
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
37
|
+
body: new URLSearchParams({
|
|
38
|
+
code,
|
|
39
|
+
client_id: config.googleClientId ?? "",
|
|
40
|
+
client_secret: config.googleClientSecret ?? "",
|
|
41
|
+
redirect_uri: callbackUrl,
|
|
42
|
+
grant_type: "authorization_code",
|
|
43
|
+
}).toString(),
|
|
44
|
+
});
|
|
45
|
+
const tokenPayload = (await tokenResponse.json());
|
|
46
|
+
const idToken = asString(tokenPayload.id_token);
|
|
47
|
+
const clientId = config.googleClientId;
|
|
48
|
+
if (!tokenResponse.ok || !idToken || !clientId || !nonce) {
|
|
49
|
+
throw safeError("INVALID_TOKEN", "Google OIDC token exchange failed", 502);
|
|
50
|
+
}
|
|
51
|
+
let claims;
|
|
52
|
+
try {
|
|
53
|
+
const verified = await jwtVerify(idToken, googleJwks, {
|
|
54
|
+
issuer: ["https://accounts.google.com", "accounts.google.com"],
|
|
55
|
+
audience: clientId,
|
|
56
|
+
algorithms: ["RS256"],
|
|
57
|
+
});
|
|
58
|
+
claims = verified.payload;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw safeError("INVALID_TOKEN", "Google OIDC ID token validation failed", 403);
|
|
62
|
+
}
|
|
63
|
+
return googleIdentityFromVerifiedClaims(claims, nonce);
|
|
64
|
+
}
|
|
65
|
+
function googleIdentityFromVerifiedClaims(claims, nonce) {
|
|
66
|
+
if (typeof claims.sub !== "string" ||
|
|
67
|
+
!claims.sub ||
|
|
68
|
+
typeof claims.email !== "string" ||
|
|
69
|
+
!claims.email ||
|
|
70
|
+
claims.email_verified !== true ||
|
|
71
|
+
claims.nonce !== nonce) {
|
|
72
|
+
throw safeError("INVALID_TOKEN", "Google OIDC identity or nonce mismatch", 403);
|
|
73
|
+
}
|
|
74
|
+
const name = typeof claims.name === "string" ? claims.name : claims.email;
|
|
75
|
+
const picture = typeof claims.picture === "string" ? claims.picture : undefined;
|
|
76
|
+
return { id: claims.sub, email: claims.email, name, picture };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=web-dashboard-oauth-users.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-dashboard-oauth-users.js","sourceRoot":"","sources":["../../src/remote/web-dashboard-oauth-users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAmB,MAAM,MAAM,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGrD,MAAM,UAAU,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,4CAA4C,CAAC,CAAC,CAAC;AAE7F,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAoB,EACpB,IAAY;IAEZ,MAAM,WAAW,GAAG,GAAG,MAAM,CAAC,aAAa,uBAAuB,CAAC;IACnE,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,6CAA6C,EAAE;QAC/E,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,SAAS,EAAE,MAAM,CAAC,cAAc;YAChC,aAAa,EAAE,MAAM,CAAC,kBAAkB;YACxC,IAAI;YACJ,YAAY,EAAE,WAAW;SAC1B,CAAC;KACH,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAA4B,CAAC;IAC7E,MAAM,WAAW,GAAG,QAAQ,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IACxD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,SAAS,CAAC,eAAe,EAAE,oCAAoC,EAAE,GAAG,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,YAAY,GAAG,MAAM,KAAK,CAAC,6BAA6B,EAAE;QAC9D,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE,MAAM,EAAE,6BAA6B,EAAE;KAC3F,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,CAAC,MAAM,YAAY,CAAC,IAAI,EAAE,CAA4B,CAAC;IAC3E,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC;IAChC,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC;IACtC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,CAAC,WAAW,EAAE,CAAC;QACpF,MAAM,SAAS,CAAC,eAAe,EAAE,6CAA6C,EAAE,GAAG,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAoB,EACpB,IAAY,EACZ,KAAa;IAEb,MAAM,WAAW,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC7C,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,qCAAqC,EAAE;QACvE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,eAAe,CAAC;YACxB,IAAI;YACJ,SAAS,EAAE,MAAM,CAAC,cAAc,IAAI,EAAE;YACtC,aAAa,EAAE,MAAM,CAAC,kBAAkB,IAAI,EAAE;YAC9C,YAAY,EAAE,WAAW;YACzB,UAAU,EAAE,oBAAoB;SACjC,CAAC,CAAC,QAAQ,EAAE;KACd,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAA4B,CAAC;IAC7E,MAAM,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,CAAC;IACvC,IAAI,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;QACzD,MAAM,SAAS,CAAC,eAAe,EAAE,mCAAmC,EAAE,GAAG,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,MAAwD,CAAC;IAC7D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE;YACpD,MAAM,EAAE,CAAC,6BAA6B,EAAE,qBAAqB,CAAC;YAC9D,QAAQ,EAAE,QAAQ;YAClB,UAAU,EAAE,CAAC,OAAO,CAAC;SACtB,CAAC,CAAC;QACH,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,SAAS,CAAC,eAAe,EAAE,wCAAwC,EAAE,GAAG,CAAC,CAAC;IAClF,CAAC;IAED,OAAO,gCAAgC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,gCAAgC,CAAC,MAAkB,EAAE,KAAa;IACzE,IACE,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;QAC9B,CAAC,MAAM,CAAC,GAAG;QACX,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;QAChC,CAAC,MAAM,CAAC,KAAK;QACb,MAAM,CAAC,cAAc,KAAK,IAAI;QAC9B,MAAM,CAAC,KAAK,KAAK,KAAK,EACtB,CAAC;QACD,MAAM,SAAS,CAAC,eAAe,EAAE,wCAAwC,EAAE,GAAG,CAAC,CAAC;IAClF,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;IAC1E,MAAM,OAAO,GAAG,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAChF,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
/** Serve only the two explicit agent installer routes; never accept user-supplied paths. */
|
|
3
|
+
export declare function serveAgentScript(res: ServerResponse, relativePath: string, contentType: string): void;
|
|
4
|
+
//# sourceMappingURL=web-dashboard-static.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-dashboard-static.d.ts","sourceRoot":"","sources":["../../src/remote/web-dashboard-static.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,cAAc,EACnB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAClB,IAAI,CAcN"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/** Serve only the two explicit agent installer routes; never accept user-supplied paths. */
|
|
4
|
+
export function serveAgentScript(res, relativePath, contentType) {
|
|
5
|
+
const fullPath = path.resolve(relativePath);
|
|
6
|
+
if (!existsSync(fullPath)) {
|
|
7
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
8
|
+
res.end("Script file not found on server");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const content = readFileSync(fullPath, "utf8");
|
|
12
|
+
res.writeHead(200, {
|
|
13
|
+
"Content-Type": contentType,
|
|
14
|
+
"Cache-Control": "public, max-age=3600",
|
|
15
|
+
"X-Content-Type-Options": "nosniff",
|
|
16
|
+
});
|
|
17
|
+
res.end(content);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=web-dashboard-static.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-dashboard-static.js","sourceRoot":"","sources":["../../src/remote/web-dashboard-static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,4FAA4F;AAC5F,MAAM,UAAU,gBAAgB,CAC9B,GAAmB,EACnB,YAAoB,EACpB,WAAmB;IAEnB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;QACrD,GAAG,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAC3C,OAAO;IACT,CAAC;IACD,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC/C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;QACjB,cAAc,EAAE,WAAW;QAC3B,eAAe,EAAE,sBAAsB;QACvC,wBAAwB,EAAE,SAAS;KACpC,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const DASHBOARD_STYLES = "\n :root {\n --bg: #0d1117;\n --card-bg: #161b22;\n --card-border: #30363d;\n --text-main: #f0f6fc;\n --text-muted: #8b949e;\n --primary: #238636;\n --primary-hover: #2ea043;\n --blue: #58a6ff;\n --blue-bg: rgba(56, 139, 253, 0.15);\n --red: #f85149;\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n }\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background-color: var(--bg);\n color: var(--text-main);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n line-height: 1.5;\n padding-bottom: 60px;\n }\n header {\n background: #161b22;\n border-bottom: 1px solid var(--card-border);\n padding: 16px 24px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n position: sticky;\n top: 0;\n z-index: 100;\n }\n .logo-area { display: flex; align-items: center; gap: 12px; }\n .logo-icon { font-size: 24px; }\n .brand-title { font-size: 18px; font-weight: 700; color: #fff; letter-spacing: -0.5px; }\n .brand-sub { font-size: 12px; color: var(--text-muted); }\n .user-area { display: flex; align-items: center; gap: 16px; }\n .badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 12px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n }\n .badge-green { background: #238636; color: #fff; }\n .badge-gray { background: #30363d; color: #8b949e; }\n .badge-blue { background: var(--blue-bg); color: var(--blue); border: 1px solid rgba(56,139,253,0.3); }\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n text-decoration: none;\n border: 1px solid transparent;\n transition: all 0.2s ease;\n }\n .btn-primary { background: var(--primary); color: #fff; border-color: rgba(240,246,252,0.1); }\n .btn-primary:hover { background: var(--primary-hover); }\n .btn-outline { background: transparent; color: var(--text-main); border-color: var(--card-border); }\n .btn-outline:hover { background: #21262d; border-color: #8b949e; }\n .btn-danger { background: rgba(248,81,73,0.1); color: var(--red); border-color: rgba(248,81,73,0.4); }\n .btn-danger:hover { background: var(--red); color: #fff; }\n .btn-sm { padding: 4px 10px; font-size: 12px; }\n\n .container { max-width: 1100px; margin: 32px auto; padding: 0 20px; }\n\n /* Hero banner */\n .hero {\n background: linear-gradient(180deg, #161b22 0%, #0d1117 100%);\n border: 1px solid var(--card-border);\n border-radius: 12px;\n padding: 48px 32px;\n text-align: center;\n margin-bottom: 32px;\n }\n .hero h1 { font-size: 32px; font-weight: 800; margin-bottom: 12px; color: #fff; }\n .hero p { font-size: 16px; color: var(--text-muted); max-width: 650px; margin: 0 auto 28px; }\n .auth-buttons { display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; }\n .auth-btn {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 24px;\n border-radius: 8px;\n font-weight: 600;\n font-size: 15px;\n text-decoration: none;\n border: 1px solid transparent;\n transition: transform 0.15s ease;\n }\n .auth-btn:hover { transform: translateY(-2px); }\n .auth-btn-google { background: #ffffff; color: #1f2937; border-color: #d1d5db; }\n .auth-btn-github { background: #24292f; color: #ffffff; border-color: #30363d; }\n\n /* Tab nav */\n .tabs { display: flex; flex-wrap: wrap; gap: 8px; border-bottom: 1px solid var(--card-border); margin-bottom: 24px; }\n .tab-item {\n background: transparent;\n border: 0;\n font-family: inherit;\n text-align: left;\n padding: 12px 20px;\n cursor: pointer;\n font-weight: 600;\n font-size: 14px;\n color: var(--text-muted);\n border-bottom: 2px solid transparent;\n transition: color 0.15s ease;\n }\n .tab-item.active { color: var(--blue); border-bottom-color: var(--blue); }\n .tab-item:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }\n .tab-pane { display: none; }\n .tab-pane.active { display: block; }\n\n /* Device Cards */\n .card {\n background: var(--card-bg);\n border: 1px solid var(--card-border);\n border-radius: 8px;\n padding: 20px;\n margin-bottom: 16px;\n }\n .device-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-wrap: wrap;\n gap: 16px;\n }\n .device-info { display: flex; align-items: center; gap: 16px; }\n .device-icon {\n font-size: 28px;\n width: 48px;\n height: 48px;\n background: #21262d;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .device-name { font-size: 16px; font-weight: 700; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }\n .device-meta { font-size: 13px; color: var(--text-muted); font-family: var(--font-mono); }\n\n /* Code block */\n .code-box {\n background: #090d13;\n border: 1px solid #30363d;\n border-radius: 6px;\n padding: 14px 18px;\n font-family: var(--font-mono);\n font-size: 13px;\n color: #7ee787;\n overflow-x: auto;\n margin: 12px 0;\n position: relative;\n }\n .copy-btn {\n position: absolute;\n top: 8px;\n right: 8px;\n padding: 4px 8px;\n font-size: 11px;\n background: #21262d;\n color: #c9d1d9;\n border: 1px solid #30363d;\n border-radius: 4px;\n cursor: pointer;\n }\n .copy-btn:hover { background: #30363d; color: #fff; }\n\n /* Config guide cards */\n .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }\n\n /* Empty state */\n .empty-box {\n text-align: center;\n padding: 48px 16px;\n color: var(--text-muted);\n }\n";
|
|
2
|
+
//# sourceMappingURL=web-dashboard-styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-dashboard-styles.d.ts","sourceRoot":"","sources":["../../src/remote/web-dashboard-styles.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,mpMAyL5B,CAAC"}
|