diff --git a/components/Application.ts b/components/Application.ts index 15d9c99d4..a6d044aaa 100644 --- a/components/Application.ts +++ b/components/Application.ts @@ -108,6 +108,21 @@ export function assertApplicationConfig( } } +/** + * Returns true when npm/git stderr indicates an SSH authentication failure — + * git exits 128 with the standard "could not read" message, or the SSH layer + * reports a missing uid (no SSH daemon user), explicit publickey denial, or + * an unverified host key. + */ +export function isSSHAuthFailure(stderr: string): boolean { + return ( + stderr.includes('Could not read from remote repository') || + stderr.includes('Permission denied (publickey)') || + stderr.includes('No user exists for uid') || + stderr.includes('Host key verification failed') + ); +} + /** * Extract an application given payload (content of the application) or package (npm-compatible identifier to the application). * @@ -189,6 +204,12 @@ export async function extractApplication(application: Application) { parentDirPath ); if (code !== 0) { + if (isSSHAuthFailure(stderr)) { + throw new Error( + `Failed to deploy private repository ${application.packageIdentifier}: SSH access failed. Verify the repository URL, configure an SSH key on this Harper instance, ensure the key has access to the target repository, and confirm the host is present in the ssh/known_hosts file.`, + { cause: new Error(stderr) } + ); + } throw new Error(`Failed to download package ${application.packageIdentifier}: ${stderr}`); } diff --git a/unitTests/components/Application.test.js b/unitTests/components/Application.test.js new file mode 100644 index 000000000..fdc855d62 --- /dev/null +++ b/unitTests/components/Application.test.js @@ -0,0 +1,67 @@ +'use strict'; + +const assert = require('node:assert'); + +const testUtils = require('../testUtils.js'); +testUtils.preTestPrep(); + +const { isSSHAuthFailure } = require('#src/components/Application'); + +describe('isSSHAuthFailure', () => { + it('returns true for "Could not read from remote repository"', () => { + const stderr = ` +npm error code 128 +npm error An unknown git error occurred +npm error command git --no-replace-objects ls-remote git@github.com:Org/repo.git +npm error fatal: Could not read from remote repository. +npm error Please make sure you have the correct access rights +npm error and the repository exists. +`; + assert.strictEqual(isSSHAuthFailure(stderr), true); + }); + + it('returns true for "Permission denied (publickey)"', () => { + const stderr = ` +npm error code 128 +npm error An unknown git error occurred +npm error git@github.com: Permission denied (publickey). +npm error fatal: Could not read from remote repository. +`; + assert.strictEqual(isSSHAuthFailure(stderr), true); + }); + + it('returns true for "No user exists for uid"', () => { + const stderr = ` +npm error code 128 +npm error An unknown git error occurred +npm error No user exists for uid 42932 +npm error fatal: Could not read from remote repository. +npm error Please make sure you have the correct access rights +npm error and the repository exists. +`; + assert.strictEqual(isSSHAuthFailure(stderr), true); + }); + + it('returns true for "Host key verification failed"', () => { + const stderr = ` +npm error code 128 +npm error An unknown git error occurred +npm error Host key verification failed. +npm error fatal: Could not read from remote repository. +`; + assert.strictEqual(isSSHAuthFailure(stderr), true); + }); + + it('returns false for unrelated npm errors', () => { + const stderr = ` +npm error code E404 +npm error 404 Not Found - GET https://registry.npmjs.org/nonexistent-pkg +npm error 404 '@scope/nonexistent-pkg@latest' is not in this registry. +`; + assert.strictEqual(isSSHAuthFailure(stderr), false); + }); + + it('returns false for empty stderr', () => { + assert.strictEqual(isSSHAuthFailure(''), false); + }); +});