#!/usr/bin/env node /** * Copy bare-ssh2 sources into packages/bare-os-openssh/vendor/bare-ssh2 (no node_modules). * * Env: BARE_OS_BARE_SSH2_SRC — default ~/dev/pearcli/holepunch-repos/bare-ssh2 */ import { execFileSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import { applyBareSsh2PearCjsFixes } from './lib/apply-bare-ssh2-pear-cjs-fixes.mjs' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const dest = path.join(root, 'packages', 'bare-os-openssh', 'vendor', 'bare-ssh2') const defaultSrc = path.join( process.env.HOME || '', 'dev/pearcli/holepunch-repos/bare-ssh2' ) const src = String(process.env.BARE_OS_BARE_SSH2_SRC || defaultSrc).trim() if (!fs.existsSync(path.join(src, 'package.json'))) { console.error('vendor-bare-ssh2: missing source:', src) process.exit(1) } fs.mkdirSync(path.dirname(dest), { recursive: true }) execFileSync( 'rsync', [ '-a', '--delete', '--exclude', 'node_modules', '--exclude', '.git', path.join(src, '/'), dest + path.sep ], { stdio: 'inherit' } ) applyBareOsCryptoShimPatch(path.join(dest, 'shims', 'crypto')) applyBareOsBareSsh2SaferBufferDep(path.join(dest, 'package.json')) applyBareOsBareSsh2TweetnaclDep(path.join(dest, 'package.json')) applyBareOsZlibPatch(path.join(dest, 'lib', 'protocol', 'zlib.js')) applyBareOsUtilsBufferSlicePatch(path.join(dest, 'lib', 'protocol', 'utils.js')) applyBareOsConstantsEddsaPatch(path.join(dest, 'lib', 'protocol', 'constants.js')) applyBareOsKexCurve25519CopyPatch(path.join(dest, 'lib', 'protocol', 'kex.js')) applyBareOsKexBareBufferCopyPatch(path.join(dest, 'lib', 'protocol', 'kex.js')) applyBareOsKeygenPatch(path.join(dest, 'lib', 'keygen.js')) applyBareOsChannelStreamFlagsPatch(path.join(dest, 'lib', 'Channel.js')) applyBareSsh2PearCjsFixes(dest) console.log('vendor-bare-ssh2: synced →', dest) /** Re-apply Bare/Pear fix after rsync (upstream uses createInflate()._handle.constructor at load time). */ function applyBareOsZlibPatch(zlibPath) { if (!fs.existsSync(zlibPath)) return let s = fs.readFileSync(zlibPath, 'utf8') if (s.includes('getZlibHandleCtor')) return const old = "} = require('zlib')\nconst ZlibHandle = createInflate()._handle.constructor\n\nfunction processCallback()" if (!s.includes(old)) { console.warn( 'vendor-bare-ssh2: zlib.js layout changed; re-apply Bare lazy zlib init manually' ) return } const neu = "} = require('zlib')\n\n" + "/**\n" + " * Node's `zlib` exposes `createInflate()._handle.constructor` for the sync\n" + " * binding. Bare's `bare-node-zlib` / `bare-zlib` often has no `_handle`, so\n" + " * reading `.constructor` at module load throws. Resolve lazily; callers that\n" + " * need zlib compression get a clear error if unavailable.\n" + " */\n" + 'let ZlibHandleCtor\n' + 'function getZlibHandleCtor() {\n' + ' if (ZlibHandleCtor !== undefined) return ZlibHandleCtor\n' + ' try {\n' + ' const inf = createInflate()\n' + ' ZlibHandleCtor =\n' + ' inf &&\n' + ' inf._handle &&\n' + ' typeof inf._handle.constructor === \'function\'\n' + ' ? inf._handle.constructor\n' + ' : null\n' + ' } catch {\n' + ' ZlibHandleCtor = null\n' + ' }\n' + ' return ZlibHandleCtor\n' + '}\n\n' + 'function processCallback()' s = s.replace(old, neu) s = s.replace( ' this._handle = new ZlibHandle(mode)', ` const Handle = getZlibHandleCtor() if (!Handle) { throw new Error( 'bare-ssh2 zlib: native zlib _handle unavailable (use compression "none" only on Bare/Pear)' ) } this._handle = new Handle(mode)` ) fs.writeFileSync(zlibPath, s) console.log('vendor-bare-ssh2: applied lib/protocol/zlib.js Bare patch') } /** * bare-crypto has `generateKeyPair('ed25519')` but not Node's `generateKeyPairSync`; * bare-ssh2 keygen expects SPKI/PKCS#8 DER buffers. Re-apply after rsync. */ function applyBareOsCryptoShimPatch(cryptoShimDir) { const indexSrc = path.join(root, 'scripts', 'patches', 'bare-ssh2-shims-crypto-index.js') const x25519Src = path.join(root, 'scripts', 'patches', 'bare-ssh2-shims-crypto-x25519-compat.js') const pemEd25519Src = path.join(root, 'scripts', 'patches', 'bare-ssh2-shims-crypto-pem-ed25519.js') if (!fs.existsSync(indexSrc)) { console.warn('vendor-bare-ssh2: missing crypto shim patch:', indexSrc) return } fs.mkdirSync(cryptoShimDir, { recursive: true }) fs.copyFileSync(indexSrc, path.join(cryptoShimDir, 'index.js')) if (fs.existsSync(x25519Src)) { fs.copyFileSync(x25519Src, path.join(cryptoShimDir, 'x25519-compat.js')) } else { console.warn('vendor-bare-ssh2: missing x25519 shim:', x25519Src) } if (fs.existsSync(pemEd25519Src)) { fs.copyFileSync(pemEd25519Src, path.join(cryptoShimDir, 'pem-ed25519.js')) } else { console.warn('vendor-bare-ssh2: missing pem-ed25519 shim:', pemEd25519Src) } console.log('vendor-bare-ssh2: applied shims/crypto Bare patches') } /** Crypto shim requires `safer-buffer` for buffers asn1's BER writer accepts. */ function applyBareOsBareSsh2SaferBufferDep(packageJsonPath) { if (!fs.existsSync(packageJsonPath)) return const j = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) const deps = j.dependencies if (!deps || deps['safer-buffer']) return j.dependencies = { ...deps, 'safer-buffer': '~2.1.0' } fs.writeFileSync(packageJsonPath, `${JSON.stringify(j, null, 2)}\n`) console.log('vendor-bare-ssh2: added dependencies.safer-buffer for crypto shim') } /** X25519 / Curve25519 SSH kex (tweetnacl); bare-crypto has no `diffieHellman` / `createPublicKey`. */ function applyBareOsBareSsh2TweetnaclDep(packageJsonPath) { if (!fs.existsSync(packageJsonPath)) return const j = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) const deps = j.dependencies if (!deps || deps.tweetnacl) return j.dependencies = { ...deps, tweetnacl: '^1.0.3' } fs.writeFileSync(packageJsonPath, `${JSON.stringify(j, null, 2)}\n`) console.log('vendor-bare-ssh2: added dependencies.tweetnacl for X25519 kex') } /** * `bufferSlice` used `Buffer[Symbol.species]`; on Bare those views break asn1 `writeBuffer` * ("argument must be a buffer"). Copy through `safer-buffer` instead. */ function applyBareOsUtilsBufferSlicePatch(utilsPath) { if (!fs.existsSync(utilsPath)) return let s = fs.readFileSync(utilsPath, 'utf8') if (s.includes('Asn1SafeBuffer')) return const oldRequire = "const Ber = require('asn1').Ber\n\nlet DISCONNECT_REASON" if (!s.includes(oldRequire)) { console.warn( 'vendor-bare-ssh2: utils.js header changed; re-apply bufferSlice Bare patch manually' ) return } s = s.replace( oldRequire, "const Ber = require('asn1').Ber\n" + "const { Buffer: Asn1SafeBuffer } = require('safer-buffer')\n\n" + 'let DISCONNECT_REASON' ) const oldSlice = 'function bufferSlice(buf, start, end) {\n' + ' if (end === undefined) end = buf.length\n' + ' return new FastBuffer(buf.buffer, buf.byteOffset + start, end - start)\n' + '}' if (!s.includes(oldSlice)) { console.warn( 'vendor-bare-ssh2: utils.js bufferSlice changed; re-apply Bare patch manually' ) return } const neuSlice = 'function bufferSlice(buf, start, end) {\n' + ' if (end === undefined) end = buf.length\n' + ' if (start < 0) start = 0\n' + ' if (end > buf.length) end = buf.length\n' + ' const len = end - start\n' + ' // Bare: `Buffer[Symbol.species]` slices are often TypedArray views that fail\n' + ' // `safer-buffer` `Buffer.isBuffer` inside asn1 `writeBuffer` ("argument must be a buffer").\n' + ' const u8 =\n' + " buf.buffer !== undefined && typeof buf.byteOffset === 'number'\n" + ' ? new Uint8Array(buf.buffer, buf.byteOffset + start, len)\n' + ' : new Uint8Array(buf).subarray(start, end)\n' + ' return Asn1SafeBuffer.from(u8)\n' + '}' fs.writeFileSync(utilsPath, s.replace(oldSlice, neuSlice)) console.log('vendor-bare-ssh2: applied lib/protocol/utils.js bufferSlice Bare patch') } /** * Node probes Ed25519 via PEM + sign/verify; Bare `bare-crypto` needs Key objects * from generateKeyPair or eddsaSupported stays false and OpenSSH ed25519 keys fail to parse. */ function applyBareOsConstantsEddsaPatch(constantsPath) { if (!fs.existsSync(constantsPath)) return let s = fs.readFileSync(constantsPath, 'utf8') if (s.includes('PEM-based probe fails')) return const old = "const eddsaSupported = (() => {\n" + " if (typeof crypto.sign === 'function' && typeof crypto.verify === 'function') {\n" + " const key =\n" + " '-----BEGIN PRIVATE KEY-----\\r\\nMC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD' +\n" + " '/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\\r\\n-----END PRIVATE KEY-----'\n" + ' const data = Buffer.from(\'a\')\n' + ' let sig\n' + ' let verified\n' + ' try {\n' + ' sig = crypto.sign(null, data, key)\n' + ' verified = crypto.verify(null, data, key, sig)\n' + ' } catch {}\n' + ' return Buffer.isBuffer(sig) && sig.length === 64 && verified === true\n' + ' }\n' + '\n' + ' return false\n' + '})()' if (!s.includes(old)) { console.warn( 'vendor-bare-ssh2: constants.js eddsaSupported block changed; re-apply Bare Ed25519 probe manually' ) return } const neu = "const eddsaSupported = (() => {\n" + " if (typeof crypto.sign === 'function' && typeof crypto.verify === 'function') {\n" + " const key =\n" + " '-----BEGIN PRIVATE KEY-----\\r\\nMC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD' +\n" + " '/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\\r\\n-----END PRIVATE KEY-----'\n" + ' const data = Buffer.from(\'a\')\n' + ' let sig\n' + ' let verified\n' + ' try {\n' + ' sig = crypto.sign(null, data, key)\n' + ' verified = crypto.verify(null, data, key, sig)\n' + ' } catch {}\n' + ' if (Buffer.isBuffer(sig) && sig.length === 64 && verified === true) return true\n' + ' }\n' + '\n' + ' // Bare `bare-crypto`: PEM-based probe fails; sign/verify require Key objects.\n' + ' if (\n' + " typeof crypto.generateKeyPair === 'function' &&\n" + " typeof crypto.sign === 'function' &&\n" + " typeof crypto.verify === 'function'\n" + ' ) {\n' + ' try {\n' + " const { publicKey, privateKey } = crypto.generateKeyPair('ed25519')\n" + " const data = Buffer.from('a')\n" + ' const sig = crypto.sign(null, data, privateKey)\n' + ' const verified = crypto.verify(null, data, publicKey, sig)\n' + ' const sigLen = sig && (sig.length ?? sig.byteLength)\n' + ' const sigOk =\n' + ' verified === true &&\n' + " typeof sigLen === 'number' &&\n" + ' sigLen === 64 &&\n' + ' (Buffer.isBuffer(sig) || ArrayBuffer.isView(sig))\n' + ' if (sigOk) return true\n' + ' } catch {}\n' + ' }\n' + '\n' + ' return false\n' + '})()' fs.writeFileSync(constantsPath, s.replace(old, neu)) console.log('vendor-bare-ssh2: applied lib/protocol/constants.js Bare eddsaSupported patch') } /** * `otherPublicKey.copy(asnWriter._buf, …)` throws: asn1 uses `safer-buffer`, Bare's * `Buffer#copy` requires a Feross/bare-node `Buffer` target ("argument should be a Buffer"). */ function applyBareOsKexCurve25519CopyPatch(kexPath) { if (!fs.existsSync(kexPath)) return let s = fs.readFileSync(kexPath, 'utf8') if (s.includes('_ab[_ao + i]')) return const old = ' asnWriter._ensure(otherPublicKey.length)\n' + ' otherPublicKey.copy(asnWriter._buf, asnWriter._offset, 0, otherPublicKey.length)\n' + ' asnWriter._offset += otherPublicKey.length' if (!s.includes(old)) { console.warn( 'vendor-bare-ssh2: kex.js Curve25519 copy block changed; re-apply Bare patch manually' ) return } const neu = ' asnWriter._ensure(otherPublicKey.length)\n' + ' {\n' + ' const _ab = asnWriter._buf\n' + ' const _ao = asnWriter._offset\n' + ' for (let i = 0; i < otherPublicKey.length; ++i) {\n' + ' _ab[_ao + i] = otherPublicKey[i]\n' + ' }\n' + ' }\n' + ' asnWriter._offset += otherPublicKey.length' fs.writeFileSync(kexPath, s.replace(old, neu)) console.log('vendor-bare-ssh2: applied lib/protocol/kex.js Curve25519 BER copy Bare patch') } /** * Replace Buffer#copy in `convertToMpint` / `convertPublicKey`: source may be safer-buffer, * target from `Buffer.allocUnsafe` is bare-node-buffer → "argument should be a Buffer". */ function applyBareOsKexBareBufferCopyPatch(kexPath) { if (!fs.existsSync(kexPath)) return let s = fs.readFileSync(kexPath, 'utf8') if (s.includes('function copyBytes(src, dst')) return const oldConvert = ' function convertToMpint(buf) {\n' + ' let idx = 0\n' + ' let length = buf.length\n' + ' while (buf[idx] === 0x00) {\n' + ' ++idx\n' + ' --length\n' + ' }\n' + ' let newBuf\n' + ' if (buf[idx] & 0x80) {\n' + ' newBuf = Buffer.allocUnsafe(1 + length)\n' + ' newBuf[0] = 0\n' + ' buf.copy(newBuf, 1, idx)\n' + ' buf = newBuf\n' + ' } else if (length !== buf.length) {\n' + ' newBuf = Buffer.allocUnsafe(length)\n' + ' buf.copy(newBuf, 0, idx)\n' + ' buf = newBuf\n' + ' }\n' + ' return buf\n' + ' }' if (!s.includes(oldConvert)) { console.warn( 'vendor-bare-ssh2: kex.js convertToMpint unchanged or already patched; skip Bare buffer copy patch' ) return } const newConvert = ' /** Avoid Buffer#copy across safer-buffer vs bare-node-buffer (throws "argument should be a Buffer"). */\n' + ' function copyBytes(src, dst, dstOff, srcStart, srcEnd) {\n' + ' for (let i = srcStart, j = dstOff; i < srcEnd; ++i, ++j) dst[j] = src[i]\n' + ' }\n' + '\n' + ' function convertToMpint(buf) {\n' + ' let idx = 0\n' + ' let length = buf.length\n' + ' while (buf[idx] === 0x00) {\n' + ' ++idx\n' + ' --length\n' + ' }\n' + ' let newBuf\n' + ' if (buf[idx] & 0x80) {\n' + ' newBuf = Buffer.allocUnsafe(1 + length)\n' + ' newBuf[0] = 0\n' + ' copyBytes(buf, newBuf, 1, idx, idx + length)\n' + ' buf = newBuf\n' + ' } else if (length !== buf.length) {\n' + ' newBuf = Buffer.allocUnsafe(length)\n' + ' copyBytes(buf, newBuf, 0, idx, buf.length)\n' + ' buf = newBuf\n' + ' }\n' + ' return buf\n' + ' }' s = s.replace(oldConvert, newConvert) s = s.replace( /key\.copy\(newKey, 1, idx\)/g, 'copyBytes(key, newKey, 1, idx, key.length)' ) s = s.replace( /key\.copy\(newKey, 0, idx\)/g, 'copyBytes(key, newKey, 0, idx, key.length)' ) fs.writeFileSync(kexPath, s) console.log('vendor-bare-ssh2: applied lib/protocol/kex.js Bare buffer copy patch') } /** Bare crypto may omit `getCurves` (keygen.js called it at module load). */ function applyBareOsKeygenPatch(keygenPath) { if (!fs.existsSync(keygenPath)) return let s = fs.readFileSync(keygenPath, 'utf8') if (s.includes('typeof getCurves ===')) return const old = 'const curves = getCurves()' if (!s.includes(old)) { console.warn( 'vendor-bare-ssh2: keygen.js layout changed; re-apply getCurves fallback manually' ) return } s = s.replace( old, "/** Bare `bare-crypto` / shims may omit `getCurves` (only needed for ECDSA keygen). */\n" + 'const curves =\n' + ' typeof getCurves === \'function\'\n' + ' ? getCurves()\n' + " : ['prime256v1', 'secp384r1', 'secp521r1']" ) fs.writeFileSync(keygenPath, s) console.log('vendor-bare-ssh2: applied lib/keygen.js Bare patch') } /** * streamx (bare-node-stream) exposes `readable` / `writable` as getter-only; ssh2 sets them on end/finish. */ function applyBareOsChannelStreamFlagsPatch(channelPath) { if (!fs.existsSync(channelPath)) return let s = fs.readFileSync(channelPath, 'utf8') if (s.includes('bare-node-stream / streamx: readable')) return const old = 'function onFinish() {\n' + ' this.eof()\n' + ' if (this.server || !this.allowHalfOpen) this.close()\n' + ' this.writable = false\n' + '}\n' + '\n' + 'function onEnd() {\n' + ' this.readable = false\n' + '}' if (!s.includes(old)) { console.warn( 'vendor-bare-ssh2: lib/Channel.js onFinish/onEnd changed; re-apply stream-flags patch manually' ) return } const neu = 'function onFinish() {\n' + ' this.eof()\n' + ' if (this.server || !this.allowHalfOpen) this.close()\n' + ' try {\n' + ' this.writable = false\n' + ' } catch (_) {\n' + ' /* bare-node-stream / streamx: writable may be getter-only */\n' + ' }\n' + '}\n' + '\n' + 'function onEnd() {\n' + ' try {\n' + ' this.readable = false\n' + ' } catch (_) {\n' + ' /* bare-node-stream / streamx: readable is getter-only */\n' + ' }\n' + '}' fs.writeFileSync(channelPath, s.replace(old, neu)) console.log('vendor-bare-ssh2: applied lib/Channel.js streamx readable/writable patch') }