/**
* Auto-import local SSH public keys into a hyper-identity registry,
* linked to the daemon owner's user account.
*
* Rationale: when a user installs pear-git (or any hyper-identity
* consumer) on their own Linux box, they typically already have
* `~/.ssh/id_*.pub` files. Making them manually re-register those keys
* is friction. This helper scans the running user's `~/.ssh/`, parses
* candidate pubkeys, and registers each as a `method: 'ssh-key'`
* identity linked to the owner's userId.
*
* Safety rules (all must hold before anything gets registered):
* 1. Process not running as root (unless opts.allowRoot).
* 2. `$HOME` is set and `.ssh/` is readable.
* 3. Registry has an explicit ownerUserId (caller resolves "who is
* the device owner"). We do not infer owner from the registry.
* 4. Owner must have at least one non-ssh-key identity already (proves
* the account exists and the caller knows what they're doing).
*
* The operation is idempotent: keys already registered by fingerprint
* are skipped, so re-running on every daemon boot is safe.
*
* Returns a summary { scanned, imported, skipped } for logging.
*/
import { scanHomeSSHKeys } from './ssh-scan.js'
/**
* Auto-import local SSH pubkeys to a registry as ssh-key identities
* linked to the given owner.
*
* @param {Object} registry - hyper-identity IdentityRegistry
* @param {string} ownerUserId - userId to link imported identities to
* @param {Object} [opts]
* @param {boolean} [opts.allowRoot=false] - allow running as uid 0
* @param {boolean} [opts.requireExistingIdentity=true] - owner must
* already have at least one non-ssh-key identity (safety check)
* @returns {Promise<{scanned, imported, skipped, skippedReasons}>}
*/
export async function autoImportLocalSSHKeys (registry, ownerUserId, opts = {}) {
const result = { scanned: 0, imported: 0, skipped: 0, skippedReasons: {} }
const requireExistingIdentity = opts.requireExistingIdentity !== false
if (!registry) throw new Error('autoImportLocalSSHKeys requires a registry')
if (!ownerUserId) throw new Error('autoImportLocalSSHKeys requires ownerUserId')
// Verify owner actually exists AND has at least one non-ssh identity
// (so we don't auto-create perms for a userId that never consented).
const user = await registry.getUser(ownerUserId)
if (!user) {
result.skippedReasons.noSuchUser = true
return result
}
if (requireExistingIdentity) {
const identities = user.identities || []
let hasNonSshIdentity = false
for (const idRef of identities) {
// identities[] may be either identityId strings or full records
const idStr = typeof idRef === 'string' ? idRef : (idRef && idRef.identityId)
if (!idStr) continue
const rec = await registry.getIdentity(idStr).catch(() => null)
if (rec && rec.method && rec.method !== 'ssh-key') {
hasNonSshIdentity = true
break
}
}
if (!hasNonSshIdentity) {
result.skippedReasons.noExistingIdentity = true
return result
}
}
const keys = await scanHomeSSHKeys({ allowRoot: opts.allowRoot })
result.scanned = keys.length
for (const k of keys) {
const existing = await registry.getIdentityBySSHKey(k.fingerprint).catch(() => null)
if (existing) {
result.skipped++
continue
}
const identityId = `ssh:${k.fingerprint}`
try {
await registry.registerIdentity({
identityId,
method: 'ssh-key',
publicKey: k.publicKey,
credentials: { fingerprint: k.fingerprint, type: k.type, comment: k.comment },
keyType: k.type,
source: 'auto-import',
autoImportedFrom: k.path,
createdAt: Date.now()
})
// Link to owner — `verified: true` is legitimate here because
// the caller of auto-import has physical-filesystem access to
// the user's $HOME, which is the strongest possible assertion
// of ownership for a locally-present key.
await registry.linkIdentityToUser(identityId, ownerUserId, { verified: true })
if (typeof registry.addIdentityToUser === 'function') {
await registry.addIdentityToUser(ownerUserId, identityId)
}
result.imported++
} catch (err) {
result.skipped++
result.skippedReasons[k.path] = err.message || String(err)
}
}
return result
}