@integsec/agentic-pentest-proxy 0.3.1 → 0.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/README.md CHANGED
@@ -4,7 +4,7 @@ An MCP proxy that checks tool calls against a pentest scope manifest before forw
4
4
 
5
5
  **Requires Node.js 22 or newer.** The proxy supports stdio tools, SSE upstreams such as Burp Suite, and HTTP upstreams.
6
6
 
7
- **Managing several tools?** The [managed gateway](docs/managed-gateway.md) adds a browser UI for tool toggles, scope drafts, and team users, one MCP URL for ChatGPT and Claude, a local Burp route, and per-scope execution profiles for local models.
7
+ **Managing several tools?** The [managed gateway](docs/managed-gateway.md) adds a browser UI with curated MCP server presets, scope templates and technique checkboxes, JSON policy import and download, administrator approval and assignment, and a separate engagement choice for each user. It supports one hosted MCP URL for ChatGPT and Claude and a local Burp route.
8
8
 
9
9
  ## Quick start: add an MCP tool
10
10
 
@@ -68,7 +68,7 @@ export class PentestGatewayContainer extends Container {
68
68
  }
69
69
 
70
70
  async persistMutation(request) {
71
- const previous = await this.ctx.storage.get('gateway-state') ?? { version: 1, upstreams: [], scopes: [], activeScopeId: null };
71
+ const previous = await this.ctx.storage.get('gateway-state') ?? { version: 2, upstreams: [], scopes: [], selectedScopeByUser: {} };
72
72
  const response = await this.containerFetch(request);
73
73
  if (!response.ok) return response;
74
74
  const currentResponse = await this.containerFetch('http://localhost:9091/__internal/state', {
@@ -111,8 +111,9 @@ export class PentestGatewayContainer extends Container {
111
111
 
112
112
  async listAudit(request) {
113
113
  const check = await this.containerFetch('http://localhost:9091/api/session', { headers: request.headers });
114
- await check.body?.cancel();
115
- if (check.status !== 200) return text('Administrator access required', 403);
114
+ if (check.status !== 200) { await check.body?.cancel(); return text('Administrator access required', 403); }
115
+ const session = await check.json();
116
+ if (session.role !== 'admin') return text('Administrator access required', 403);
116
117
  const entries = await this.ctx.storage.list({ prefix: 'audit:', reverse: true, limit: 100 });
117
118
  return json({ entries: [...entries.values()] });
118
119
  }
@@ -4,12 +4,13 @@
4
4
  "main": "src/worker.mjs",
5
5
  "compatibility_date": "2026-11-07",
6
6
  "workers_dev": true,
7
+ "preview_urls": false,
7
8
  "routes": [{ "pattern": "agentic-proxy.pentestprepper.com", "custom_domain": true }],
8
9
  "observability": { "enabled": true },
9
10
  "containers": [
10
11
  {
11
12
  "class_name": "PentestGatewayContainer",
12
- "image": "docker.io/integsec/agentic-pentest-mcp-proxy:0.3.1",
13
+ "image": "docker.io/integsec/agentic-pentest-mcp-proxy:0.3.3",
13
14
  "instance_type": "basic",
14
15
  "max_instances": 1
15
16
  }
@@ -12,9 +12,9 @@ const url = new URL(process.argv[2] ?? process.env.GATEWAY_URL ?? "http://127.0.
12
12
  if (!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) || url.protocol !== "http:") {
13
13
  throw new Error("The local bridge only accepts a loopback HTTP gateway URL");
14
14
  }
15
- const upstream = new Client({ name: "agentic-pentest-bridge", version: "0.3.1" });
15
+ const upstream = new Client({ name: "agentic-pentest-bridge", version: "0.3.3" });
16
16
  await upstream.connect(new StreamableHTTPClientTransport(url));
17
- const bridge = new Server({ name: "agentic-pentest-local", version: "0.3.1" }, { capabilities: { tools: {} } });
17
+ const bridge = new Server({ name: "agentic-pentest-local", version: "0.3.3" }, { capabilities: { tools: {} } });
18
18
  bridge.setRequestHandler(ListToolsRequestSchema, async (request) => upstream.listTools(request.params));
19
19
  bridge.setRequestHandler(CallToolRequestSchema, async (request) => upstream.callTool(request.params));
20
20
  await bridge.connect(new StdioServerTransport());
@@ -50,7 +50,7 @@ export function loadConfig() {
50
50
  techniqueMapPath: process.env.TECHNIQUE_MAP_PATH,
51
51
  dnsCacheTtl: parseInt(process.env.DNS_CACHE_TTL || "60", 10),
52
52
  logLevel: process.env.LOG_LEVEL || "INFO",
53
- proxyVersion: process.env.PROXY_VERSION || "0.3.1",
53
+ proxyVersion: process.env.PROXY_VERSION || "0.3.3",
54
54
  };
55
55
  }
56
56
  //# sourceMappingURL=config.js.map
@@ -4,19 +4,18 @@
4
4
  <meta name="viewport" content="width=device-width, initial-scale=1">
5
5
  <title>IntegSec MCP Gateway</title>
6
6
  <style>
7
- :root{font:15px system-ui,sans-serif;color:#e8f0f2;background:#0b1218}
8
- *{box-sizing:border-box}body{margin:0}header{padding:22px 5vw;background:#10202b;border-bottom:1px solid #234052;display:flex;justify-content:space-between;align-items:center;gap:20px}
9
- h1{font-size:21px;margin:0}h2{font-size:18px;margin:0 0 14px}h3{font-size:15px;margin:16px 0 6px}p{color:#a8bdc8;line-height:1.45}main{max-width:1200px;margin:30px auto;padding:0 20px;display:grid;grid-template-columns:1fr 1fr;gap:20px}.card{background:#14222c;border:1px solid #294454;border-radius:13px;padding:24px}.wide{grid-column:1/-1}
10
- input,select,textarea,button{font:inherit}input,select,textarea{width:100%;background:#0b1720;color:#fff;border:1px solid #426073;border-radius:7px;padding:9px;margin:5px 0 12px}textarea{min-height:70px}label{display:block;color:#c1d0d8;font-weight:600;font-size:13px}button{border:0;border-radius:7px;background:#26bfa4;color:#081914;padding:9px 14px;font-weight:700;cursor:pointer;margin:4px 7px 4px 0}button.secondary{background:#304b5a;color:#e8f0f2}button.danger{background:#6d3541;color:#fff}button:disabled{opacity:.5;cursor:not-allowed}.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.row{border-top:1px solid #294454;padding:12px 0;display:flex;align-items:center;justify-content:space-between;gap:12px}.muted{font-size:12px;color:#93acb9}#status{padding:8px 5vw;color:#8ee7ce;min-height:35px}#status.error{color:#ff9e9e}code{color:#a6e7db}[hidden]{display:none!important}@media(max-width:800px){main{display:block}.card{margin-bottom:20px}.grid{display:block}}
7
+ :root{font:15px system-ui,sans-serif;color:#e8f0f2;background:#0b1218}*{box-sizing:border-box}body{margin:0}header{padding:22px 5vw;background:#10202b;border-bottom:1px solid #234052;display:flex;justify-content:space-between;align-items:center;gap:20px}h1{font-size:21px;margin:0}h2{font-size:18px;margin:0 0 14px}h3{font-size:15px;margin:16px 0 6px}p{color:#a8bdc8;line-height:1.45}main{max-width:1200px;margin:30px auto;padding:0 20px;display:grid;grid-template-columns:1fr 1fr;gap:20px}.card{background:#14222c;border:1px solid #294454;border-radius:13px;padding:24px}.wide{grid-column:1/-1}input,select,textarea,button{font:inherit}input:not([type=checkbox]),select,textarea{width:100%;background:#0b1720;color:#fff;border:1px solid #426073;border-radius:7px;padding:9px;margin:5px 0 12px}textarea{min-height:70px}label{display:block;color:#c1d0d8;font-weight:600;font-size:13px}button{border:0;border-radius:7px;background:#26bfa4;color:#081914;padding:9px 14px;font-weight:700;cursor:pointer;margin:4px 7px 4px 0}button.secondary{background:#304b5a;color:#e8f0f2}button.danger{background:#6d3541;color:#fff}button:disabled{opacity:.5;cursor:not-allowed}.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.row{border-top:1px solid #294454;padding:12px 0;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.muted{font-size:12px;color:#93acb9}.choices{display:flex;flex-wrap:wrap;gap:8px 18px;margin:12px 0}.choices label{display:inline-flex;gap:7px;align-items:center;font-weight:400;font-size:14px}.choices input{width:auto;margin:0}.pill{display:inline-block;background:#234051;border-radius:20px;padding:3px 9px;font-size:12px;margin-left:8px}.actions{display:flex;flex-wrap:wrap;gap:3px}.help{background:#0d1e28;border-left:3px solid #26bfa4;padding:10px 14px;margin:12px 0}.review{white-space:pre-wrap;background:#0b1720;border:1px solid #426073;border-radius:7px;padding:14px;line-height:1.5}#status{padding:8px 5vw;color:#8ee7ce;min-height:35px}#status.error{color:#ff9e9e}code{color:#a6e7db}[hidden]{display:none!important}a{color:#8ee7ce}@media(max-width:800px){main{display:block}.card{margin-bottom:20px}.grid{display:block}}
11
8
  </style>
12
- <header><h1>IntegSec MCP Gateway</h1><span class="muted">Manage tools and engagement scope</span></header>
13
- <div id="status" role="status"></div>
9
+ <header><h1>IntegSec MCP Gateway</h1><span id="identity" class="muted">Loading session…</span></header>
10
+ <div id="status" role="status" aria-live="polite"></div>
14
11
  <main>
15
- <section id="token-card" class="card wide" hidden><h2>Administrator access</h2><p>Enter the local gateway admin token. It stays in this browser tab only.</p><div class="grid"><label>Admin token<input id="token" type="password" autocomplete="off"></label><div><br><button id="connect">Connect</button></div></div></section>
16
- <section id="users-card" class="card wide" hidden><h2>Team users</h2><p>Cloudflare Access signs users in. Only people on this list can use the gateway; administrators can also manage tools, scopes, and users. Bootstrap administrators are set in deployment configuration.</p><div id="users"></div><h3>Add a user</h3><div class="grid"><label>Email<input id="user-email" type="email" autocomplete="off" placeholder="tester@example.com"></label><label>Role<select id="user-role"><option value="tester">Tester: MCP tools</option><option value="admin">Administrator: manage gateway</option></select></label></div><button id="add-user">Add user</button></section>
17
- <section class="card"><h2>MCP servers</h2><div id="upstreams"></div><h3>Add a server</h3><div class="grid"><label>Name<input id="up-name" placeholder="burp"></label><label>Transport<select id="up-transport"><option value="stdio">Local process</option><option value="http">Streamable HTTP</option><option value="sse">SSE</option></select></label></div><label>Command or URL<input id="up-target" placeholder="burp-mcp-server or https://example.com/mcp"></label><label>Arguments, one per line<textarea id="up-args" placeholder="--port\n3000"></textarea></label><button id="add-upstream">Add server</button></section>
18
- <section class="card"><h2>Engagement scopes</h2><p>Save a draft, review it, then activate it. The active scope cannot be edited in place.</p><div id="scopes"></div><h3>Scope editor</h3><div class="grid"><label>Engagement ID<input id="engagement" placeholder="ENG-2026-001"></label><label>Client<input id="client" placeholder="Example Client"></label><label>Operator<input id="operator" placeholder="Test Team"></label><label>Starts (UTC)<input id="starts" type="datetime-local"></label><label>Ends (UTC)<input id="ends" type="datetime-local"></label><label>Execution profile<select id="profile"><option value="isolated">Isolated: local MCP only</option><option value="restricted">Restricted: approved hosts</option><option value="connected">Connected: network MCP allowed</option></select></label></div><p class="muted">Use one item per line in each field.</p><div class="grid"><label>IP ranges<textarea id="ips"></textarea></label><label>Domains<textarea id="domains"></textarea></label><label>URLs<textarea id="urls"></textarea></label><label>Cloud accounts<textarea id="cloud"></textarea></label><label>Excluded targets<textarea id="excluded-targets"></textarea></label><label>Authorized techniques<textarea id="techniques"></textarea></label><label>Excluded techniques<textarea id="excluded-techniques"></textarea></label><label>Allowed MCP hosts (restricted profile)<textarea id="hosts"></textarea></label></div><button id="save-scope">Save draft</button><button id="new-scope" class="secondary">Clear editor</button></section>
19
- <section class="card wide"><h2>Client connection</h2><p id="connection">Connect as an administrator to see the endpoint.</p><p class="muted">Local Burp tools must run on each tester’s own gateway or local MCP connection. A public gateway cannot launch Burp on a tester’s laptop.</p></section>
12
+ <section id="token-card" class="card wide" hidden><h2>Local administrator access</h2><p>Enter the local gateway admin token. It stays in this browser tab only.</p><div class="grid"><label>Admin token<input id="token" type="password" autocomplete="off"></label><div><br><button id="connect">Connect</button></div></div></section>
13
+ <section id="my-card" class="card wide" hidden><h2>My engagement</h2><p>Choose an approved engagement assigned to you. Your choice affects only your MCP calls. No engagement means no tools.</p><div class="actions"><button id="create-engagement" hidden>Create engagement</button><button id="import-policy" class="secondary" hidden>Import scope JSON</button><input id="policy-file" type="file" accept=".json,application/json" hidden><button id="clear-selection" class="secondary" hidden>Clear selection</button></div><div id="my-engagements"></div><p id="my-selection" class="help"></p></section>
14
+ <section id="users-card" class="card wide admin-only" hidden><h2>Team users</h2><p>Cloudflare Access handles sign-in. People must also be on this exact-email list and assigned to an approved engagement. Administrators manage users, servers and scopes.</p><div id="users"></div><h3>Add a user</h3><div class="grid"><label>Email<input id="user-email" type="email" autocomplete="off" placeholder="tester@example.com"></label><label>Role<select id="user-role"><option value="tester">Tester</option><option value="admin">Administrator</option></select></label></div><button id="add-user">Add user</button></section>
15
+ <section id="upstreams-card" class="card admin-only" hidden><h2>MCP servers</h2><p>Add an MCP server, then turn it on when its connection is ready. Adding a preset does not install the upstream software. Tools that run on a tester’s laptop must be added to that tester’s local gateway.</p><div id="upstreams"></div><h3>Curated servers</h3><div id="presets"></div><h3>Add a custom server</h3><div class="grid"><label>Name<input id="up-name" placeholder="burp"></label><label>Transport<select id="up-transport"><option value="stdio">Local process</option><option value="http">Streamable HTTP</option><option value="sse">SSE</option></select></label></div><label>Command or URL<input id="up-target" placeholder="installed-command or https://tools.example.com/mcp"></label><label>Arguments, one per line<textarea id="up-args" placeholder="mcp"></textarea></label><button id="add-upstream">Add server</button></section>
16
+ <section id="scopes-card" class="card admin-only" hidden><h2>Engagement scopes</h2><p>Create or import a scope, save it, review and approve it, then assign users. Approved scopes are immutable; duplicate one to revise it. Every user chooses their own current engagement above.</p><div id="scopes"></div></section>
17
+ <section id="editor-card" class="card wide admin-only" hidden><h2>Scope editor</h2><div id="editor-help" class="help">Start with a blank form or load a template. Enter the real client and authorized targets before approval.</div><div class="actions"><button id="example-web" class="secondary">Web template</button><button id="example-internal" class="secondary">Network template</button><button id="example-cloud" class="secondary">Cloud template</button></div><div class="grid"><label>Engagement ID<input id="engagement" maxlength="64" placeholder="Engagement identifier"></label><label>Client<input id="client" maxlength="120" placeholder="Authorized client name"></label><label>Operator<input id="operator" maxlength="120" placeholder="Testing team or operator"></label><label>Starts (your local time)<input id="starts" type="datetime-local"></label><label>Ends (your local time)<input id="ends" type="datetime-local"></label><label>Execution profile<select id="profile"><option value="isolated">Isolated: local MCP only</option><option value="restricted">Restricted: approved MCP hosts</option><option value="connected">Connected: network MCP allowed</option></select></label></div><p class="muted">Use one authorized item per line. IPs require CIDR notation. Domains are hostnames. URLs must start with https:// or http://. Cloud accounts use aws:123456789012, azure:subscription-uuid, or gcp:project-id.</p><div class="grid"><label>IP ranges<textarea id="ips" placeholder="Authorized CIDR range"></textarea></label><label>Domains<textarea id="domains" placeholder="Authorized client domain"></textarea></label><label>URLs<textarea id="urls" placeholder="Authorized client URL"></textarea></label><label>Cloud accounts<textarea id="cloud" placeholder="Authorized cloud account"></textarea></label><label>Excluded targets<textarea id="excluded-targets" placeholder="Explicitly excluded target"></textarea></label><label>Allowed MCP hosts (restricted profile)<textarea id="hosts" placeholder="Approved MCP hostname"></textarea></label></div><h3>Authorized techniques</h3><p class="muted">Choose only activities the signed rules of engagement permit. Unselected categories are blocked. Unrecognized tool categories are blocked until mapped.</p><div id="techniques" class="choices"></div><h3>Explicit exclusions</h3><div id="excluded-techniques" class="choices"></div><div class="actions"><button id="save-scope">Save</button><button id="approve-current" class="secondary" hidden>Approve scope</button><button id="assign-me" class="secondary" hidden>Assign to me</button><button id="use-current" class="secondary" hidden>Use engagement</button><button id="new-scope" class="secondary">New scope</button></div><h3>Review before approval</h3><div id="scope-review" class="review"></div></section>
18
+ <section class="card wide"><h2>Client connection</h2><p id="connection">Sign in to see your MCP endpoint.</p><p class="muted">Local Burp tools run on each tester’s laptop through their local gateway. The hosted gateway cannot reach a tester’s laptop automatically.</p><p class="muted">Cloudflare Access supports passkeys for faster repeat sign-in when enabled for this application. Email codes remain available for enrollment and recovery.</p></section>
20
19
  </main>
21
20
  <script src="/admin.js" defer></script>
22
21
  </html>
@@ -1,109 +1,284 @@
1
1
  const $ = (id) => document.getElementById(id);
2
+ const token = $('token');
3
+ token.value = sessionStorage.getItem('gateway-token') || '';
4
+ let mode = 'token';
5
+ let role = 'admin';
6
+ let email = null;
2
7
  let state = null;
3
8
  let editing = null;
4
- let mode = 'token';
9
+ let viewingScopeId = null;
10
+ let dirty = false;
5
11
  let bootstrapAdmins = new Set();
6
- const token = $('token');
7
- token.value = sessionStorage.getItem('gateway-token') || '';
12
+ const techniques = [
13
+ ['recon', 'Reconnaissance and discovery'], ['web_app', 'Web and API testing'],
14
+ ['exploitation', 'Exploitation'], ['cloud_audit', 'Cloud assessment'],
15
+ ['dos', 'Denial of service'], ['destructive', 'Destructive actions'],
16
+ ['social_engineering', 'Social engineering'],
17
+ ];
18
+ const presets = [
19
+ { name: 'burp', label: 'Burp Suite', transport: 'sse', url: 'http://127.0.0.1:9876', local: true, source: 'https://github.com/PortSwigger/mcp-server', note: 'Enable the official MCP extension inside the tester’s local Burp.' },
20
+ { name: 'semgrep', label: 'Semgrep', transport: 'stdio', command: 'semgrep', args: ['mcp'], local: true, source: 'https://github.com/semgrep/semgrep', note: 'Install and configure the official Semgrep CLI first.' },
21
+ { name: 'trivy', label: 'Trivy', transport: 'stdio', command: 'trivy', args: ['mcp'], local: true, source: 'https://github.com/aquasecurity/trivy-mcp', note: 'Install Trivy and its official MCP plugin first; pin and review your version.' },
22
+ { name: 'snyk', label: 'Snyk', transport: 'stdio', command: 'snyk', args: ['mcp', '-t', 'stdio'], local: true, source: 'https://github.com/snyk/studio-mcp', note: 'Install the Snyk CLI and authenticate locally first.' },
23
+ { label: 'OWASP ZAP', source: 'https://www.zaproxy.org/docs/desktop/addons/mcp-integration/mcp-options/', note: 'Reference only: its MCP security key needs authenticated upstream support.' },
24
+ { label: 'PortSwigger DAST', source: 'https://portswigger.net/burp/documentation/dast/user-guide/using-ai/mcp-server', note: 'Reference only: its hosted MCP endpoint requires an API key.' },
25
+ { label: 'Prowler Cloud', source: 'https://prowler.com/prowler-cloud', note: 'Reference only: its hosted MCP endpoint requires an API key.' },
26
+ ];
8
27
 
9
- function status(message, error = false) { const node = $('status'); node.textContent = message; node.classList.toggle('error', error); }
28
+ function status(message, error = false) { $('status').textContent = message; $('status').classList.toggle('error', error); }
10
29
  async function api(path, method = 'GET', data) {
11
- const response = await fetch(path, { method, credentials: 'same-origin', cache: 'no-store', headers: { ...(mode === 'token' ? { Authorization: `Bearer ${token.value}` } : {}), ...(data ? { 'Content-Type': 'application/json' } : {}) }, body: data ? JSON.stringify(data) : undefined });
12
- const result = await response.json();
30
+ const response = await fetch(path, { method, credentials: 'same-origin', cache: 'no-store', headers: {
31
+ ...(mode === 'token' ? { Authorization: `Bearer ${token.value}` } : {}),
32
+ ...(data !== undefined ? { 'Content-Type': 'application/json' } : {}),
33
+ }, body: data !== undefined ? JSON.stringify(data) : undefined });
34
+ const result = await response.json().catch(() => ({}));
13
35
  if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
14
36
  return result;
15
37
  }
16
- const lines = (id) => $(id).value.split(/\r?\n/).map(x => x.trim()).filter(Boolean);
38
+ const lines = (id) => $(id).value.split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
17
39
  const setLines = (id, values) => { $(id).value = values.join('\n'); };
18
40
  const make = (tag, value) => { const node = document.createElement(tag); node.textContent = value; return node; };
19
- const button = (label, action, style = 'secondary') => { const node = make('button', label); node.className = style; node.addEventListener('click', () => action().catch(e => status(e.message, true))); return node; };
41
+ function button(label, action, style = 'secondary') {
42
+ const node = make('button', label); node.className = style;
43
+ node.addEventListener('click', () => Promise.resolve().then(action).catch((error) => status(error.message, true)));
44
+ return node;
45
+ }
46
+ function choiceList(container, name) {
47
+ const result = [];
48
+ for (const [value, label] of techniques) {
49
+ const option = make('label', ''); const input = document.createElement('input');
50
+ input.type = 'checkbox'; input.name = name; input.value = value;
51
+ option.append(input, make('span', label)); container.append(option); result.push(input);
52
+ }
53
+ return result;
54
+ }
55
+ const authorizedChoices = choiceList($('techniques'), 'authorized-technique');
56
+ const excludedChoices = choiceList($('excluded-techniques'), 'excluded-technique');
57
+ const checked = (choices) => choices.filter((item) => item.checked).map((item) => item.value);
58
+ function setChecked(choices, values) { for (const item of choices) item.checked = values.includes(item.value); }
59
+ function localDate(value) { const date = new Date(value); return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16); }
60
+ function draftFromForm() {
61
+ if (!$('starts').value || !$('ends').value) throw new Error('Enter both engagement dates.');
62
+ const start = new Date($('starts').value); const end = new Date($('ends').value);
63
+ if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime())) throw new Error('Enter valid engagement dates.');
64
+ return { profile: $('profile').value, allowedHosts: lines('hosts'), manifest: {
65
+ engagement_id: $('engagement').value.trim(), client: $('client').value.trim(), operator: $('operator').value.trim(),
66
+ authorized_targets: { ip_ranges: lines('ips'), domains: lines('domains'), urls: lines('urls'), cloud_accounts: lines('cloud') },
67
+ excluded_targets: lines('excluded-targets'), authorized_techniques: checked(authorizedChoices),
68
+ excluded_techniques: checked(excludedChoices), engagement_window: { start: start.toISOString(), end: end.toISOString() },
69
+ } };
70
+ }
71
+ function summarize(scope) {
72
+ const m = scope.manifest;
73
+ return `Engagement: ${m.engagement_id || '(missing)'}\nClient: ${m.client || '(missing)'} · Operator: ${m.operator || '(missing)'}\nWindow: ${m.engagement_window.start} → ${m.engagement_window.end}\nProfile: ${scope.profile}\nTargets: ${Object.values(m.authorized_targets).flat().join(', ') || '(none)'}\nExcluded targets: ${m.excluded_targets.join(', ') || '(none)'}\nAuthorized techniques: ${m.authorized_techniques.join(', ') || '(none)'}\nExcluded techniques: ${m.excluded_techniques.join(', ') || '(none)'}\nAllowed MCP hosts: ${scope.allowedHosts.join(', ') || '(none)'}`;
74
+ }
75
+ function downloadPolicy(scope) {
76
+ const policy = { ...scope.manifest, gateway: { profile: scope.profile, allowed_hosts: scope.allowedHosts || [] } };
77
+ const filename = `${(scope.manifest.engagement_id || 'engagement').replace(/[^a-z0-9_-]/gi, '_')}.scope.json`;
78
+ const url = URL.createObjectURL(new Blob([`${JSON.stringify(policy, null, 2)}\n`], { type: 'application/json' }));
79
+ const link = document.createElement('a'); link.href = url; link.download = filename; document.body.append(link); link.click(); link.remove();
80
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
81
+ }
82
+ function updateEditorActions() {
83
+ const current = state?.scopes.find((scope) => scope.id === viewingScopeId);
84
+ $('approve-current').hidden = !(current?.status === 'draft');
85
+ $('approve-current').disabled = dirty;
86
+ $('assign-me').hidden = !(role === 'admin' && mode === 'access' && current?.status === 'approved' && email && !current.assignedUserEmails.includes(email));
87
+ const windowOpen = current && Date.now() >= Date.parse(current.manifest.engagement_window.start) && Date.now() <= Date.parse(current.manifest.engagement_window.end);
88
+ $('use-current').hidden = !(current?.status === 'approved' && windowOpen && (mode === 'token' || current.assignedUserEmails.includes(email)) && !(mode === 'access' && current.profile === 'isolated'));
89
+ }
90
+ function newScope() {
91
+ if (dirty && !confirm('Discard unsaved scope changes?')) return;
92
+ clearEditor(); $('editor-card').scrollIntoView({ behavior: 'smooth' }); $('engagement').focus();
93
+ }
94
+ async function approveScope(scope) {
95
+ if (scope.id === viewingScopeId && dirty) throw new Error('Save your changes before approving this scope.');
96
+ if (!confirm(`Approve this scope for ${scope.manifest.engagement_id}?\n\n${summarize(scope)}\n\nApproval makes it eligible for assignment. No user is automatically assigned.`)) return;
97
+ await api(`/api/scopes/${scope.id}/approve`, 'POST'); await refresh();
98
+ const approved = state.scopes.find((item) => item.id === scope.id); if (approved) loadScope(approved);
99
+ status('Scope approved. Assign testers, then each tester selects the engagement.');
100
+ }
101
+ function reviewForm() {
102
+ try {
103
+ const scope = draftFromForm(); const m = scope.manifest; const warnings = [];
104
+ if (/^example/i.test(m.engagement_id) || /^example client$/i.test(m.client)) warnings.push('Replace the example engagement ID and client.');
105
+ if (m.authorized_targets.domains.some((value) => value.includes('example.'))) warnings.push('Replace documentation domains with real authorized domains.');
106
+ if (m.authorized_targets.urls.some((value) => value.includes('example.'))) warnings.push('Replace documentation URLs with real authorized URLs.');
107
+ if (Date.parse(m.engagement_window.end) <= Date.now()) warnings.push('Choose an end time in the future.');
108
+ $('scope-review').textContent = `${warnings.length ? `Before approval: ${warnings.join(' ')}\n\n` : ''}${summarize(scope)}`;
109
+ } catch (error) { $('scope-review').textContent = error.message; }
110
+ updateEditorActions();
111
+ }
112
+ for (const field of $('editor-card').querySelectorAll('input,select,textarea')) field.addEventListener('input', () => { dirty = true; reviewForm(); });
20
113
 
21
114
  async function refresh() {
22
- state = await api('/api/state');
23
- if (mode === 'access') {
24
- const users = $('users'); users.replaceChildren();
25
- for (const item of state.users) {
26
- const row = make('div', ''); row.className = 'row';
27
- const bootstrap = bootstrapAdmins.has(item.email);
28
- row.append(make('div', `${item.email} · ${item.role}${bootstrap ? ' · Bootstrap administrator' : ''}`));
29
- if (!bootstrap) {
30
- row.append(button(item.role === 'admin' ? 'Make tester' : 'Make admin', async () => { await api(`/api/users/${item.id}`, 'PATCH', { role: item.role === 'admin' ? 'tester' : 'admin' }); await refresh(); }));
31
- row.append(button('Remove', async () => { if (confirm(`Remove ${item.email}?`)) { await api(`/api/users/${item.id}`, 'DELETE'); await refresh(); } }, 'danger'));
32
- }
33
- users.append(row);
115
+ const mine = await api('/api/engagements');
116
+ $('my-card').hidden = false;
117
+ $('create-engagement').hidden = role !== 'admin';
118
+ $('import-policy').hidden = role !== 'admin';
119
+ const list = $('my-engagements'); list.replaceChildren();
120
+ for (const scope of mine.engagements) {
121
+ const active = mine.selectedScopeId === scope.id;
122
+ const windowOpen = Date.now() >= Date.parse(scope.manifest.engagement_window.start) && Date.now() <= Date.parse(scope.manifest.engagement_window.end);
123
+ const row = make('div', ''); row.className = 'row';
124
+ row.append(make('span', `${scope.manifest.engagement_id} · ${scope.manifest.client} · ${scope.profile} · ${windowOpen ? 'window open' : 'window closed'}${active ? ' · SELECTED' : ''}`));
125
+ const select = button(active ? 'Selected' : 'Select', async () => { await api('/api/my-scope', 'POST', { scopeId: scope.id }); await refresh(); });
126
+ select.disabled = active || !windowOpen;
127
+ row.append(select, button('Download scope JSON', () => downloadPolicy(scope))); list.append(row);
128
+ }
129
+ if (!mine.engagements.length) list.append(make('p', role === 'admin' ? 'No engagement assigned to you yet. Create one, or assign yourself to an approved scope below.' : 'No approved engagements have been assigned to you yet. Ask an administrator.'));
130
+ const selected = mine.engagements.find((item) => item.id === mine.selectedScopeId);
131
+ $('clear-selection').hidden = !selected;
132
+ const selectedWindowOpen = selected && Date.now() >= Date.parse(selected.manifest.engagement_window.start) && Date.now() <= Date.parse(selected.manifest.engagement_window.end);
133
+ $('my-selection').textContent = selected ? selectedWindowOpen ? `Current engagement: ${selected.manifest.engagement_id}. Its scope applies only to your MCP calls.` : `Engagement ${selected.manifest.engagement_id} is outside its approved window. Your MCP client will see no tools.` : 'No engagement selected. Your MCP client will see no tools.';
134
+ $('connection').textContent = `MCP endpoint: ${location.origin}/mcp. ${selected ? `Current engagement: ${selected.manifest.engagement_id}.` : 'Select an engagement before using tools.'}`;
135
+ if (role === 'admin') {
136
+ state = await api('/api/state');
137
+ renderUsers(); renderUpstreams(); renderPresets(); renderScopes();
138
+ }
139
+ status('Gateway state loaded.');
140
+ }
141
+ function renderUsers() {
142
+ if (mode !== 'access') return;
143
+ const container = $('users'); container.replaceChildren();
144
+ for (const user of state.users) {
145
+ const row = make('div', ''); row.className = 'row'; const bootstrap = bootstrapAdmins.has(user.email);
146
+ row.append(make('span', `${user.email} · ${user.role}${bootstrap ? ' · bootstrap administrator' : ''}`));
147
+ if (!bootstrap) {
148
+ row.append(button(user.role === 'admin' ? 'Make tester' : 'Make admin', async () => { await api(`/api/users/${user.id}`, 'PATCH', { role: user.role === 'admin' ? 'tester' : 'admin' }); await refresh(); }));
149
+ row.append(button('Remove', async () => { if (confirm(`Remove ${user.email} and all engagement assignments?`)) { await api(`/api/users/${user.id}`, 'DELETE'); await refresh(); } }, 'danger'));
34
150
  }
151
+ container.append(row);
35
152
  }
36
- const up = $('upstreams'); up.replaceChildren();
153
+ }
154
+ function renderUpstreams() {
155
+ const container = $('upstreams'); container.replaceChildren();
37
156
  for (const item of state.upstreams) {
38
157
  const row = make('div', ''); row.className = 'row';
39
- const label = make('div', `${item.name} · ${item.transport} · ${item.enabled ? 'Enabled' : 'Disabled'}`);
40
- label.appendChild(make('div', item.transport === 'stdio' ? item.command : item.url));
41
- row.append(label, button(item.enabled ? 'Disable' : 'Enable', async () => { await api(`/api/upstreams/${item.id}`, 'PATCH', { enabled: !item.enabled }); await refresh(); }));
42
- row.append(button('Remove', async () => { if (confirm(`Remove ${item.name}?`)) { await api(`/api/upstreams/${item.id}`, 'DELETE'); await refresh(); } }, 'danger'));
43
- up.append(row);
158
+ row.append(make('span', `${item.name} · ${item.transport} · ${item.enabled ? 'On' : 'Off'} · ${item.transport === 'stdio' ? item.command : item.url}`));
159
+ row.append(button(item.enabled ? 'Turn off' : 'Turn on', async () => { await api(`/api/upstreams/${item.id}`, 'PATCH', { enabled: !item.enabled }); await refresh(); }));
160
+ row.append(button('Remove', async () => { if (confirm(`Remove server ${item.name}?`)) { await api(`/api/upstreams/${item.id}`, 'DELETE'); await refresh(); } }, 'danger'));
161
+ container.append(row);
162
+ }
163
+ }
164
+ function renderPresets() {
165
+ const container = $('presets'); container.replaceChildren();
166
+ for (const preset of presets) {
167
+ const row = make('div', ''); row.className = 'row'; const detail = make('div', `${preset.label} · ${preset.note} `);
168
+ const link = make('a', 'Official information'); link.href = preset.source; link.target = '_blank'; link.rel = 'noopener noreferrer'; detail.append(link);
169
+ row.append(detail);
170
+ if (preset.name) {
171
+ if (mode === 'access' && preset.local) row.append(make('span', 'Local gateway only'));
172
+ else if (state.upstreams.some((item) => item.name === preset.name)) row.append(make('span', 'Added'));
173
+ else row.append(button('Add server', async () => { await api('/api/upstreams', 'POST', { name: preset.name, enabled: false, transport: preset.transport, command: preset.command, args: preset.args || [], url: preset.url }); await refresh(); status(`${preset.label} added and off. Turn it on when ready.`); }));
174
+ }
175
+ container.append(row);
44
176
  }
45
- const scopes = $('scopes'); scopes.replaceChildren();
46
- for (const item of state.scopes) {
47
- const active = item.id === state.activeScopeId;
177
+ }
178
+ function renderScopes() {
179
+ const container = $('scopes'); container.replaceChildren();
180
+ for (const scope of state.scopes) {
48
181
  const row = make('div', ''); row.className = 'row';
49
- row.append(make('div', `${item.manifest.engagement_id} · ${item.profile}${active ? ' · ACTIVE' : ''}`));
50
- row.append(button('View', async () => loadScope(item)));
51
- if (!active) {
52
- row.append(button('Activate', async () => { await api(`/api/scopes/${item.id}/activate`, 'POST'); await refresh(); }));
53
- row.append(button('Delete', async () => { if (confirm(`Delete draft ${item.manifest.engagement_id}?`)) { await api(`/api/scopes/${item.id}`, 'DELETE'); await refresh(); } }, 'danger'));
182
+ const label = make('div', `${scope.manifest.engagement_id} · ${scope.manifest.client}`);
183
+ label.append(make('span', scope.status === 'draft' ? 'Needs approval' : scope.status)); label.lastChild.className = 'pill';
184
+ row.append(label);
185
+ const actions = make('div', ''); actions.className = 'actions';
186
+ actions.append(button(scope.status === 'draft' ? 'Edit' : 'View', () => loadScope(scope)));
187
+ actions.append(button('Duplicate', async () => { const copy = await api(`/api/scopes/${scope.id}/duplicate`, 'POST'); await refresh(); loadScope(copy); status('Draft copy created. Edit and approve it when ready.'); }));
188
+ actions.append(button('Download JSON', () => downloadPolicy(scope)));
189
+ if (scope.status === 'draft') actions.append(button('Review and approve', () => approveScope(scope)));
190
+ actions.append(button('Delete', async () => { if (confirm(`Delete ${scope.manifest.engagement_id} (${scope.status})? Assigned users will lose this engagement.`)) { await api(`/api/scopes/${scope.id}`, 'DELETE'); await refresh(); } }, 'danger'));
191
+ row.append(actions); container.append(row);
192
+ if (scope.status === 'approved' && mode === 'access') {
193
+ const assignment = make('div', ''); assignment.className = 'choices';
194
+ const inputs = state.users.map((user) => { const label = make('label', ''); const input = document.createElement('input'); input.type = 'checkbox'; input.value = user.email; input.checked = scope.assignedUserEmails.includes(user.email); label.append(input, make('span', user.email)); assignment.append(label); return input; });
195
+ assignment.append(button('Save assignments', async () => { await api(`/api/scopes/${scope.id}/assignments`, 'PUT', { emails: inputs.filter((item) => item.checked).map((item) => item.value) }); await refresh(); }));
196
+ container.append(assignment);
54
197
  }
55
- scopes.append(row);
56
198
  }
57
- $('connection').textContent = `MCP endpoint: ${location.origin}/mcp. ${state.activeScopeId ? 'An engagement scope is active.' : 'Activate a scope before connecting clients.'}`;
58
- status('Gateway state loaded.');
199
+ if (!state.scopes.length) container.append(make('p', 'No scopes yet. Use the editor below to create a draft.'));
59
200
  }
60
- function loadScope(item) {
61
- editing = item.id === state.activeScopeId ? null : item.id;
62
- const m = item.manifest;
201
+ function loadScope(scope) {
202
+ editing = scope.status === 'draft' ? scope.id : null; viewingScopeId = scope.id; dirty = false;
203
+ const m = scope.manifest;
63
204
  $('engagement').value = m.engagement_id; $('client').value = m.client; $('operator').value = m.operator;
64
- $('starts').value = m.engagement_window.start.slice(0,16); $('ends').value = m.engagement_window.end.slice(0,16);
65
- $('profile').value = item.profile; setLines('ips', m.authorized_targets.ip_ranges); setLines('domains', m.authorized_targets.domains);
205
+ $('starts').value = localDate(m.engagement_window.start); $('ends').value = localDate(m.engagement_window.end);
206
+ $('profile').value = scope.profile; setLines('ips', m.authorized_targets.ip_ranges); setLines('domains', m.authorized_targets.domains);
66
207
  setLines('urls', m.authorized_targets.urls); setLines('cloud', m.authorized_targets.cloud_accounts);
67
- setLines('excluded-targets', m.excluded_targets); setLines('techniques', m.authorized_techniques);
68
- setLines('excluded-techniques', m.excluded_techniques); setLines('hosts', item.allowedHosts);
69
- status(item.id === state.activeScopeId ? 'Viewing active scope. Saving creates a new draft.' : 'Draft loaded for editing.');
208
+ setLines('excluded-targets', m.excluded_targets); setLines('hosts', scope.allowedHosts);
209
+ setChecked(authorizedChoices, m.authorized_techniques); setChecked(excludedChoices, m.excluded_techniques);
210
+ for (const field of $('editor-card').querySelectorAll('input,select,textarea')) field.disabled = scope.status !== 'draft';
211
+ $('save-scope').disabled = scope.status !== 'draft';
212
+ $('editor-help').textContent = scope.status === 'draft' ? 'Editing a saved scope. Replace any example details and choose a future end date before approval.' : 'Viewing an immutable scope. Choose Duplicate to create an editable scope.';
213
+ reviewForm(); $('editor-card').scrollIntoView({ behavior: 'smooth' });
70
214
  }
71
- $('connect').onclick = () => { sessionStorage.setItem('gateway-token', token.value); refresh().catch(e => status(e.message, true)); };
72
- $('add-user').onclick = async () => {
73
- try {
74
- await api('/api/users', 'POST', { email: $('user-email').value.trim(), role: $('user-role').value });
75
- $('user-email').value = ''; await refresh();
76
- } catch (e) { status(e.message, true); }
77
- };
78
- $('add-upstream').onclick = async () => {
79
- try {
80
- const transport = $('up-transport').value;
81
- await api('/api/upstreams', 'POST', { name: $('up-name').value.trim(), enabled: true, transport,
82
- ...(transport === 'stdio' ? { command: $('up-target').value.trim(), args: lines('up-args') } : { url: $('up-target').value.trim(), args: [] }) });
83
- $('up-name').value = ''; $('up-target').value = ''; $('up-args').value = ''; await refresh();
84
- } catch (e) { status(e.message, true); }
85
- };
86
- $('new-scope').onclick = () => { editing = null; for (const id of ['engagement','client','operator','starts','ends','ips','domains','urls','cloud','excluded-targets','techniques','excluded-techniques','hosts']) $(id).value = ''; $('profile').value = 'isolated'; status('New draft.'); };
87
- $('save-scope').onclick = async () => {
215
+ function clearEditor() {
216
+ editing = null; viewingScopeId = null; dirty = false;
217
+ for (const id of ['engagement', 'client', 'operator', 'starts', 'ends', 'ips', 'domains', 'urls', 'cloud', 'excluded-targets', 'hosts']) $(id).value = '';
218
+ $('profile').value = mode === 'access' ? 'connected' : 'isolated'; setChecked(authorizedChoices, []); setChecked(excludedChoices, []);
219
+ for (const field of $('editor-card').querySelectorAll('input,select,textarea')) field.disabled = false;
220
+ $('save-scope').disabled = false; $('editor-help').textContent = 'New scope. Enter the client, exact authorized targets, window and techniques, then Save.'; reviewForm();
221
+ }
222
+ function loadExample(kind) {
223
+ if (dirty && !confirm('Discard unsaved scope changes and load a template?')) return;
224
+ clearEditor();
225
+ if (kind === 'web') setChecked(authorizedChoices, ['recon', 'web_app']);
226
+ if (kind === 'internal') setChecked(authorizedChoices, ['recon']);
227
+ if (kind === 'cloud') setChecked(authorizedChoices, ['cloud_audit']);
228
+ $('editor-help').textContent = 'Template loaded. Enter your real engagement ID, client, operator, authorized targets and signed time window. Review the selected techniques against the rules of engagement.';
229
+ reviewForm();
230
+ }
231
+ $('connect').onclick = () => { sessionStorage.setItem('gateway-token', token.value); refresh().catch((error) => status(error.message, true)); };
232
+ $('create-engagement').onclick = newScope;
233
+ $('import-policy').onclick = () => $('policy-file').click();
234
+ $('policy-file').onchange = async () => {
235
+ const file = $('policy-file').files?.[0]; $('policy-file').value = '';
236
+ if (!file) return;
88
237
  try {
89
- const scope = { profile: $('profile').value, allowedHosts: lines('hosts'), manifest: {
90
- engagement_id: $('engagement').value.trim(), client: $('client').value.trim(), operator: $('operator').value.trim(),
91
- authorized_targets: { ip_ranges: lines('ips'), domains: lines('domains'), urls: lines('urls'), cloud_accounts: lines('cloud') },
92
- excluded_targets: lines('excluded-targets'), authorized_techniques: lines('techniques'), excluded_techniques: lines('excluded-techniques'),
93
- engagement_window: { start: new Date($('starts').value).toISOString(), end: new Date($('ends').value).toISOString() },
94
- } };
95
- await api(editing ? `/api/scopes/${editing}` : '/api/scopes', editing ? 'PUT' : 'POST', scope);
96
- editing = null; await refresh(); status('Draft saved. Review it before activation.');
97
- } catch (e) { status(e.message, true); }
238
+ if (!file.name.toLowerCase().endsWith('.json') || file.size > 1024 * 1024 || file.size === 0) throw new Error('Choose a nonempty JSON policy file under 1 MB.');
239
+ const input = JSON.parse(await file.text());
240
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Policy file must contain a JSON object.');
241
+ const manifest = input.manifest || input;
242
+ const profile = input.profile || input.gateway?.profile || (mode === 'access' ? 'connected' : 'isolated');
243
+ const allowedHosts = input.allowedHosts || input.gateway?.allowed_hosts || [];
244
+ const imported = await api('/api/scopes', 'POST', { manifest, profile, allowedHosts });
245
+ await refresh(); loadScope(imported);
246
+ status('Policy imported as a saved scope. Review and approve it before assigning users.');
247
+ } catch (error) { status(`Could not import policy: ${error.message}`, true); }
98
248
  };
249
+ $('clear-selection').onclick = () => api('/api/my-scope', 'POST', { scopeId: null }).then(refresh).catch((error) => status(error.message, true));
250
+ $('add-user').onclick = async () => { try { await api('/api/users', 'POST', { email: $('user-email').value.trim(), role: $('user-role').value }); $('user-email').value = ''; await refresh(); } catch (error) { status(error.message, true); } };
251
+ $('add-upstream').onclick = async () => { try { const transport = $('up-transport').value; await api('/api/upstreams', 'POST', { name: $('up-name').value.trim(), enabled: false, transport,
252
+ ...(transport === 'stdio' ? { command: $('up-target').value.trim(), args: lines('up-args') } : { url: $('up-target').value.trim(), args: [] }) });
253
+ $('up-name').value = ''; $('up-target').value = ''; $('up-args').value = ''; await refresh(); status('Server added and off. Turn it on when ready.'); } catch (error) { status(error.message, true); } };
254
+ $('new-scope').onclick = newScope;
255
+ for (const kind of ['web', 'internal', 'cloud']) $(`example-${kind}`).onclick = () => loadExample(kind);
256
+ $('save-scope').onclick = async () => { try { if (viewingScopeId && !editing) throw new Error('Duplicate an approved or archived scope to edit it.');
257
+ const scope = draftFromForm(); const result = await api(editing ? `/api/scopes/${editing}` : '/api/scopes', editing ? 'PUT' : 'POST', scope);
258
+ const saved = editing ? result.scopes.find((item) => item.id === editing) : result;
259
+ await refresh(); loadScope(saved); status('Scope saved. Review it, then approve and assign users.');
260
+ } catch (error) { status(error.message, true); } };
261
+ $('approve-current').onclick = () => { const scope = state?.scopes.find((item) => item.id === viewingScopeId); if (scope) approveScope(scope).catch((error) => status(error.message, true)); };
262
+ $('assign-me').onclick = async () => { try {
263
+ const scope = state?.scopes.find((item) => item.id === viewingScopeId);
264
+ if (!scope || !email) throw new Error('Select an approved scope first.');
265
+ await api(`/api/scopes/${scope.id}/assignments`, 'PUT', { emails: [...new Set([...scope.assignedUserEmails, email])] });
266
+ await refresh(); loadScope(state.scopes.find((item) => item.id === scope.id)); status('Engagement assigned to you. Choose Use engagement when its window opens.');
267
+ } catch (error) { status(error.message, true); } };
268
+ $('use-current').onclick = async () => { try {
269
+ if (!viewingScopeId) throw new Error('Select an approved scope first.');
270
+ await api('/api/my-scope', 'POST', { scopeId: viewingScopeId }); await refresh(); status('Engagement selected for your MCP calls.');
271
+ } catch (error) { status(error.message, true); } };
99
272
  async function initialize() {
100
273
  const response = await fetch('/api/session', { credentials: 'same-origin', cache: 'no-store' });
101
- const session = await response.json();
102
- if (!response.ok) throw new Error(session.error || `HTTP ${response.status}`);
103
- mode = session.mode;
274
+ const session = await response.json(); if (!response.ok) throw new Error(session.error || `HTTP ${response.status}`);
275
+ mode = session.mode; role = session.role || 'admin'; email = session.email || null;
104
276
  bootstrapAdmins = new Set(session.bootstrapAdminEmails || []);
277
+ $('identity').textContent = email ? `${email} · ${role}` : 'Local gateway';
105
278
  $('token-card').hidden = mode !== 'token';
106
- $('users-card').hidden = mode !== 'access';
279
+ for (const card of document.querySelectorAll('.admin-only')) card.hidden = role !== 'admin' || (card.id === 'users-card' && mode !== 'access');
280
+ clearEditor();
107
281
  if (mode === 'access' || token.value) await refresh();
282
+ reviewForm();
108
283
  }
109
- initialize().catch(e => status(e.message, true));
284
+ initialize().catch((error) => status(error.message, true));
@@ -9,7 +9,6 @@ export declare class GatewayRuntime {
9
9
  private clients;
10
10
  private pending;
11
11
  private proxies;
12
- private proxyScope?;
13
12
  constructor(store: GatewayStore, hosted: boolean, requiredAuditSink?: ((entry: AuditEntry) => Promise<void>) | undefined, isActorAllowed?: ((email?: string) => boolean) | undefined);
14
13
  sync(): Promise<void>;
15
14
  private scopeProxy;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/gateway/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAiE,MAAM,YAAY,CAAC;AAIzG,qBAAa,cAAc;IAMb,QAAQ,CAAC,KAAK,EAAE,YAAY;IAAE,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAW,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IAAwC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IALvL,OAAO,CAAC,OAAO,CAA4D;IAC3E,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,UAAU,CAAC,CAAc;gBAEZ,KAAK,EAAE,YAAY,EAAmB,MAAM,EAAE,OAAO,EAAmB,iBAAiB,CAAC,GAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,aAAA,EAAmB,cAAc,CAAC,GAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,aAAA;IAE9M,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,CAAC,UAAU;YAcJ,SAAS;IA2BvB,OAAO,CAAC,SAAS;IAOjB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM;IAmE9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/gateway/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAKnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAiE,MAAM,YAAY,CAAC;AAIzG,qBAAa,cAAc;IAKb,QAAQ,CAAC,KAAK,EAAE,YAAY;IAAE,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAW,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IAAwC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAJvL,OAAO,CAAC,OAAO,CAA4D;IAC3E,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,OAAO,CAA4C;gBAEtC,KAAK,EAAE,YAAY,EAAmB,MAAM,EAAE,OAAO,EAAmB,iBAAiB,CAAC,GAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,aAAA,EAAmB,cAAc,CAAC,GAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,aAAA;IAE9M,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,OAAO,CAAC,UAAU;YAWJ,SAAS;IA2BvB,OAAO,CAAC,SAAS;IAOjB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM;IAyE9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}