slaunt 0.1.16 → 0.1.18
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 +17 -106
- package/bin/slaunt-hooks.js +96 -0
- package/package.json +9 -59
- package/release/SlauntKillBar.zip +0 -0
- package/release/manifest.json +7 -0
- package/scripts/killbar-portability-check.js +67 -0
- package/scripts/package-killbar-macos.sh +38 -0
- package/src/approval-manager.js +53 -0
- package/src/cli-auth.js +230 -0
- package/src/compact-wordmark.js +13 -0
- package/src/desktop/broker-daemon.js +18 -0
- package/src/desktop/broker-grants.js +37 -0
- package/src/desktop/broker-hook-command.js +23 -0
- package/src/desktop/broker-keychain.js +29 -0
- package/src/desktop/broker-launch-agent.js +34 -0
- package/src/desktop/broker-service.js +40 -0
- package/src/desktop/chat-capture-command.js +7 -0
- package/src/desktop/chat-hook-capture.js +445 -0
- package/src/desktop/claude-lifecycle.js +143 -0
- package/src/desktop/client-identity.js +19 -0
- package/src/desktop/native-claude-broker.js +34 -0
- package/src/desktop/native-claude-connection.js +42 -0
- package/src/desktop/tool-guard-command.js +13 -0
- package/src/desktop/tool-guard.js +225 -0
- package/src/discovery/catalog.js +734 -0
- package/src/full-screen.js +88 -0
- package/src/helper-release.js +27 -0
- package/src/hidden-oauth.js +70 -0
- package/src/independent-grants.js +38 -0
- package/src/install-flow.js +71 -0
- package/src/killbar-background-install.js +47 -0
- package/src/killbar-enrollment.js +74 -0
- package/src/killbar-launch-agent.js +10 -0
- package/src/mcp-identity.js +6 -0
- package/src/native-config.js +150 -0
- package/src/native-oauth.js +33 -0
- package/src/oauth-cohort.js +43 -0
- package/src/pty-wrapper.py +57 -0
- package/src/read-json.js +8 -0
- package/src/setup/client-installer.js +93 -0
- package/src/setup/codex-guard-supervisor.js +34 -0
- package/src/setup/hook-installer.js +496 -0
- package/src/setup/managed-policy.js +26 -0
- package/src/setup/mcp-config-installer.js +204 -0
- package/src/setup-flow.js +66 -0
- package/src/tui.js +205 -0
- package/test/approval-manager.test.js +27 -0
- package/test/broker-grants.test.js +14 -0
- package/test/broker-keychain.test.js +14 -0
- package/test/broker-launch-agent.test.js +15 -0
- package/test/broker-service.test.js +42 -0
- package/test/cli-auth-recovery.test.js +15 -0
- package/test/codex-guard-supervisor.test.js +25 -0
- package/test/full-screen.test.js +49 -0
- package/test/helper-release.test.js +22 -0
- package/test/hidden-oauth.test.js +44 -0
- package/test/hook-installer.test.js +232 -0
- package/test/independent-grants.test.js +16 -0
- package/test/install-flow.test.js +24 -0
- package/test/killbar-background-install.test.js +15 -0
- package/test/killbar-enrollment.test.js +115 -0
- package/test/killbar-launch-agent.test.js +12 -0
- package/test/killbar-portability.test.js +17 -0
- package/test/managed-policy.test.js +18 -0
- package/test/native-claude-broker.test.js +19 -0
- package/test/native-claude-connection.test.js +17 -0
- package/test/native-claude-hooks.test.js +9 -0
- package/test/native-config.test.js +3 -0
- package/test/native-integration.cjs +29 -0
- package/test/native-oauth.test.js +23 -0
- package/test/oauth-cohort.test.js +26 -0
- package/test/tui.test.js +100 -0
- package/LICENSE +0 -201
- package/dist/audit/risk-engine.d.ts +0 -4
- package/dist/audit/risk-engine.d.ts.map +0 -1
- package/dist/audit/risk-engine.js +0 -174
- package/dist/audit/risk-engine.js.map +0 -1
- package/dist/audit/run.d.ts +0 -16
- package/dist/audit/run.d.ts.map +0 -1
- package/dist/audit/run.js +0 -66
- package/dist/audit/run.js.map +0 -1
- package/dist/classification/builtin-catalog.d.ts +0 -3
- package/dist/classification/builtin-catalog.d.ts.map +0 -1
- package/dist/classification/builtin-catalog.js +0 -283
- package/dist/classification/builtin-catalog.js.map +0 -1
- package/dist/classification/catalog.d.ts +0 -10
- package/dist/classification/catalog.d.ts.map +0 -1
- package/dist/classification/catalog.js +0 -194
- package/dist/classification/catalog.js.map +0 -1
- package/dist/classification/classifier.d.ts +0 -3
- package/dist/classification/classifier.d.ts.map +0 -1
- package/dist/classification/classifier.js +0 -64
- package/dist/classification/classifier.js.map +0 -1
- package/dist/classification/public-catalog-config.d.ts +0 -3
- package/dist/classification/public-catalog-config.d.ts.map +0 -1
- package/dist/classification/public-catalog-config.js +0 -5
- package/dist/classification/public-catalog-config.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -220
- package/dist/cli.js.map +0 -1
- package/dist/config/discover.d.ts +0 -8
- package/dist/config/discover.d.ts.map +0 -1
- package/dist/config/discover.js +0 -163
- package/dist/config/discover.js.map +0 -1
- package/dist/config/install.d.ts +0 -10
- package/dist/config/install.d.ts.map +0 -1
- package/dist/config/install.js +0 -143
- package/dist/config/install.js.map +0 -1
- package/dist/config/paths.d.ts +0 -24
- package/dist/config/paths.d.ts.map +0 -1
- package/dist/config/paths.js +0 -90
- package/dist/config/paths.js.map +0 -1
- package/dist/demo.d.ts +0 -4
- package/dist/demo.d.ts.map +0 -1
- package/dist/demo.js +0 -99
- package/dist/demo.js.map +0 -1
- package/dist/explanation/local.d.ts +0 -24
- package/dist/explanation/local.d.ts.map +0 -1
- package/dist/explanation/local.js +0 -667
- package/dist/explanation/local.js.map +0 -1
- package/dist/mcp/introspect.d.ts +0 -14
- package/dist/mcp/introspect.d.ts.map +0 -1
- package/dist/mcp/introspect.js +0 -186
- package/dist/mcp/introspect.js.map +0 -1
- package/dist/presentation/plain-language.d.ts +0 -12
- package/dist/presentation/plain-language.d.ts.map +0 -1
- package/dist/presentation/plain-language.js +0 -120
- package/dist/presentation/plain-language.js.map +0 -1
- package/dist/privacy/redaction.d.ts +0 -2
- package/dist/privacy/redaction.d.ts.map +0 -1
- package/dist/privacy/redaction.js +0 -50
- package/dist/privacy/redaction.js.map +0 -1
- package/dist/report/html.d.ts +0 -4
- package/dist/report/html.d.ts.map +0 -1
- package/dist/report/html.js +0 -101
- package/dist/report/html.js.map +0 -1
- package/dist/tui/analysis-path.d.ts +0 -17
- package/dist/tui/analysis-path.d.ts.map +0 -1
- package/dist/tui/analysis-path.js +0 -184
- package/dist/tui/analysis-path.js.map +0 -1
- package/dist/tui/render.d.ts +0 -4
- package/dist/tui/render.d.ts.map +0 -1
- package/dist/tui/render.js +0 -273
- package/dist/tui/render.js.map +0 -1
- package/dist/tui/splash.d.ts +0 -15
- package/dist/tui/splash.d.ts.map +0 -1
- package/dist/tui/splash.js +0 -130
- package/dist/tui/splash.js.map +0 -1
- package/dist/tui/stream.d.ts +0 -6
- package/dist/tui/stream.d.ts.map +0 -1
- package/dist/tui/stream.js +0 -56
- package/dist/tui/stream.js.map +0 -1
- package/dist/types.d.ts +0 -131
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -8
- package/dist/version.js.map +0 -1
- package/models/APACHE-2.0.txt +0 -202
- package/models/MODEL.json +0 -29
- package/models/NOTICE.md +0 -23
package/src/cli-auth.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const http = require('node:http');
|
|
5
|
+
const {readJson} = require('./read-json');
|
|
6
|
+
|
|
7
|
+
const OAUTH_ISSUER = 'https://central-mcp-ccr-production.up.railway.app';
|
|
8
|
+
const OAUTH_CLIENT_ID = 'ai.slaunt.cli';
|
|
9
|
+
const OAUTH_REDIRECT_URI = 'http://127.0.0.1:53683/oauth/callback';
|
|
10
|
+
const OAUTH_PORT = 53683;
|
|
11
|
+
const OAUTH_SCOPE = 'mcp:tools';
|
|
12
|
+
const TOKEN_REQUEST_TIMEOUT_MS = 15_000;
|
|
13
|
+
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60_000;
|
|
14
|
+
|
|
15
|
+
function writeCallbackPage(response, statusCode, message) {
|
|
16
|
+
response.writeHead(statusCode, {
|
|
17
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
18
|
+
'Cache-Control': 'no-store',
|
|
19
|
+
'Referrer-Policy': 'no-referrer',
|
|
20
|
+
'X-Content-Type-Options': 'nosniff',
|
|
21
|
+
'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
|
|
22
|
+
});
|
|
23
|
+
response.end(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Slaunt CLI</title><body style="margin:0;background:#101217;color:#f2f4f3;font:16px system-ui,sans-serif;display:grid;min-height:100vh;place-items:center"><main style="max-width:34rem;padding:2rem;text-align:center"><div style="color:#9be7c3;font-size:2.2rem;margin-bottom:1rem">●</div><h1 style="font-size:1.4rem">${message}</h1><p style="color:#a7ada9">You can close this tab and return to Slaunt CLI.</p></main></body></html>`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeTokenResponse(payload) {
|
|
27
|
+
const token = (value) => typeof value === 'string'
|
|
28
|
+
&& value.length > 0
|
|
29
|
+
&& value.length <= 16_384
|
|
30
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
31
|
+
const expiresIn = Number(payload?.expires_in);
|
|
32
|
+
if (!token(payload?.access_token) || !token(payload?.refresh_token)
|
|
33
|
+
|| String(payload?.token_type || '').toLowerCase() !== 'bearer'
|
|
34
|
+
|| payload.scope !== OAUTH_SCOPE
|
|
35
|
+
|| !Number.isSafeInteger(expiresIn) || expiresIn < 1 || expiresIn > 86_400) {
|
|
36
|
+
throw new Error('oauth-token-response-invalid');
|
|
37
|
+
}
|
|
38
|
+
if (typeof payload.setup_ticket !== 'string' || !/^sat1\.[A-Za-z0-9_-]{43}$/.test(payload.setup_ticket)) throw new Error('oauth-token-response-invalid');
|
|
39
|
+
return {
|
|
40
|
+
version: 1,
|
|
41
|
+
setupTicket: payload.setup_ticket,
|
|
42
|
+
accessToken: payload.access_token,
|
|
43
|
+
refreshToken: payload.refresh_token,
|
|
44
|
+
expiresAt: Date.now() + (expiresIn * 1000)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function exchangeAuthorizationCode({ code, codeVerifier, sessionSink, issuer = OAUTH_ISSUER, fetchImpl = fetch }) {
|
|
49
|
+
const body = new URLSearchParams({
|
|
50
|
+
grant_type: 'authorization_code',
|
|
51
|
+
client_id: OAUTH_CLIENT_ID,
|
|
52
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
53
|
+
code,
|
|
54
|
+
code_verifier: codeVerifier
|
|
55
|
+
});
|
|
56
|
+
let payload;
|
|
57
|
+
// The server keeps a brief receipt bound to this exact code and verifier.
|
|
58
|
+
// Retry only an uncertain transport/server failure, never a rejected grant.
|
|
59
|
+
for(let attempt=0;attempt<2;attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
const response=await fetchImpl(`${issuer}/token`,{method:'POST',redirect:'error',cache:'no-store',signal:AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams(body)});
|
|
62
|
+
if(!response.ok && response.status<500)throw new Error('oauth-grant-rejected');
|
|
63
|
+
if(!response.ok)throw new Error('oauth-token-exchange-failed');
|
|
64
|
+
const contentLength=Number(response.headers.get('content-length')||0);
|
|
65
|
+
if(contentLength>128*1024)throw new Error('oauth-token-response-invalid');
|
|
66
|
+
payload=await readJson(response,128*1024);break;
|
|
67
|
+
} catch (error) {if(error?.message==='oauth-grant-rejected'||attempt===1)throw new Error('oauth-token-exchange-failed');}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tokens = safeTokenResponse(payload);
|
|
71
|
+
await sessionSink.save(tokens);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createAuthorizationUrl({ state, codeChallenge, issuer = OAUTH_ISSUER }) {
|
|
75
|
+
const authorizeUrl = new URL('/authorize', issuer);
|
|
76
|
+
authorizeUrl.search = new URLSearchParams({
|
|
77
|
+
response_type: 'code',
|
|
78
|
+
client_id: OAUTH_CLIENT_ID,
|
|
79
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
80
|
+
scope: OAUTH_SCOPE,
|
|
81
|
+
state,
|
|
82
|
+
code_challenge: codeChallenge,
|
|
83
|
+
code_challenge_method: 'S256'
|
|
84
|
+
}).toString();
|
|
85
|
+
return authorizeUrl;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isLoopbackAddress(address) {
|
|
89
|
+
return address === '127.0.0.1' || address === '::ffff:127.0.0.1';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function startOAuthFlow({ shell, sessionSink, signal, requests, issuer = OAUTH_ISSUER, fetchImpl = fetch }) {
|
|
93
|
+
if (!shell || typeof shell.openExternal !== 'function') {
|
|
94
|
+
return Promise.reject(new Error('oauth-browser-unavailable'));
|
|
95
|
+
}
|
|
96
|
+
if (!sessionSink || typeof sessionSink.save !== 'function') {
|
|
97
|
+
return Promise.reject(new Error('credential-store-unavailable'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!Array.isArray(requests) || requests.length < 1 || requests.length > 2 || requests.some(x => !['codex','claude'].includes(x.client) || typeof x.authorization_url !== 'string')) return Promise.reject(new Error('invalid-setup-request'));
|
|
101
|
+
const ordered = [...requests].sort((a,b) => ['codex','claude'].indexOf(a.client)-['codex','claude'].indexOf(b.client));
|
|
102
|
+
if (new Set(ordered.map(x=>x.client)).size !== ordered.length) return Promise.reject(new Error('invalid-setup-request'));
|
|
103
|
+
const digest = crypto.createHash('sha256').update(JSON.stringify(ordered.map(({client,authorization_url})=>({client,authorization_url})))).digest('hex');
|
|
104
|
+
const state = `cli-v1.${ordered.map(x=>x.client).join('-')}.${digest}.${crypto.randomBytes(32).toString('base64url')}`;
|
|
105
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
106
|
+
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
107
|
+
const authorizeUrl = createAuthorizationUrl({ state, codeChallenge: challenge, issuer });
|
|
108
|
+
let callbackReceived = false;
|
|
109
|
+
let settled = false;
|
|
110
|
+
let timeout;
|
|
111
|
+
let abortListener;
|
|
112
|
+
let resolveFlow;
|
|
113
|
+
let rejectFlow;
|
|
114
|
+
const server = http.createServer();
|
|
115
|
+
|
|
116
|
+
const flow = new Promise((resolve, reject) => {
|
|
117
|
+
resolveFlow = resolve;
|
|
118
|
+
rejectFlow = reject;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const finish = (error) => {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
settled = true;
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
if (signal && abortListener) signal.removeEventListener('abort', abortListener);
|
|
126
|
+
try { server.close(); } catch {}
|
|
127
|
+
if (error) rejectFlow(error);
|
|
128
|
+
else resolveFlow({ authenticated: true });
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
server.on('request', async (request, response) => {
|
|
132
|
+
if (!isLoopbackAddress(request.socket.remoteAddress)) {
|
|
133
|
+
writeCallbackPage(response, 403, 'This sign-in callback is not allowed.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (request.method !== 'GET' || typeof request.url !== 'string' || request.url.length > 4096) {
|
|
137
|
+
writeCallbackPage(response, 404, 'This sign-in link is not valid.');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let callbackUrl;
|
|
142
|
+
try {
|
|
143
|
+
callbackUrl = new URL(request.url, OAUTH_REDIRECT_URI);
|
|
144
|
+
} catch {
|
|
145
|
+
writeCallbackPage(response, 400, 'This sign-in link is not valid.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (callbackUrl.origin !== `http://127.0.0.1:${OAUTH_PORT}` || callbackUrl.pathname !== '/oauth/callback') {
|
|
149
|
+
writeCallbackPage(response, 404, 'This sign-in link is not valid.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const receivedState = callbackUrl.searchParams.get('state') || '';
|
|
154
|
+
const receivedStateBuffer = Buffer.from(receivedState);
|
|
155
|
+
const expectedStateBuffer = Buffer.from(state);
|
|
156
|
+
const stateMatches = receivedStateBuffer.length === expectedStateBuffer.length
|
|
157
|
+
&& crypto.timingSafeEqual(receivedStateBuffer, expectedStateBuffer);
|
|
158
|
+
if (!stateMatches) {
|
|
159
|
+
writeCallbackPage(response, 400, 'This sign-in request could not be verified.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (callbackReceived) {
|
|
163
|
+
writeCallbackPage(response, 409, 'This sign-in request has already been used.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
callbackReceived = true;
|
|
168
|
+
const providerError = callbackUrl.searchParams.get('error');
|
|
169
|
+
if (providerError) {
|
|
170
|
+
writeCallbackPage(response, 401, 'Sign-in was cancelled.');
|
|
171
|
+
finish(new Error('oauth-cancelled'));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const code = callbackUrl.searchParams.get('code') || '';
|
|
176
|
+
if (!/^[A-Za-z0-9._~-]{8,2048}$/.test(code)) {
|
|
177
|
+
writeCallbackPage(response, 400, 'The sign-in response was incomplete.');
|
|
178
|
+
finish(new Error('oauth-code-invalid'));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await exchangeAuthorizationCode({ code, codeVerifier: verifier, sessionSink, issuer, fetchImpl });
|
|
184
|
+
writeCallbackPage(response, 200, 'You’re signed in to Slaunt.');
|
|
185
|
+
finish(null);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
writeCallbackPage(response, 401, 'Sign-in could not be completed. Return to Slaunt CLI and try again.');
|
|
188
|
+
finish(new Error(error?.message === 'credential-store-unavailable'
|
|
189
|
+
? 'credential-store-unavailable'
|
|
190
|
+
: error?.message === 'credential-store-unsafe'
|
|
191
|
+
? 'credential-store-unsafe'
|
|
192
|
+
: 'oauth-token-exchange-failed'));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
server.on('error', (error) => {
|
|
197
|
+
finish(new Error(error?.code === 'EADDRINUSE' ? 'oauth-callback-port-in-use' : 'oauth-callback-unavailable'));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (signal?.aborted) {
|
|
201
|
+
finish(new Error('oauth-cancelled'));
|
|
202
|
+
return flow;
|
|
203
|
+
}
|
|
204
|
+
if (signal) {
|
|
205
|
+
abortListener = () => finish(new Error('oauth-cancelled'));
|
|
206
|
+
signal.addEventListener('abort', abortListener, { once: true });
|
|
207
|
+
}
|
|
208
|
+
timeout = setTimeout(() => finish(new Error('oauth-timeout')), OAUTH_FLOW_TIMEOUT_MS);
|
|
209
|
+
timeout.unref?.();
|
|
210
|
+
|
|
211
|
+
server.listen(OAUTH_PORT, '127.0.0.1', async () => {
|
|
212
|
+
try {
|
|
213
|
+
await shell.openExternal(authorizeUrl.toString());
|
|
214
|
+
} catch {
|
|
215
|
+
finish(new Error('oauth-browser-unavailable'));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return flow;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
OAUTH_ISSUER,
|
|
224
|
+
OAUTH_CLIENT_ID,
|
|
225
|
+
OAUTH_REDIRECT_URI,
|
|
226
|
+
OAUTH_SCOPE,
|
|
227
|
+
exchangeAuthorizationCode,
|
|
228
|
+
createAuthorizationUrl,
|
|
229
|
+
startOAuthFlow
|
|
230
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Terminal-cell rendering derived from the workspace's Neue Haas Display
|
|
3
|
+
// Medium font, with character advances tightened by 0.08em. Terminals still
|
|
4
|
+
// control the appearance and dimensions of the block glyphs.
|
|
5
|
+
const COMPACT_WORDMARK = [
|
|
6
|
+
' ██ ▄▄',
|
|
7
|
+
' ▄▄▄▄ ██ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄▄ ▄██▄',
|
|
8
|
+
' ██▀▀██ ██ ▄█▀▀▀████ ██ ██▀▀▀██ ▀██▀',
|
|
9
|
+
' ▀▀███▄ ██ ▄▄██▀████ ██ ██ ██ ██',
|
|
10
|
+
' ██▄▄██▀██ ██▄▄▄████▄▄▄██ ██ ██ ██▄',
|
|
11
|
+
' ▀▀▀▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀'
|
|
12
|
+
];
|
|
13
|
+
module.exports = { COMPACT_WORDMARK };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const os=require('node:os');
|
|
3
|
+
const path=require('node:path');
|
|
4
|
+
const {createBrokerKeychain}=require('./broker-keychain');
|
|
5
|
+
const {createBrokerGrantProvider}=require('./broker-grants');
|
|
6
|
+
const {startBrokerService}=require('./broker-service');
|
|
7
|
+
async function main(){
|
|
8
|
+
const manifest=require('../../release/manifest.json');
|
|
9
|
+
if(manifest.signing!=='adhoc'||!/^[a-f0-9]{40}$/.test(manifest.cdhash||''))throw new Error('broker-release-invalid');
|
|
10
|
+
const app=path.join(os.homedir(),'Library/Application Support/Slaunt/KillBar/SlauntKillBar.app');
|
|
11
|
+
const store=createBrokerKeychain({app,trust:{mode:'adhoc',cdhash:manifest.cdhash}});
|
|
12
|
+
const grantProvider=createBrokerGrantProvider({store});
|
|
13
|
+
const service=await startBrokerService({grantProvider,initialize:()=>grantProvider.getFreshTokens()});
|
|
14
|
+
const stop=()=>{void service.close().finally(()=>process.exit(0));};
|
|
15
|
+
process.once('SIGINT',stop);process.once('SIGTERM',stop);
|
|
16
|
+
}
|
|
17
|
+
if(require.main===module)void main().catch(()=>{process.stderr.write('Slaunt broker could not start.\n');process.exitCode=1;});
|
|
18
|
+
module.exports={main};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {readJson}=require('../read-json');
|
|
3
|
+
const TOKEN_ENDPOINT='https://central-mcp-ccr-production.up.railway.app/token';
|
|
4
|
+
const CLIENT_ID='ai.slaunt.claude-broker';
|
|
5
|
+
const validToken=value=>typeof value==='string' && value.length>0 && value.length<=16384 && !/[\x00-\x1f\x7f]/.test(value);
|
|
6
|
+
|
|
7
|
+
// store.update must serialize mutations across processes and durably commit
|
|
8
|
+
// before returning. The caller supplies an OS-protected credential store.
|
|
9
|
+
function createBrokerGrantProvider({store,fetchImpl=fetch,now=Date.now}={}) {
|
|
10
|
+
if(!store || typeof store.get!=='function'||typeof store.update!=='function')throw new Error('broker-grant-store-unavailable');
|
|
11
|
+
async function refresh(current){
|
|
12
|
+
if(!validToken(current?.refreshToken))throw new Error('broker-grant-unavailable');
|
|
13
|
+
let response;try{response=await fetchImpl(TOKEN_ENDPOINT,{method:'POST',redirect:'error',cache:'no-store',signal:AbortSignal.timeout(15000),headers:{'content-type':'application/x-www-form-urlencoded'},body:new URLSearchParams({grant_type:'refresh_token',client_id:CLIENT_ID,refresh_token:current.refreshToken})});}catch{throw new Error('broker-refresh-unavailable');}
|
|
14
|
+
if(!response.ok)throw new Error('broker-refresh-unavailable');
|
|
15
|
+
let payload;try{payload=await readJson(response,32768);}catch{throw new Error('broker-refresh-invalid');}
|
|
16
|
+
const lifetime=Number(payload?.expires_in);
|
|
17
|
+
if(!validToken(payload?.access_token)||!validToken(payload?.refresh_token)||payload.scope!=='slaunt:guard'||String(payload?.token_type||'').toLowerCase()!=='bearer'||!Number.isSafeInteger(lifetime)||lifetime<1||lifetime>86400)throw new Error('broker-refresh-invalid');
|
|
18
|
+
return {accessToken:payload.access_token,refreshToken:payload.refresh_token,expiresAt:now()+lifetime*1000};
|
|
19
|
+
}
|
|
20
|
+
async function getFreshTokens(){
|
|
21
|
+
const current=await store.get();if(!current)throw new Error('broker-grant-unavailable');
|
|
22
|
+
if(validToken(current.accessToken)&&Number.isSafeInteger(current.expiresAt)&¤t.expiresAt>now()+60000)return current;
|
|
23
|
+
return store.update(async latest=>{
|
|
24
|
+
if(latest&&validToken(latest.accessToken)&&Number.isSafeInteger(latest.expiresAt)&&latest.expiresAt>now()+60000)return latest;
|
|
25
|
+
return refresh(latest);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function refreshAfterUnauthorized(failedAccessToken){
|
|
29
|
+
return store.update(async latest=>{
|
|
30
|
+
if(!latest)throw new Error('broker-grant-unavailable');
|
|
31
|
+
if(latest.accessToken!==failedAccessToken&&validToken(latest.accessToken)&&latest.expiresAt>now())return latest;
|
|
32
|
+
return refresh(latest);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {getFreshTokens,refreshAfterUnauthorized};
|
|
36
|
+
}
|
|
37
|
+
module.exports={createBrokerGrantProvider,CLIENT_ID,TOKEN_ENDPOINT};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs=require('node:fs');
|
|
3
|
+
const net=require('node:net');
|
|
4
|
+
const path=require('node:path');
|
|
5
|
+
const {serviceRoot}=require('./broker-service');
|
|
6
|
+
async function sendLifecycle(input,{root=serviceRoot(),timeoutMs=10000}={}) {
|
|
7
|
+
const event_name=input?.hook_event_name,session_id=input?.session_id;
|
|
8
|
+
if(!['SessionStart','UserPromptSubmit','Stop'].includes(event_name)||typeof session_id!=='string'||!session_id.trim()||session_id.length>256||/[\x00-\x1f\x7f]/.test(session_id))throw new Error('invalid-lifecycle-event');
|
|
9
|
+
if(input.agent_id && event_name==='UserPromptSubmit')return;
|
|
10
|
+
const directory=fs.lstatSync(root),address=path.join(root,'control.sock'),socketStat=fs.lstatSync(address);
|
|
11
|
+
if(!directory.isDirectory()||directory.isSymbolicLink()||directory.uid!==process.getuid()||(directory.mode&0o077)||!socketStat.isSocket()||socketStat.uid!==process.getuid()||(socketStat.mode&0o077))throw new Error('broker-unavailable');
|
|
12
|
+
await new Promise((resolve,reject)=>{
|
|
13
|
+
const socket=net.createConnection(address);let response='',done=false;
|
|
14
|
+
const finish=ok=>{if(done)return;done=true;clearTimeout(timer);socket.destroy();ok?resolve():reject(new Error('broker-unavailable'));};
|
|
15
|
+
const timer=setTimeout(()=>finish(false),timeoutMs);
|
|
16
|
+
socket.once('connect',()=>socket.write(`${JSON.stringify({event_name,session_id})}\n`));
|
|
17
|
+
socket.on('data',chunk=>{response+=chunk.toString();if(Buffer.byteLength(response)>4096)return finish(false);if(response.includes('\n')){try{finish(JSON.parse(response).ok===true);}catch{finish(false);}}});
|
|
18
|
+
socket.once('error',()=>finish(false));socket.once('end',()=>finish(false));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function main(){let raw='';for await(const chunk of process.stdin){raw+=chunk;if(Buffer.byteLength(raw)>1048576)throw new Error('invalid-lifecycle-event');}await sendLifecycle(JSON.parse(raw));process.stdout.write('{}\n');}
|
|
22
|
+
if(require.main===module)void main().catch(()=>{process.stderr.write('Slaunt broker unavailable.\n');process.exitCode=1;});
|
|
23
|
+
module.exports={sendLifecycle};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {spawn}=require('node:child_process');
|
|
3
|
+
const path=require('node:path');
|
|
4
|
+
const {verifyHelper}=require('../killbar-enrollment');
|
|
5
|
+
|
|
6
|
+
function createBrokerKeychain({app,trust,verify=verifyHelper,spawnImpl=spawn}={}) {
|
|
7
|
+
if(!path.isAbsolute(app||'')||!app.endsWith('/SlauntKillBar.app'))throw new Error('broker-keychain-unavailable');
|
|
8
|
+
const binary=path.join(app,'Contents/MacOS/SlauntKillBar');
|
|
9
|
+
let queue=Promise.resolve();
|
|
10
|
+
async function invoke(command,input){
|
|
11
|
+
await verify(app,trust);
|
|
12
|
+
return new Promise((resolve,reject)=>{
|
|
13
|
+
const child=spawnImpl(binary,[command],{stdio:['pipe','pipe','ignore'],env:{HOME:process.env.HOME,PATH:'/usr/bin:/bin',TMPDIR:process.env.TMPDIR}});
|
|
14
|
+
let output='',done=false;const finish=error=>{if(done)return;done=true;clearTimeout(timer);error?reject(new Error('broker-keychain-unavailable')):resolve(output);};
|
|
15
|
+
const timer=setTimeout(()=>{child.kill('SIGKILL');finish(true);},15000);
|
|
16
|
+
child.stdout.on('data',chunk=>{output+=chunk;if(Buffer.byteLength(output)>32768){child.kill('SIGKILL');finish(true);}});
|
|
17
|
+
child.stdin.on('error',()=>finish(true));child.on('error',()=>finish(true));child.on('close',code=>finish(code!==0));
|
|
18
|
+
child.stdin.end(input===undefined?'':JSON.stringify(input));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function get(){let value;try{value=JSON.parse(await invoke('--broker-grant-read'));}catch{throw new Error('broker-grant-unavailable');}return value;}
|
|
22
|
+
const serialize=operation=>{const current=queue.then(operation);queue=current.catch(()=>{});return current;};
|
|
23
|
+
return {
|
|
24
|
+
get,
|
|
25
|
+
save:grant=>serialize(async()=>{await invoke('--broker-grant-create-stdin',grant);}),
|
|
26
|
+
update:operation=>serialize(async()=>{const current=await get();const next=await operation(current);await invoke('--broker-grant-update-stdin',next);return next;})
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
module.exports={createBrokerKeychain};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs=require('node:fs/promises');
|
|
3
|
+
const path=require('node:path');
|
|
4
|
+
const net=require('node:net');
|
|
5
|
+
const {execFile}=require('node:child_process');
|
|
6
|
+
const {promisify}=require('node:util');
|
|
7
|
+
const {serviceRoot}=require('./broker-service');
|
|
8
|
+
const execute=promisify(execFile),LABEL='ai.slaunt.claude-broker';
|
|
9
|
+
const xml=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
10
|
+
function brokerPlist({node=process.execPath,script=path.join(__dirname,'broker-daemon.js')}={}){
|
|
11
|
+
if(!path.isAbsolute(node)||!path.isAbsolute(script)||/[\x00-\x1f\x7f]/.test(node+script))throw new Error('broker-launch-invalid');
|
|
12
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${LABEL}</string><key>ProgramArguments</key><array><string>${xml(node)}</string><string>${xml(script)}</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>30</integer><key>EnvironmentVariables</key><dict><key>NODE_OPTIONS</key><string></string><key>NODE_PATH</key><string></string></dict></dict></plist>\n`;
|
|
13
|
+
}
|
|
14
|
+
async function health(address){
|
|
15
|
+
try{const root=await fs.lstat(path.dirname(address)),socket=await fs.lstat(address);if(!root.isDirectory()||root.isSymbolicLink()||root.uid!==process.getuid()||(root.mode&0o077)||!socket.isSocket()||socket.uid!==process.getuid()||(socket.mode&0o077))return false;}catch{return false;}
|
|
16
|
+
return new Promise(resolve=>{const s=net.createConnection(address);let raw='',done=false;const finish=ok=>{if(done)return;done=true;clearTimeout(timer);s.destroy();resolve(ok);};const timer=setTimeout(()=>finish(false),1000);s.once('connect',()=>s.write('{"health":true}\n'));s.on('data',c=>{raw+=c;if(raw.length>4096)return finish(false);if(raw.includes('\n')){try{finish(JSON.parse(raw).ok===true);}catch{finish(false);}}});s.once('error',()=>finish(false));s.once('end',()=>finish(false));});
|
|
17
|
+
}
|
|
18
|
+
async function installBrokerLaunchAgent({home,platform=process.platform,exec=execute,check=health}={}){
|
|
19
|
+
if(platform!=='darwin'||!path.isAbsolute(home||'')||!process.getuid)throw new Error('broker-launch-unavailable');
|
|
20
|
+
const agents=path.join(home,'Library/LaunchAgents'),stats=await fs.lstat(agents);
|
|
21
|
+
if(!stats.isDirectory()||stats.isSymbolicLink()||stats.uid!==process.getuid()||(stats.mode&0o022))throw new Error('broker-launch-unsafe');
|
|
22
|
+
const file=path.join(agents,`${LABEL}.plist`),domain=`gui/${process.getuid()}`;
|
|
23
|
+
await exec('/bin/launchctl',['print',domain],{timeout:10000});
|
|
24
|
+
try{await fs.writeFile(file,brokerPlist(),{mode:0o600,flag:'wx'});}catch{throw new Error('broker-existing-install');}
|
|
25
|
+
let attempted=false;
|
|
26
|
+
try{
|
|
27
|
+
attempted=true;await exec('/bin/launchctl',['bootstrap',domain,file],{timeout:15000});
|
|
28
|
+
await exec('/bin/launchctl',['print',`${domain}/${LABEL}`],{timeout:10000});
|
|
29
|
+
const address=path.join(serviceRoot(),'control.sock');
|
|
30
|
+
for(let i=0;i<25;i++){if(await check(address))return {registered:true};await new Promise(resolve=>setTimeout(resolve,200));}
|
|
31
|
+
throw new Error('broker-start-unverified');
|
|
32
|
+
}catch{if(!attempted)await fs.unlink(file).catch(()=>{});throw new Error(attempted?'broker-start-unverified':'broker-install-failed');}
|
|
33
|
+
}
|
|
34
|
+
module.exports={brokerPlist,installBrokerLaunchAgent,health};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs=require('node:fs');
|
|
3
|
+
const net=require('node:net');
|
|
4
|
+
const path=require('node:path');
|
|
5
|
+
const {startNativeClaudeConnection}=require('./native-claude-connection');
|
|
6
|
+
|
|
7
|
+
function serviceRoot(){if(!process.getuid)throw new Error('broker-platform-unsupported');return `/tmp/slaunt-broker-${process.getuid()}`;}
|
|
8
|
+
function ensureRoot(root){
|
|
9
|
+
try{fs.mkdirSync(root,{mode:0o700});}catch(e){if(e.code!=='EEXIST')throw e;}
|
|
10
|
+
const s=fs.lstatSync(root);if(!s.isDirectory()||s.isSymbolicLink()||s.uid!==process.getuid()||(s.mode&0o077))throw new Error('broker-directory-unsafe');
|
|
11
|
+
}
|
|
12
|
+
async function startBrokerService({grantProvider,root=serviceRoot(),connect=startNativeClaudeConnection,initialize=async()=>{}}={}) {
|
|
13
|
+
ensureRoot(root);
|
|
14
|
+
const address=path.join(root,'control.sock');
|
|
15
|
+
let old;try{old=fs.lstatSync(address);}catch(e){if(e.code!=='ENOENT')throw e;}
|
|
16
|
+
if(old){
|
|
17
|
+
if(!old.isSocket()||old.uid!==process.getuid())throw new Error('broker-socket-unsafe');
|
|
18
|
+
await new Promise((resolve,reject)=>{const probe=net.createConnection(address);probe.setTimeout(500,()=>{probe.destroy();reject(new Error('broker-already-running'));});probe.once('connect',()=>{probe.destroy();reject(new Error('broker-already-running'));});probe.once('error',e=>e.code==='ECONNREFUSED'||e.code==='ENOENT'?resolve():reject(e));});
|
|
19
|
+
const current=fs.lstatSync(address);if(current.ino!==old.ino||current.dev!==old.dev)throw new Error('broker-socket-changed');fs.unlinkSync(address);
|
|
20
|
+
}
|
|
21
|
+
const sessions=new Map(),sockets=new Set();let closed=false,resolveReady,rejectReady;
|
|
22
|
+
const ready=new Promise((resolve,reject)=>{resolveReady=resolve;rejectReady=reject;});ready.catch(()=>{});
|
|
23
|
+
async function event(input){
|
|
24
|
+
if(closed||!input||typeof input!=='object'||Array.isArray(input)||!['SessionStart','UserPromptSubmit','Stop'].includes(input.event_name)||typeof input.session_id!=='string'||!input.session_id.trim()||input.session_id.length>256||/[\x00-\x1f\x7f]/.test(input.session_id)||Object.keys(input).some(k=>!['event_name','session_id'].includes(k)))throw new Error('broker-event-invalid');
|
|
25
|
+
let session=sessions.get(input.session_id);
|
|
26
|
+
if(!session){if(input.event_name==='Stop')return;if(sessions.size>=64)throw new Error('broker-capacity');session=Promise.resolve().then(()=>connect({grantProvider}));sessions.set(input.session_id,session);}
|
|
27
|
+
try{const connection=await session;await connection.event(input);if(input.event_name==='Stop'){sessions.delete(input.session_id);await connection.close();}}
|
|
28
|
+
catch(error){if(sessions.get(input.session_id)===session)sessions.delete(input.session_id);try{await (await session).close();}catch{}throw error;}
|
|
29
|
+
}
|
|
30
|
+
const server=net.createServer(socket=>{
|
|
31
|
+
sockets.add(socket);socket.on('close',()=>sockets.delete(socket));socket.on('error',()=>{});
|
|
32
|
+
let data='',handled=false;const timer=setTimeout(()=>socket.destroy(),15000);socket.once('close',()=>clearTimeout(timer));
|
|
33
|
+
socket.on('data',chunk=>{if(handled)return;data+=chunk.toString('utf8');if(Buffer.byteLength(data)>4096){handled=true;socket.end('{"ok":false}\n');return;}if(!data.includes('\n'))return;handled=true;void (async()=>{try{await ready;const input=JSON.parse(data);if(!(input?.health===true && Object.keys(input).length===1))await event(input);socket.end('{"ok":true}\n');}catch{socket.end('{"ok":false}\n');}})();});
|
|
34
|
+
});
|
|
35
|
+
server.maxConnections=64;
|
|
36
|
+
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(address,()=>{try{fs.chmodSync(address,0o600);resolve();}catch{reject(new Error('broker-socket-unavailable'));}});}).catch(e=>{server.close();throw e;});
|
|
37
|
+
try{await initialize();resolveReady();}catch(error){rejectReady(error);for(const s of sockets)s.destroy();server.close();throw error;}
|
|
38
|
+
return {address,async close(){if(closed)return;closed=true;for(const s of sockets)s.destroy();server.close();await Promise.allSettled([...sessions.values()].map(async promise=>(await promise).close()));sessions.clear();}};
|
|
39
|
+
}
|
|
40
|
+
module.exports={startBrokerService,serviceRoot};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Electron's embedded Node runs this hook without opening any desktop window.
|
|
3
|
+
const { runCaptureCommand } = require('./chat-hook-capture');
|
|
4
|
+
void runCaptureCommand().then(() => process.exit(0), () => {
|
|
5
|
+
process.stderr.write('Slaunt visible chat capture unavailable (capture-broker-unavailable).\n');
|
|
6
|
+
process.stdout.write('{}\n', () => process.exit(0));
|
|
7
|
+
});
|