slaunt 0.1.18 → 0.1.20
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/.github/workflows/publish.yml +59 -0
- package/bin/slaunt-hooks.js +6 -2
- package/package.json +1 -1
- package/src/full-screen.js +1 -3
- package/src/hidden-oauth.js +1 -1
- package/src/oauth-cohort.js +6 -3
- package/src/setup-flow.js +4 -1
- package/test/hidden-oauth.test.js +12 -0
- package/test/killbar-enrollment.test.js +6 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish npm package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: macos-14
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '24'
|
|
20
|
+
package-manager-cache: false
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: npm ci --ignore-scripts
|
|
23
|
+
- name: Run tests
|
|
24
|
+
run: npm test
|
|
25
|
+
- name: Verify bundled helper
|
|
26
|
+
run: |
|
|
27
|
+
node -e "const f=require('fs'),c=require('crypto'),m=require('./release/manifest.json'); if(m.format!==1 || m.arch!=='arm64' || m.signing!=='adhoc' || !/^[a-f0-9]{40}$/.test(m.cdhash) || c.createHash('sha256').update(f.readFileSync('release/SlauntKillBar.zip')).digest('hex')!==m.sha256) process.exit(1)"
|
|
28
|
+
mkdir "$RUNNER_TEMP/slaunt-helper"
|
|
29
|
+
ditto -x -k release/SlauntKillBar.zip "$RUNNER_TEMP/slaunt-helper"
|
|
30
|
+
codesign --verify --deep --strict "$RUNNER_TEMP/slaunt-helper/SlauntKillBar.app"
|
|
31
|
+
EXPECTED_CDHASH="$(node -p "require('./release/manifest.json').cdhash")"
|
|
32
|
+
codesign -dv --verbose=4 "$RUNNER_TEMP/slaunt-helper/SlauntKillBar.app" 2> "$RUNNER_TEMP/codesign-info"
|
|
33
|
+
grep -Fx "CDHash=$EXPECTED_CDHASH" "$RUNNER_TEMP/codesign-info"
|
|
34
|
+
grep -Fx "Identifier=ai.slaunt.killbar" "$RUNNER_TEMP/codesign-info"
|
|
35
|
+
grep -Fx "Signature=adhoc" "$RUNNER_TEMP/codesign-info"
|
|
36
|
+
- name: Prepare public package metadata
|
|
37
|
+
env:
|
|
38
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
39
|
+
run: |
|
|
40
|
+
node <<'NODE'
|
|
41
|
+
const fs = require('fs');
|
|
42
|
+
const tag = process.env.RELEASE_TAG;
|
|
43
|
+
if (!/^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(tag)) throw Error('Expected a stable version tag, e.g. v0.1.20');
|
|
44
|
+
const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
45
|
+
p.name = 'slaunt';
|
|
46
|
+
p.version = tag.slice(1);
|
|
47
|
+
delete p.private;
|
|
48
|
+
p.bin = { slaunt: 'bin/slaunt-hooks.js', 'slaunt-hooks': 'bin/slaunt-hooks.js' };
|
|
49
|
+
p.repository = { type: 'git', url: 'git+https://github.com/kerodem/slaunt-hook-cli.git' };
|
|
50
|
+
fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');
|
|
51
|
+
NODE
|
|
52
|
+
- name: Verify package contents
|
|
53
|
+
run: |
|
|
54
|
+
npm pack --dry-run --json > "$RUNNER_TEMP/slaunt-pack.json"
|
|
55
|
+
node -e "const p=require(process.env.RUNNER_TEMP+'/slaunt-pack.json')[0]; for(const n of ['bin/slaunt-hooks.js','release/manifest.json','release/SlauntKillBar.zip']) if(!p.files.some(f=>f.path===n)) throw Error('Missing package file: '+n)"
|
|
56
|
+
- name: Publish to npm
|
|
57
|
+
env:
|
|
58
|
+
NPM_CONFIG_PROVENANCE: 'false'
|
|
59
|
+
run: npm publish --access public
|
package/bin/slaunt-hooks.js
CHANGED
|
@@ -75,10 +75,14 @@ async function main(args=process.argv.slice(2),dependencies={}) {
|
|
|
75
75
|
if(clientLabel && error?.message==='client-configuration-conflict') messages[error.message]=`${clientLabel} already has a conflicting Slaunt configuration.`;
|
|
76
76
|
if(clientLabel && error?.message==='client-configuration-failed') messages[error.message]=`${clientLabel} configuration could not be updated.`;
|
|
77
77
|
if(error?.message==='cohort-failed') {
|
|
78
|
-
if(error.setupStage==='native-login' && clientLabel)
|
|
78
|
+
if(error.setupStage==='native-login' && clientLabel) {
|
|
79
|
+
const nativeMessages={launch:'could not be started',request:'did not provide a valid authorization request', 'waiting-approval':'exited while waiting for authorization', callback:'could not complete the authorization handoff', 'client-exit':'did not complete authorization after the callback'};
|
|
80
|
+
messages[error.message]=`${clientLabel} ${nativeMessages[error.nativeStage] || 'could not complete its Slaunt authorization'}.`;
|
|
81
|
+
}
|
|
79
82
|
else if(error.setupStage==='authorization') {
|
|
80
83
|
const details={'sign-in':'Slaunt sign-in could not be completed.','workspace':'Slaunt workspace identity could not be verified.','enrollment':'KillBar enrollment could not be completed.','client-grants':'Slaunt could not issue client authorization grants.'};
|
|
81
|
-
|
|
84
|
+
const enrollmentMessages={'killbar-helper-unavailable':'KillBar helper is unavailable.','killbar-helper-untrusted':'KillBar helper could not be verified.','killbar-authentication-failed':'KillBar could not use the sign-in grant.','killbar-identity-invalid':'KillBar workspace identity is invalid.','killbar-lock-unavailable':'KillBar enrollment lock could not be created.','killbar-enrollment-busy':'Another KillBar enrollment is in progress.','killbar-status-unavailable':'KillBar status could not be read.','killbar-status-invalid':'KillBar returned an invalid status.','killbar-restart-required':'KillBar requires a restart before enrollment.','killbar-already-enrolled':'KillBar is already enrolled.','killbar-enrollment-failed':'KillBar enrollment request failed.','killbar-enrollment-unverified':'KillBar enrollment could not be verified for this workspace.'};
|
|
85
|
+
messages[error.message]=(error.setupDetail==='enrollment' && enrollmentMessages[error.enrollmentReason]) || details[error.setupDetail] || 'Slaunt authorization or enrollment could not be completed.';
|
|
82
86
|
}
|
|
83
87
|
else if(error.setupStage==='timeout') messages[error.message]='Slaunt authorization timed out.';
|
|
84
88
|
}
|
package/package.json
CHANGED
package/src/full-screen.js
CHANGED
|
@@ -21,7 +21,7 @@ function createFullScreen({output,logo,color,motion}) {
|
|
|
21
21
|
const contents=[];
|
|
22
22
|
const add=(text='',tone='normal')=>contents.push({text,tone});
|
|
23
23
|
add('');
|
|
24
|
-
add(view==='select'?' CONNECT YOUR CLIENTS':'
|
|
24
|
+
add(view==='select'?' CONNECT YOUR CLIENTS':' Installing Slaunt','heading');
|
|
25
25
|
add('');
|
|
26
26
|
if(view==='select'){
|
|
27
27
|
add(cols<52?' Choose clients':' Which clients would you like to connect?','title');
|
|
@@ -42,8 +42,6 @@ function createFullScreen({output,logo,color,motion}) {
|
|
|
42
42
|
if(spacious)add('');
|
|
43
43
|
add(' ↑↓ move ↵ select esc','muted');
|
|
44
44
|
} else {
|
|
45
|
-
add(` ${selection||'Preparing your setup'}`,'title');
|
|
46
|
-
add('');
|
|
47
45
|
for(const item of completed.slice(-4))add(` ✓ ${item}`,'success');
|
|
48
46
|
if(current)add(` ${motion?'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[tick%10]:'…'} ${current}`,'selected');
|
|
49
47
|
if(failure){add(` ! ${failure.message}`,'warning');if(failure.detail)add(` ${failure.detail}`,'muted');}
|
package/src/hidden-oauth.js
CHANGED
|
@@ -34,7 +34,7 @@ async function runHiddenLogin({ executable, commandPath = executable, issuer, au
|
|
|
34
34
|
let settled = false, text = '', found = false, submitted = false, timer;
|
|
35
35
|
const result = new Promise((resolve, reject) => {
|
|
36
36
|
const onAbort = () => finish(new Error('login-cancelled'));
|
|
37
|
-
const finish = (err) => { if (settled) return; settled = true; clearTimeout(timer); signal?.removeEventListener('abort', onAbort); if (err) { child.kill(); reject(err); } else resolve(); };
|
|
37
|
+
const finish = (err) => { if (err) { err.nativeStage = err.message === 'client-start-failed' ? 'launch' : err.message === 'authorization-handoff-failed' || err.message === 'login-input-failed' ? 'callback' : submitted ? 'client-exit' : found ? 'waiting-approval' : 'request'; } if (settled) return; settled = true; clearTimeout(timer); signal?.removeEventListener('abort', onAbort); if (err) { child.kill(); reject(err); } else resolve(); };
|
|
38
38
|
if (signal?.aborted) { finish(new Error('login-cancelled')); return; }
|
|
39
39
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
40
40
|
child.stdin.on('error', () => finish(new Error('login-input-failed')));
|
package/src/oauth-cohort.js
CHANGED
|
@@ -12,7 +12,7 @@ async function authorizeCohort({ clients, issuer, approve, login = runHiddenLogi
|
|
|
12
12
|
let timer;
|
|
13
13
|
let rejectOverall;
|
|
14
14
|
const failedPromise = new Promise((_, reject) => { rejectOverall = reject; });
|
|
15
|
-
const fail = (stage, client, detail) => {
|
|
15
|
+
const fail = (stage, client, detail, enrollmentReason) => {
|
|
16
16
|
if (failed) return;
|
|
17
17
|
failed = true;
|
|
18
18
|
clearTimeout(timer);
|
|
@@ -21,7 +21,10 @@ async function authorizeCohort({ clients, issuer, approve, login = runHiddenLogi
|
|
|
21
21
|
const error = new Error('cohort-failed');
|
|
22
22
|
error.setupStage = stage;
|
|
23
23
|
if (['sign-in','workspace','enrollment','client-grants'].includes(detail)) error.setupDetail = detail;
|
|
24
|
+
const enrollmentReasons=['killbar-helper-unavailable','killbar-helper-untrusted','killbar-authentication-failed','killbar-identity-invalid','killbar-lock-unavailable','killbar-enrollment-busy','killbar-status-unavailable','killbar-status-invalid','killbar-restart-required','killbar-already-enrolled','killbar-enrollment-failed','killbar-enrollment-unverified'];
|
|
25
|
+
if(stage==='authorization' && detail==='enrollment' && enrollmentReasons.includes(enrollmentReason)) error.enrollmentReason=enrollmentReason;
|
|
24
26
|
if (client === 'codex' || client === 'claude') error.setupClient = client;
|
|
27
|
+
if (stage === 'native-login' && ['launch','request','waiting-approval','callback','client-exit'].includes(detail)) error.nativeStage = detail;
|
|
25
28
|
rejectOverall(error);
|
|
26
29
|
};
|
|
27
30
|
timer = setTimeout(() => fail('timeout'), timeoutMs);
|
|
@@ -35,9 +38,9 @@ async function authorizeCohort({ clients, issuer, approve, login = runHiddenLogi
|
|
|
35
38
|
if (failed) return;
|
|
36
39
|
if (!callbacks || typeof callbacks !== 'object' || Array.isArray(callbacks) || Object.keys(callbacks).length !== clients.length || clients.some(name => typeof callbacks[name] !== 'string')) throw new Error('cohort-invalid-response');
|
|
37
40
|
for (const name of clients) pending.get(name).resolve(callbacks[name]);
|
|
38
|
-
}).catch(error => fail('authorization', undefined, error?.setupDetail));
|
|
41
|
+
}).catch(error => fail('authorization', undefined, error?.setupDetail, error?.enrollmentReason));
|
|
39
42
|
});
|
|
40
|
-
const runs = clients.map(executable => Promise.resolve().then(() => login({executable,commandPath:executablePaths[executable] || executable,issuer,authorize,timeoutMs,signal:controller.signal})).catch(error => { fail('native-login', executable); throw error; }));
|
|
43
|
+
const runs = clients.map(executable => Promise.resolve().then(() => login({executable,commandPath:executablePaths[executable] || executable,issuer,authorize,timeoutMs,signal:controller.signal})).catch(error => { fail('native-login', executable, error?.nativeStage); throw error; }));
|
|
41
44
|
try { await Promise.race([Promise.all(runs), failedPromise]); } finally { clearTimeout(timer); }
|
|
42
45
|
}
|
|
43
46
|
module.exports = { authorizeCohort };
|
package/src/setup-flow.js
CHANGED
|
@@ -59,7 +59,10 @@ async function runSetupFlow({clients,issuer=ISSUER,fetchImpl=fetch,shell={openEx
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
return payload.callbacks;
|
|
62
|
-
} catch { const error=new Error('setup-step-failed'); error.setupDetail=diagnosticStage;
|
|
62
|
+
} catch (cause) { const error=new Error('setup-step-failed'); error.setupDetail=diagnosticStage;
|
|
63
|
+
const allowed=['killbar-helper-unavailable','killbar-helper-untrusted','killbar-authentication-failed','killbar-identity-invalid','killbar-lock-unavailable','killbar-enrollment-busy','killbar-status-unavailable','killbar-status-invalid','killbar-restart-required','killbar-already-enrolled','killbar-enrollment-failed','killbar-enrollment-unverified'];
|
|
64
|
+
if(diagnosticStage==='enrollment' && allowed.includes(cause?.message)) error.enrollmentReason=cause.message;
|
|
65
|
+
throw error; }
|
|
63
66
|
}});
|
|
64
67
|
} finally {session=null;}
|
|
65
68
|
}
|
|
@@ -42,3 +42,15 @@ test('does not report success when client exits before callback is supplied', as
|
|
|
42
42
|
child.stdout.write(request + '\n'); child.emit('close',0);
|
|
43
43
|
await assert.rejects(running,/login-failed/);
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
test('reports bounded native handoff stages without child output', async () => {
|
|
47
|
+
const create = () => { const c = new EventEmitter(); c.stdin = new PassThrough(); c.stdout = new PassThrough(); c.stderr = new PassThrough(); c.kill = () => {}; return c; };
|
|
48
|
+
for (const phase of ['launch','request','waiting-approval','callback','client-exit']) {
|
|
49
|
+
const child = create();
|
|
50
|
+
const running = runHiddenLogin({executable:'claude',issuer,spawnImpl:()=>child,authorize:async()=>phase==='callback' ? 'http://bad.example/secret' : phase==='waiting-approval' ? new Promise(()=>{}) : 'http://127.0.0.1:1234/callback?code=ok&state=xyz'});
|
|
51
|
+
if (phase === 'launch') child.emit('error',new Error('secret-child-output'));
|
|
52
|
+
else if (phase === 'request') { child.stderr.write('secret-child-output\n'); child.emit('close',1); }
|
|
53
|
+
else { child.stdout.write(request+'\n'); if (phase==='waiting-approval') child.emit('close',1); if (phase==='client-exit') child.stdin.on('end',()=>child.emit('close',1)); if (phase==='client-exit') child.stdin.resume(); }
|
|
54
|
+
await assert.rejects(running,e=>e.nativeStage===phase && !JSON.stringify(e).includes('secret'));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -113,3 +113,9 @@ test('setup failures report only an allowlisted stage and never the underlying s
|
|
|
113
113
|
}),error=>error.message==='cohort-failed'&&error.setupDetail===stage&&!JSON.stringify(error).includes(secret));
|
|
114
114
|
}
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
test('enrollment diagnostics expose only allowed failure categories',async()=>{
|
|
118
|
+
for(const [message,expected] of [['killbar-status-unavailable','killbar-status-unavailable'],['killbar-enrollment-failed','killbar-enrollment-failed'],['token-secret-not-allowed',undefined]]){
|
|
119
|
+
await assert.rejects(runSetupFlow({clients:['codex'],issuer:'https://example.test',authenticate:async({sessionSink})=>sessionSink.save({accessToken:'private'}),killbarEnrollment:async()=>{throw new Error(message)},login:async({authorize})=>authorize({client:'codex',authorizationUrl:'request'}),fetchImpl:async()=>identityResponse()}),error=>error.setupDetail==='enrollment'&&error.enrollmentReason===expected&&!JSON.stringify(error).includes('token-secret-not-allowed'));
|
|
120
|
+
}
|
|
121
|
+
});
|