airlock-bot 0.2.35 → 0.2.36
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 +21 -0
- package/dist/audit/api.d.ts +2 -0
- package/dist/audit/api.d.ts.map +1 -1
- package/dist/audit/api.js +3 -13
- package/dist/audit/api.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +40 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +91 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +7 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/configure-web/cli.d.ts +29 -2
- package/dist/configure-web/cli.d.ts.map +1 -1
- package/dist/configure-web/cli.js +957 -28
- package/dist/configure-web/cli.js.map +1 -1
- package/dist/gateway.d.ts +7 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +81 -26
- package/dist/gateway.js.map +1 -1
- package/dist/hitl/api.d.ts +2 -0
- package/dist/hitl/api.d.ts.map +1 -1
- package/dist/hitl/api.js +3 -13
- package/dist/hitl/api.js.map +1 -1
- package/dist/hitl/approval-dashboard.d.ts +19 -0
- package/dist/hitl/approval-dashboard.d.ts.map +1 -0
- package/dist/hitl/approval-dashboard.js +148 -0
- package/dist/hitl/approval-dashboard.js.map +1 -0
- package/dist/hitl/provider-factory.js +1 -1
- package/dist/hitl/provider-factory.js.map +1 -1
- package/dist/hitl/providers/dashboard.d.ts +4 -5
- package/dist/hitl/providers/dashboard.d.ts.map +1 -1
- package/dist/hitl/providers/dashboard.js +29 -469
- package/dist/hitl/providers/dashboard.js.map +1 -1
- package/dist/hook/api.d.ts +2 -0
- package/dist/hook/api.d.ts.map +1 -1
- package/dist/hook/api.js +3 -13
- package/dist/hook/api.js.map +1 -1
- package/dist/index.js +29 -4
- package/dist/index.js.map +1 -1
- package/dist/security/request.d.ts +14 -0
- package/dist/security/request.d.ts.map +1 -0
- package/dist/security/request.js +47 -0
- package/dist/security/request.js.map +1 -0
- package/dist/tools/api.d.ts +2 -0
- package/dist/tools/api.d.ts.map +1 -1
- package/dist/tools/api.js +3 -13
- package/dist/tools/api.js.map +1 -1
- package/dist/transport/http-server.d.ts +2 -0
- package/dist/transport/http-server.d.ts.map +1 -1
- package/dist/transport/http-server.js +9 -22
- package/dist/transport/http-server.js.map +1 -1
- package/dist/transport/sse-server.d.ts +2 -0
- package/dist/transport/sse-server.d.ts.map +1 -1
- package/dist/transport/sse-server.js +9 -26
- package/dist/transport/sse-server.js.map +1 -1
- package/examples/docker-compose.yaml +63 -0
- package/examples/docker-gateway.yaml +61 -0
- package/examples/gateway.yaml +7 -0
- package/package.json +1 -1
- package/schema.json +35 -0
|
@@ -31,7 +31,7 @@ function createSingleProvider(cfg, approvalApi, options) {
|
|
|
31
31
|
case 'macos':
|
|
32
32
|
return new MacosHitlProvider(approvalApi, { sound: cfg.sound });
|
|
33
33
|
case 'dashboard':
|
|
34
|
-
return new DashboardHitlProvider({ port: cfg.port, config_path: options.configPath }, approvalApi);
|
|
34
|
+
return new DashboardHitlProvider({ host: cfg.host, port: cfg.port, config_path: options.configPath }, approvalApi);
|
|
35
35
|
case 'stdio':
|
|
36
36
|
return new StdioHitlProvider(approvalApi);
|
|
37
37
|
default:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-factory.js","sourceRoot":"","sources":["../../src/hitl/provider-factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,GAAG,GAAG,WAAW,CAAC,cAAc,CAAC,CAAC;AAExC,MAAM,UAAU,kBAAkB,CAChC,GAA8C,EAC9C,WAAwB,EACxB,UAAmC,EAAE;IAErC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QAChF,OAAO,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,qBAAqB,CAAC,SAAS,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,oBAAoB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,oBAAoB,CAC3B,GAAuB,EACvB,WAAwB,EACxB,OAAgC;IAEhC,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,IAAI,oBAAoB,CAC7B,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,EAClD,WAAW,CACZ,CAAC;QACJ,KAAK,UAAU;YACb,OAAO,IAAI,oBAAoB,CAC7B,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,EAChF,WAAW,CACZ,CAAC;QACJ,KAAK,OAAO;YACV,OAAO,IAAI,iBAAiB,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;QACjE,KAAK,SAAS;YACZ,OAAO,IAAI,mBAAmB,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACzE,KAAK,KAAK;YACR,OAAO,IAAI,eAAe,CAAC,WAAW,CAAC,CAAC;QAC1C,KAAK,OAAO;YACV,OAAO,IAAI,iBAAiB,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;QAClE,KAAK,WAAW;YACd,OAAO,IAAI,qBAAqB,CAC9B,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"provider-factory.js","sourceRoot":"","sources":["../../src/hitl/provider-factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ApprovalApi, HitlNotification, HitlProvider } from './types.js';
|
|
2
2
|
export interface DashboardHitlConfig {
|
|
3
|
+
host: string;
|
|
3
4
|
port: number;
|
|
4
5
|
config_path?: string;
|
|
5
6
|
}
|
|
6
7
|
export declare class DashboardHitlProvider implements HitlProvider {
|
|
7
8
|
private config;
|
|
8
9
|
private approvalApi;
|
|
9
|
-
private
|
|
10
|
-
private
|
|
11
|
-
private pending;
|
|
10
|
+
private app?;
|
|
11
|
+
private approvalRoutes?;
|
|
12
12
|
constructor(config: DashboardHitlConfig, approvalApi: ApprovalApi);
|
|
13
13
|
init(): Promise<void>;
|
|
14
14
|
stop(): Promise<void>;
|
|
15
15
|
notify(requests: HitlNotification[]): Promise<void>;
|
|
16
|
-
private broadcast;
|
|
17
16
|
}
|
|
18
17
|
//# sourceMappingURL=dashboard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/hitl/providers/dashboard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../../src/hitl/providers/dashboard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI9E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,qBAAsB,YAAW,YAAY;IAKtD,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,WAAW;IALrB,OAAO,CAAC,GAAG,CAAC,CAAkB;IAC9B,OAAO,CAAC,cAAc,CAAC,CAA0B;gBAGvC,MAAM,EAAE,mBAAmB,EAC3B,WAAW,EAAE,WAAW;IAG5B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBrB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAU3B,MAAM,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAIpD"}
|
|
@@ -1,490 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createConfigureWebApp } from '../../configure-web/cli.js';
|
|
2
2
|
import { childLogger } from '../../util/logger.js';
|
|
3
|
-
import {
|
|
4
|
-
import { rememberAllow } from '../../config/mutator.js';
|
|
3
|
+
import { ApprovalDashboardRoutes } from '../approval-dashboard.js';
|
|
5
4
|
const log = childLogger('hitl-dashboard');
|
|
6
|
-
let latestVersionCache = null;
|
|
7
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
8
5
|
export class DashboardHitlProvider {
|
|
9
6
|
config;
|
|
10
7
|
approvalApi;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
pending = new Map();
|
|
8
|
+
app;
|
|
9
|
+
approvalRoutes;
|
|
14
10
|
constructor(config, approvalApi) {
|
|
15
11
|
this.config = config;
|
|
16
12
|
this.approvalApi = approvalApi;
|
|
17
13
|
}
|
|
18
14
|
async init() {
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
// Send current pending requests
|
|
34
|
-
for (const req of this.pending.values()) {
|
|
35
|
-
res.write(`data: ${JSON.stringify({ type: 'new', request: req })}\n\n`);
|
|
36
|
-
}
|
|
37
|
-
req.on('close', () => this.clients.delete(res));
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (req.method === 'POST' && url.pathname === '/approve') {
|
|
41
|
-
const code = url.searchParams.get('code');
|
|
42
|
-
if (code) {
|
|
43
|
-
const pendingRequest = this.pending.get(code);
|
|
44
|
-
const remember = url.searchParams.get('remember');
|
|
45
|
-
if (remember) {
|
|
46
|
-
if (!this.config.config_path) {
|
|
47
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
48
|
-
res.end(JSON.stringify({ error: 'Config mutation is not available' }));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (remember !== 'always' && remember !== 'temporary') {
|
|
52
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
53
|
-
res.end(JSON.stringify({ error: 'Invalid remember mode' }));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (!pendingRequest) {
|
|
57
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
58
|
-
res.end(JSON.stringify({ error: 'No pending request found for code' }));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const durationMsParam = url.searchParams.get('duration_ms');
|
|
62
|
-
const durationMs = durationMsParam ? Number(durationMsParam) : undefined;
|
|
63
|
-
if (durationMs !== undefined && (!Number.isFinite(durationMs) || durationMs <= 0)) {
|
|
64
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
65
|
-
res.end(JSON.stringify({ error: 'Invalid duration_ms' }));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
const result = rememberAllow({
|
|
70
|
-
configPath: this.config.config_path,
|
|
71
|
-
agentId: pendingRequest.agentId,
|
|
72
|
-
tool: pendingRequest.tool,
|
|
73
|
-
mode: remember,
|
|
74
|
-
...(durationMs ? { durationMs } : {}),
|
|
75
|
-
});
|
|
76
|
-
log.info(result, 'Updated config from approval decision');
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
log.error({ err, code, remember }, 'Failed to update config from approval decision');
|
|
80
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
81
|
-
res.end(JSON.stringify({ error: 'Failed to update config' }));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
this.pending.delete(code);
|
|
86
|
-
this.approvalApi.approve(code);
|
|
87
|
-
this.broadcast({ type: 'resolved', code, action: 'approved' });
|
|
88
|
-
log.info({ code }, 'Approved via dashboard');
|
|
89
|
-
}
|
|
90
|
-
res.writeHead(200);
|
|
91
|
-
res.end('ok');
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (req.method === 'POST' && url.pathname === '/deny') {
|
|
95
|
-
const code = url.searchParams.get('code');
|
|
96
|
-
if (code) {
|
|
97
|
-
this.pending.delete(code);
|
|
98
|
-
this.approvalApi.deny(code, 'Denied via dashboard');
|
|
99
|
-
this.broadcast({ type: 'resolved', code, action: 'denied' });
|
|
100
|
-
log.info({ code }, 'Denied via dashboard');
|
|
101
|
-
}
|
|
102
|
-
res.writeHead(200);
|
|
103
|
-
res.end('ok');
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (req.method === 'GET' && url.pathname === '/version') {
|
|
107
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
108
|
-
res.end(JSON.stringify({ version: VERSION }));
|
|
109
|
-
return;
|
|
15
|
+
if (!this.config.config_path) {
|
|
16
|
+
log.warn('Dashboard provider requires a config path; running without dashboard UI');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.approvalRoutes = new ApprovalDashboardRoutes(this.approvalApi);
|
|
20
|
+
this.app = createConfigureWebApp(this.config.config_path, { approvals: this.approvalRoutes });
|
|
21
|
+
try {
|
|
22
|
+
await this.app.listen({ port: this.config.port, host: this.config.host });
|
|
23
|
+
log.info({ host: this.config.host, port: this.config.port }, 'Airlock dashboard listening');
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined;
|
|
27
|
+
if (code === 'EADDRINUSE') {
|
|
28
|
+
log.warn({ port: this.config.port }, 'Dashboard port in use; running without dashboard UI');
|
|
110
29
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (latestVersionCache && now - latestVersionCache.fetchedAt < CACHE_TTL_MS) {
|
|
114
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
115
|
-
res.end(JSON.stringify({ latest: latestVersionCache.version }));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
fetch('https://registry.npmjs.org/airlock-bot/latest')
|
|
119
|
-
.then((r) => r.json())
|
|
120
|
-
.then((data) => {
|
|
121
|
-
latestVersionCache = { version: data.version, fetchedAt: now };
|
|
122
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
123
|
-
res.end(JSON.stringify({ latest: data.version }));
|
|
124
|
-
})
|
|
125
|
-
.catch((err) => {
|
|
126
|
-
log.warn({ err }, 'Failed to fetch latest version from npm');
|
|
127
|
-
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
128
|
-
res.end(JSON.stringify({ error: 'Failed to fetch latest version' }));
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
30
|
+
else {
|
|
31
|
+
log.error({ err }, 'Dashboard server error');
|
|
131
32
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
await new Promise((resolve) => {
|
|
136
|
-
this.server.on('error', (err) => {
|
|
137
|
-
if (err.code === 'EADDRINUSE') {
|
|
138
|
-
log.warn({ port: this.config.port }, 'Dashboard port in use — running without dashboard UI');
|
|
139
|
-
this.server = undefined;
|
|
140
|
-
resolve();
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
log.error({ err }, 'Dashboard server error');
|
|
144
|
-
resolve(); // don't crash the process
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
this.server.listen(this.config.port, '127.0.0.1', () => {
|
|
148
|
-
log.info({ port: this.config.port }, 'HITL dashboard listening');
|
|
149
|
-
resolve();
|
|
150
|
-
});
|
|
151
|
-
});
|
|
33
|
+
this.app = undefined;
|
|
34
|
+
this.approvalRoutes = undefined;
|
|
35
|
+
}
|
|
152
36
|
}
|
|
153
37
|
async stop() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/* swallow */
|
|
160
|
-
}
|
|
38
|
+
this.approvalRoutes?.stop();
|
|
39
|
+
this.approvalRoutes = undefined;
|
|
40
|
+
if (this.app) {
|
|
41
|
+
await this.app.close().catch(() => { });
|
|
42
|
+
this.app = undefined;
|
|
161
43
|
}
|
|
162
|
-
this.clients.clear();
|
|
163
|
-
await new Promise((resolve) => {
|
|
164
|
-
if (this.server)
|
|
165
|
-
this.server.close(() => resolve());
|
|
166
|
-
else
|
|
167
|
-
resolve();
|
|
168
|
-
});
|
|
169
44
|
}
|
|
170
45
|
notify(requests) {
|
|
171
|
-
|
|
172
|
-
this.pending.set(req.code, req);
|
|
173
|
-
this.broadcast({ type: 'new', request: req });
|
|
174
|
-
}
|
|
46
|
+
this.approvalRoutes?.notify(requests);
|
|
175
47
|
return Promise.resolve();
|
|
176
48
|
}
|
|
177
|
-
broadcast(data) {
|
|
178
|
-
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
179
|
-
for (const client of this.clients) {
|
|
180
|
-
try {
|
|
181
|
-
client.write(msg);
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
/* swallow */
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
49
|
}
|
|
189
|
-
const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
190
|
-
<html>
|
|
191
|
-
<head>
|
|
192
|
-
<meta charset="utf-8">
|
|
193
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
194
|
-
<title>Airlock</title>
|
|
195
|
-
<style>
|
|
196
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
197
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0d1117; color: #c9d1d9; padding: 20px; }
|
|
198
|
-
header { display: flex; align-items: center; margin-bottom: 16px; gap: 12px; }
|
|
199
|
-
h1 { font-size: 18px; color: #58a6ff; }
|
|
200
|
-
.settings-btn { background: none; border: 1px solid #30363d; border-radius: 6px; color: #8b949e; cursor: pointer; padding: 4px 8px; font-size: 12px; }
|
|
201
|
-
.settings-btn:hover { border-color: #58a6ff; color: #c9d1d9; }
|
|
202
|
-
#settings { display: none; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 16px; margin-bottom: 12px; }
|
|
203
|
-
#settings.open { display: block; }
|
|
204
|
-
#settings label { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #c9d1d9; cursor: pointer; margin-bottom: 6px; }
|
|
205
|
-
#settings label:last-child { margin-bottom: 0; }
|
|
206
|
-
#empty { color: #484f58; font-style: italic; }
|
|
207
|
-
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 12px; cursor: pointer; transition: border-color 0.15s; }
|
|
208
|
-
.card:hover { border-color: #58a6ff; }
|
|
209
|
-
.card.approved { border-color: #238636; opacity: 0.5; }
|
|
210
|
-
.card.denied { border-color: #da3633; opacity: 0.5; }
|
|
211
|
-
.card .actions { position: relative; z-index: 2; }
|
|
212
|
-
.tool { font-weight: 600; color: #f0883e; font-size: 15px; }
|
|
213
|
-
.agent { color: #8b949e; font-size: 13px; margin-top: 2px; }
|
|
214
|
-
.args { font-family: "SF Mono", Monaco, monospace; font-size: 12px; background: #0d1117; border-radius: 4px; padding: 8px; margin: 8px 0; white-space: pre-wrap; word-break: break-all; color: #8b949e; max-height: 120px; overflow-y: auto; }
|
|
215
|
-
.code { font-family: "SF Mono", Monaco, monospace; font-size: 11px; color: #484f58; margin-bottom: 8px; }
|
|
216
|
-
.actions { display: flex; gap: 8px; }
|
|
217
|
-
.btn { border: none; padding: 6px 16px; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
218
|
-
.btn-approve { background: #238636; color: #fff; }
|
|
219
|
-
.btn-approve:hover { background: #2ea043; }
|
|
220
|
-
.btn-deny { background: #21262d; color: #f85149; border: 1px solid #da3633; }
|
|
221
|
-
.btn-deny:hover { background: #da3633; color: #fff; }
|
|
222
|
-
.btn kbd { display: inline-block; background: rgba(255,255,255,0.1); border-radius: 3px; padding: 0 4px; font-size: 11px; font-family: inherit; margin-left: 6px; opacity: 0.7; }
|
|
223
|
-
.status { font-size: 12px; font-weight: 600; margin-top: 8px; }
|
|
224
|
-
.status.approved { color: #3fb950; }
|
|
225
|
-
.status.denied { color: #f85149; }
|
|
226
|
-
.version-info { margin-top: 12px; font-size: 12px; color: #484f58; }
|
|
227
|
-
.version-info .current { color: #8b949e; }
|
|
228
|
-
.version-info .update-available { color: #f0883e; margin-top: 4px; }
|
|
229
|
-
.version-info .update-available code { background: #21262d; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
|
|
230
|
-
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 100; justify-content: center; align-items: center; }
|
|
231
|
-
.modal-overlay.open { display: flex; }
|
|
232
|
-
.modal { background: #161b22; border: 1px solid #30363d; border-radius: 10px; width: 90vw; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; animation: modalIn 0.15s ease-out; }
|
|
233
|
-
@keyframes modalIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
|
234
|
-
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #30363d; }
|
|
235
|
-
.modal-header .tool { margin: 0; }
|
|
236
|
-
.modal-close { background: none; border: none; color: #8b949e; font-size: 20px; cursor: pointer; padding: 4px 8px; border-radius: 4px; }
|
|
237
|
-
.modal-close:hover { background: #21262d; color: #c9d1d9; }
|
|
238
|
-
.modal-body { padding: 16px; overflow-y: auto; flex: 1; }
|
|
239
|
-
.modal-body .detail-label { font-size: 11px; color: #484f58; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 12px; margin-bottom: 4px; }
|
|
240
|
-
.modal-body .detail-label:first-child { margin-top: 0; }
|
|
241
|
-
.modal-body .detail-value { font-size: 13px; color: #c9d1d9; }
|
|
242
|
-
.modal-body .detail-value.mono { font-family: "SF Mono", Monaco, monospace; font-size: 12px; color: #8b949e; }
|
|
243
|
-
.modal-body .args-full { max-height: 50vh; overflow-y: auto; }
|
|
244
|
-
.modal-body .arg-entry { margin-bottom: 10px; }
|
|
245
|
-
.modal-body .arg-entry:last-child { margin-bottom: 0; }
|
|
246
|
-
.modal-body .arg-key { font-family: "SF Mono", Monaco, monospace; font-size: 12px; color: #79c0ff; margin-bottom: 4px; }
|
|
247
|
-
.modal-body .arg-val { font-family: "SF Mono", Monaco, monospace; font-size: 12px; background: #0d1117; border-radius: 4px; padding: 10px; white-space: pre-wrap; word-break: break-word; color: #c9d1d9; overflow-x: auto; }
|
|
248
|
-
.modal-body .arg-val pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
|
|
249
|
-
.modal-body .arg-val code.hljs { background: transparent; padding: 0; }
|
|
250
|
-
.modal-footer { padding: 12px 16px; border-top: 1px solid #30363d; display: flex; gap: 8px; }
|
|
251
|
-
.modal-footer .status { margin: 0; padding: 6px 0; }
|
|
252
|
-
</style>
|
|
253
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|
254
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></scr` +
|
|
255
|
-
`ipt>
|
|
256
|
-
</head>
|
|
257
|
-
<body>
|
|
258
|
-
<header>
|
|
259
|
-
<h1>Airlock Approvals</h1>
|
|
260
|
-
<button class="settings-btn" onclick="document.getElementById('settings').classList.toggle('open')">Settings</button>
|
|
261
|
-
</header>
|
|
262
|
-
<div id="settings">
|
|
263
|
-
<label><input type="checkbox" id="notifs" checked> Browser notifications</label>
|
|
264
|
-
<label><input type="checkbox" id="sound" checked> Sound</label>
|
|
265
|
-
<div class="version-info" id="version-info"></div>
|
|
266
|
-
</div>
|
|
267
|
-
<div id="list"><div id="empty">No pending requests</div></div>
|
|
268
|
-
<div class="modal-overlay" id="modal-overlay" onclick="closeModal(event)">
|
|
269
|
-
<div class="modal" onclick="event.stopPropagation()">
|
|
270
|
-
<div class="modal-header">
|
|
271
|
-
<div class="tool" id="modal-tool"></div>
|
|
272
|
-
<button class="modal-close" onclick="closeModal()">×</button>
|
|
273
|
-
</div>
|
|
274
|
-
<div class="modal-body" id="modal-body"></div>
|
|
275
|
-
<div class="modal-footer" id="modal-footer"></div>
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
<script>
|
|
279
|
-
var list = document.getElementById('list');
|
|
280
|
-
var empty = document.getElementById('empty');
|
|
281
|
-
var cards = new Map();
|
|
282
|
-
var requestData = new Map();
|
|
283
|
-
|
|
284
|
-
// Settings — persisted in localStorage
|
|
285
|
-
var notifsEl = document.getElementById('notifs');
|
|
286
|
-
var soundEl = document.getElementById('sound');
|
|
287
|
-
|
|
288
|
-
notifsEl.checked = localStorage.getItem('airlock:notifs') === 'true';
|
|
289
|
-
soundEl.checked = localStorage.getItem('airlock:sound') !== 'false';
|
|
290
|
-
notifsEl.onchange = function() { localStorage.setItem('airlock:notifs', notifsEl.checked); };
|
|
291
|
-
soundEl.onchange = function() { localStorage.setItem('airlock:sound', soundEl.checked); };
|
|
292
|
-
|
|
293
|
-
// Version check
|
|
294
|
-
(async function checkVersion() {
|
|
295
|
-
try {
|
|
296
|
-
var curRes = await fetch('/version').then(function(r) { return r.json(); });
|
|
297
|
-
var latestRes = await fetch('/version/latest').then(function(r) { return r.json(); });
|
|
298
|
-
var el = document.getElementById('version-info');
|
|
299
|
-
var html = '<div class="current">Version: v' + esc(curRes.version) + '</div>';
|
|
300
|
-
if (latestRes.latest && isNewer(latestRes.latest, curRes.version)) {
|
|
301
|
-
html += '<div class="update-available">New version available: v' + esc(latestRes.latest) + ' — run <code>npm update -g airlock-bot</code></div>';
|
|
302
|
-
}
|
|
303
|
-
el.innerHTML = html;
|
|
304
|
-
} catch (e) { /* ignore version check failures */ }
|
|
305
|
-
})();
|
|
306
|
-
|
|
307
|
-
function isNewer(latest, current) {
|
|
308
|
-
var a = latest.split('.').map(Number);
|
|
309
|
-
var b = current.split('.').map(Number);
|
|
310
|
-
for (var i = 0; i < 3; i++) {
|
|
311
|
-
if ((a[i] || 0) > (b[i] || 0)) return true;
|
|
312
|
-
if ((a[i] || 0) < (b[i] || 0)) return false;
|
|
313
|
-
}
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Syntax highlight via highlight.js if loaded, otherwise plain escaped text
|
|
318
|
-
function highlight(code, lang) {
|
|
319
|
-
if (typeof hljs !== 'undefined') {
|
|
320
|
-
try {
|
|
321
|
-
if (lang) return hljs.highlight(code, { language: lang }).value;
|
|
322
|
-
return hljs.highlightAuto(code).value;
|
|
323
|
-
} catch (e) { /* fall through */ }
|
|
324
|
-
}
|
|
325
|
-
return esc(code);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Render a single arg value — strings get displayed directly (with highlighting
|
|
329
|
-
// for multiline content), objects/arrays get JSON-formatted
|
|
330
|
-
function renderArgValue(val) {
|
|
331
|
-
if (typeof val === 'string') {
|
|
332
|
-
if (val.includes('\\n') && val.length > 60) {
|
|
333
|
-
return '<div class="arg-val"><pre><code>' + highlight(val) + '</code></pre></div>';
|
|
334
|
-
}
|
|
335
|
-
return '<div class="arg-val">' + esc(val) + '</div>';
|
|
336
|
-
}
|
|
337
|
-
var json = JSON.stringify(val, null, 2);
|
|
338
|
-
return '<div class="arg-val"><pre><code>' + highlight(json, 'json') + '</code></pre></div>';
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
var currentModalCode = null;
|
|
342
|
-
|
|
343
|
-
function openModal(code) {
|
|
344
|
-
var req = requestData.get(code);
|
|
345
|
-
if (!req) return;
|
|
346
|
-
currentModalCode = code;
|
|
347
|
-
var overlay = document.getElementById('modal-overlay');
|
|
348
|
-
document.getElementById('modal-tool').textContent = req.tool;
|
|
349
|
-
var body = document.getElementById('modal-body');
|
|
350
|
-
var html = '<div class="detail-label">Agent</div><div class="detail-value">' + esc(req.agentId) + '</div>';
|
|
351
|
-
html += '<div class="detail-label">Request Code</div><div class="detail-value mono">' + esc(req.code) + '</div>';
|
|
352
|
-
if (req.timeoutMs) {
|
|
353
|
-
html += '<div class="detail-label">Timeout</div><div class="detail-value">' + Math.round(req.timeoutMs / 1000) + 's</div>';
|
|
354
|
-
}
|
|
355
|
-
html += '<div class="detail-label">Arguments</div><div class="args-full">';
|
|
356
|
-
var keys = Object.keys(req.args || {});
|
|
357
|
-
if (keys.length === 0) {
|
|
358
|
-
html += '<div class="arg-val" style="color:#484f58">No arguments</div>';
|
|
359
|
-
} else {
|
|
360
|
-
for (var i = 0; i < keys.length; i++) {
|
|
361
|
-
html += '<div class="arg-entry"><div class="arg-key">' + esc(keys[i]) + '</div>' + renderArgValue(req.args[keys[i]]) + '</div>';
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
html += '</div>';
|
|
365
|
-
body.innerHTML = html;
|
|
366
|
-
|
|
367
|
-
var footer = document.getElementById('modal-footer');
|
|
368
|
-
var el = cards.get(code);
|
|
369
|
-
var resolved = el && (el.classList.contains('approved') || el.classList.contains('denied'));
|
|
370
|
-
if (resolved) {
|
|
371
|
-
var which = el.classList.contains('approved') ? 'approved' : 'denied';
|
|
372
|
-
footer.innerHTML = '<div class="status ' + which + '">' + which + '</div>';
|
|
373
|
-
} else {
|
|
374
|
-
footer.innerHTML = actionButtons(code, true);
|
|
375
|
-
}
|
|
376
|
-
overlay.classList.add('open');
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function closeModal(e) {
|
|
380
|
-
if (e && e.target && e.target !== document.getElementById('modal-overlay')) return;
|
|
381
|
-
document.getElementById('modal-overlay').classList.remove('open');
|
|
382
|
-
currentModalCode = null;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function actAndClose(action, code, remember, durationMs) {
|
|
386
|
-
act(action, code, remember, durationMs);
|
|
387
|
-
document.getElementById('modal-overlay').classList.remove('open');
|
|
388
|
-
currentModalCode = null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function actionButtons(code, closeAfter) {
|
|
392
|
-
var fn = closeAfter ? 'actAndClose' : 'act';
|
|
393
|
-
return '<button class="btn btn-approve" onclick="' + fn + '(\\x27approve\\x27,\\x27' + code + '\\x27)">Approve<kbd>A</kbd></button>' +
|
|
394
|
-
'<button class="btn btn-approve" onclick="' + fn + '(\\x27approve\\x27,\\x27' + code + '\\x27,\\x27temporary\\x27,3600000)">Allow 1h</button>' +
|
|
395
|
-
'<button class="btn btn-approve" onclick="' + fn + '(\\x27approve\\x27,\\x27' + code + '\\x27,\\x27always\\x27)">Always Allow</button>' +
|
|
396
|
-
'<button class="btn btn-deny" onclick="' + fn + '(\\x27deny\\x27,\\x27' + code + '\\x27)">Deny<kbd>D</kbd></button>';
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function render(req) {
|
|
400
|
-
requestData.set(req.code, req);
|
|
401
|
-
var el = document.createElement('div');
|
|
402
|
-
el.className = 'card';
|
|
403
|
-
el.id = 'card-' + req.code;
|
|
404
|
-
el.onclick = function(e) {
|
|
405
|
-
if (e.target.closest('.actions')) return;
|
|
406
|
-
openModal(req.code);
|
|
407
|
-
};
|
|
408
|
-
var args = JSON.stringify(req.args, null, 2);
|
|
409
|
-
el.innerHTML =
|
|
410
|
-
'<div class="tool">' + esc(req.tool) + '</div>' +
|
|
411
|
-
'<div class="agent">agent: ' + esc(req.agentId) + '</div>' +
|
|
412
|
-
'<div class="args">' + esc(args) + '</div>' +
|
|
413
|
-
'<div class="code">' + req.code + '</div>' +
|
|
414
|
-
'<div class="actions">' +
|
|
415
|
-
actionButtons(req.code, false) +
|
|
416
|
-
'</div>';
|
|
417
|
-
cards.set(req.code, el);
|
|
418
|
-
list.prepend(el);
|
|
419
|
-
empty.style.display = 'none';
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function act(action, code, remember, durationMs) {
|
|
423
|
-
var url = '/' + action + '?code=' + encodeURIComponent(code);
|
|
424
|
-
if (remember) url += '&remember=' + encodeURIComponent(remember);
|
|
425
|
-
if (durationMs) url += '&duration_ms=' + encodeURIComponent(String(durationMs));
|
|
426
|
-
fetch(url, { method: 'POST' });
|
|
427
|
-
var el = cards.get(code);
|
|
428
|
-
if (el) {
|
|
429
|
-
el.querySelector('.actions').innerHTML = '<div class="status ' + action + (action === 'approve' ? 'd' : '') + '">' + action + 'd</div>';
|
|
430
|
-
el.classList.add(action === 'approve' ? 'approved' : 'denied');
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
435
|
-
|
|
436
|
-
// Only request notification permission if the user has notifications enabled
|
|
437
|
-
if ('Notification' in window && notifsEl.checked) Notification.requestPermission();
|
|
438
|
-
notifsEl.addEventListener('change', function() {
|
|
439
|
-
if (notifsEl.checked && 'Notification' in window) Notification.requestPermission();
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// Keyboard shortcuts: a=approve, d=deny, Escape=close modal
|
|
443
|
-
function getFirstPendingCode() {
|
|
444
|
-
for (var entry of cards) {
|
|
445
|
-
if (!entry[1].classList.contains('approved') && !entry[1].classList.contains('denied')) return entry[0];
|
|
446
|
-
}
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
document.addEventListener('keydown', function(e) {
|
|
451
|
-
if (e.key === 'Escape') { closeModal(); return; }
|
|
452
|
-
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
453
|
-
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
454
|
-
var key = e.key.toLowerCase();
|
|
455
|
-
if (key === 'a' || key === 'd') {
|
|
456
|
-
var action = key === 'a' ? 'approve' : 'deny';
|
|
457
|
-
var code = currentModalCode || getFirstPendingCode();
|
|
458
|
-
if (!code) return;
|
|
459
|
-
var el = cards.get(code);
|
|
460
|
-
if (el && (el.classList.contains('approved') || el.classList.contains('denied'))) return;
|
|
461
|
-
if (currentModalCode) { actAndClose(action, code); } else { act(action, code); }
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
var es = new EventSource('/events');
|
|
466
|
-
es.onmessage = function(e) {
|
|
467
|
-
var msg = JSON.parse(e.data);
|
|
468
|
-
if (msg.type === 'new') {
|
|
469
|
-
render(msg.request);
|
|
470
|
-
if (notifsEl.checked && Notification.permission === 'granted') {
|
|
471
|
-
new Notification('Airlock: ' + msg.request.tool, {
|
|
472
|
-
body: 'agent: ' + msg.request.agentId + '\\n' + msg.request.code,
|
|
473
|
-
tag: msg.request.code,
|
|
474
|
-
silent: !soundEl.checked,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (msg.type === 'resolved') {
|
|
479
|
-
var el = cards.get(msg.code);
|
|
480
|
-
if (el) {
|
|
481
|
-
el.querySelector('.actions').innerHTML = '<div class="status ' + (msg.action === 'approved' ? 'approved' : 'denied') + '">' + msg.action + '</div>';
|
|
482
|
-
el.classList.add(msg.action);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
</scr` +
|
|
487
|
-
`ipt>
|
|
488
|
-
</body>
|
|
489
|
-
</html>`;
|
|
490
50
|
//# sourceMappingURL=dashboard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../../src/hitl/providers/dashboard.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../../src/hitl/providers/dashboard.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAGnE,MAAM,GAAG,GAAG,WAAW,CAAC,gBAAgB,CAAC,CAAC;AAQ1C,MAAM,OAAO,qBAAqB;IAKtB;IACA;IALF,GAAG,CAAmB;IACtB,cAAc,CAA2B;IAEjD,YACU,MAA2B,EAC3B,WAAwB;QADxB,WAAM,GAAN,MAAM,CAAqB;QAC3B,gBAAW,GAAX,WAAW,CAAa;IAC/B,CAAC;IAEJ,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC,yEAAyE,CAAC,CAAC;YACpF,OAAO;QACT,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,IAAI,uBAAuB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACpE,IAAI,CAAC,GAAG,GAAG,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;QAE9F,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1E,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,6BAA6B,CAAC,CAAC;QAC9F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,GAAG,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;YACpF,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC1B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,qDAAqD,CAAC,CAAC;YAC9F,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,wBAAwB,CAAC,CAAC;YAC/C,CAAC;YACD,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC;YACrB,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;QAClC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;QAEhC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACvC,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,QAA4B;QACjC,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;CACF"}
|
package/dist/hook/api.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface HookApiOpts {
|
|
|
9
9
|
hitlBatcher: HitlBatcher;
|
|
10
10
|
auditLogger: AuditLogger;
|
|
11
11
|
secret?: string;
|
|
12
|
+
authRequired?: boolean;
|
|
13
|
+
allowedOrigins?: string[];
|
|
12
14
|
}
|
|
13
15
|
export declare function hookApiPlugin(app: FastifyInstance, opts: HookApiOpts): Promise<void>;
|
|
14
16
|
//# sourceMappingURL=api.d.ts.map
|
package/dist/hook/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/hook/api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/hook/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,eAAe,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAWD,wBAAsB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA4E1F"}
|
package/dist/hook/api.js
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
|
-
import { timingSafeEqual } from 'crypto';
|
|
2
1
|
import { normalizeTool } from './normalizer.js';
|
|
3
2
|
import { childLogger } from '../util/logger.js';
|
|
3
|
+
import { checkRequestSecurity } from '../security/request.js';
|
|
4
4
|
const log = childLogger('hook-api');
|
|
5
|
-
function constantTimeEqual(a, b) {
|
|
6
|
-
const bufA = Buffer.from(a);
|
|
7
|
-
const bufB = Buffer.from(b);
|
|
8
|
-
if (bufA.length !== bufB.length)
|
|
9
|
-
return false;
|
|
10
|
-
return timingSafeEqual(bufA, bufB);
|
|
11
|
-
}
|
|
12
5
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
13
6
|
export async function hookApiPlugin(app, opts) {
|
|
14
|
-
const { allowlist, hitlEngine, hitlBatcher, auditLogger
|
|
7
|
+
const { allowlist, hitlEngine, hitlBatcher, auditLogger } = opts;
|
|
15
8
|
app.addHook('preHandler', async (request, reply) => {
|
|
16
|
-
if (!
|
|
9
|
+
if (!checkRequestSecurity(request, reply, opts)) {
|
|
17
10
|
return;
|
|
18
|
-
const auth = request.headers.authorization ?? '';
|
|
19
|
-
if (!constantTimeEqual(auth, `Bearer ${secret}`)) {
|
|
20
|
-
return reply.status(401).send({ error: 'Unauthorized' });
|
|
21
11
|
}
|
|
22
12
|
});
|
|
23
13
|
app.post('/hook', async (request, reply) => {
|