@pulsehub/runner 0.1.13 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulsehub/runner",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Local, credential-safe Pulse Runner",
5
5
  "repository": {
6
6
  "type": "git",
package/src/github-app.js CHANGED
@@ -17,6 +17,13 @@ export function bindingFor(config, projectId, repo) {
17
17
  if (bindings.length !== 1) throw new Error(`Configure one local GitHub App for project ${projectId}, repo ${repo}.`);
18
18
  return validateBinding(bindings[0]);
19
19
  }
20
+ export function parseAppKeyOutput(output, allowHex = false) {
21
+ let key = output.trim();
22
+ // macOS security renders passwords containing newlines as hexadecimal.
23
+ if (allowHex && /^(?:[0-9a-fA-F]{2})+$/.test(key)) key = Buffer.from(key, 'hex').toString('utf8').trim();
24
+ if (!/-----BEGIN (?:RSA )?PRIVATE KEY-----/.test(key)) throw new Error('Local GitHub App key is unavailable in the OS credential store.');
25
+ return key;
26
+ }
20
27
  export async function readAppKey(binding) {
21
28
  const service = `pulse-runner.github-app.${binding.appId}.${binding.keyId}`;
22
29
  try {
@@ -27,8 +34,8 @@ export async function readAppKey(binding) {
27
34
  : platform() === 'linux'
28
35
  ? await exec('secret-tool', ['lookup', 'service', service], { timeout: 10_000, maxBuffer: 32_768 })
29
36
  : null;
30
- if (!result || !/-----BEGIN (?:RSA )?PRIVATE KEY-----/.test(result.stdout)) throw new Error();
31
- return result.stdout.trim();
37
+ if (!result) throw new Error();
38
+ return parseAppKeyOutput(result.stdout, platform() === 'darwin');
32
39
  } catch { throw new Error('Local GitHub App key is unavailable in the OS credential store.'); }
33
40
  }
34
41
  export function appJwt(appId, privateKey, now = Date.now()) {
@@ -1,11 +1,25 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { generateKeyPairSync, verify } from 'node:crypto';
4
- import { appJwt, authorizeRepo, appGit, bindingFor, githubReadiness, githubApi } from '../src/github-app.js';
4
+ import { appJwt, authorizeRepo, appGit, bindingFor, githubReadiness, githubApi, parseAppKeyOutput } from '../src/github-app.js';
5
5
  const {privateKey, publicKey}=generateKeyPairSync('rsa',{modulusLength:2048});
6
6
  const pem=privateKey.export({type:'pkcs8',format:'pem'}).toString();
7
7
  const binding={projectId:'project-1',repo:'owner/repo',appId:'1',installationId:'2',slug:'local-app',keyId:'local-key'};
8
8
  const config={githubApps:[binding]};
9
+ test('macOS Keychain hexadecimal multiline output produces a verifiable RSA signature',()=>{
10
+ for (const type of ['pkcs1', 'pkcs8']) {
11
+ const original=privateKey.export({type,format:'pem'}).toString();
12
+ assert.equal(parseAppKeyOutput(original,true),original.trim());
13
+ const decoded=parseAppKeyOutput(`${Buffer.from(original).toString('hex')}\n`,true);
14
+ assert.equal(decoded,original.trim());
15
+ const [header,payload,signature]=appJwt('1',decoded).split('.');
16
+ assert(verify('RSA-SHA256',Buffer.from(`${header}.${payload}`),publicKey,Buffer.from(signature,'base64url')));
17
+ assert.throws(()=>parseAppKeyOutput(Buffer.from(original).toString('hex')));
18
+ }
19
+ for (const invalid of ['sentinel-secret','abc','00ff',Buffer.from('sentinel-secret').toString('hex')]) {
20
+ assert.throws(()=>parseAppKeyOutput(invalid,true),error=>!error.message.includes('sentinel'));
21
+ }
22
+ });
9
23
  function apiFixture(overrides={}) {
10
24
  const calls=[];
11
25
  const api=async(path,token,options)=>{