/**
* resolvePusher: map a push's credentials (session cookie/bearer, or a PAT in
* HTTP Basic) to the canonical userId recorded in the push-index.
*
* REGRESSION: the PAT branch read `.userId` straight off resolveToken's return,
* but resolveToken resolves to { user, scope, identityId } — not a User. That
* yielded undefined, resolvePusher returned null, and recordPush was skipped
* for EVERY token-authenticated push. Pushes succeeded while the push-index
* stayed empty and profiles showed "Repos pushed (0)", so HTTPS push activity
* was invisible on the daemon. Caught in production 2026-07-29 only by reading
* the upstream Apache access log.
*/
import test from 'brittle'
import b4a from 'b4a'
import { makeResolvePusher } from '../lib/handlers/repos.js'
const TOKEN = 'pgv3_' + 'a'.repeat(48)
// Minimal registry stub shaped like the real IdentityRegistry surface that
// resolveToken + getUserByIdentity touch.
function makeRegistry ({ tokenUserId = null, sessionUserId = null } = {}) {
const tokenIdentityId = 'tok:' + hashOf(TOKEN)
return {
async getIdentity (id) {
if (id === tokenIdentityId && tokenUserId) {
return { identityId: id, method: 'access-token', credentials: { scope: 'write' } }
}
return null
},
async isRevoked () { return false },
async getUserByIdentity (id) {
if (id === tokenIdentityId && tokenUserId) return { userId: tokenUserId }
if (sessionUserId && id === 'sessionpub') return { userId: sessionUserId }
return null
}
}
}
// resolveToken hashes the secret with hypercore-crypto; mirror that so the stub
// registry answers on the same identityId the resolver will look up.
function hashOf (secret) {
// eslint-disable-next-line no-undef
const crypto = globalThis.__pgv3TestCrypto
return b4a.toString(crypto.hash(b4a.from(secret, 'utf8')), 'hex')
}
function basicReq (secret) {
return {
headers: {
authorization: 'Basic ' + b4a.toString(b4a.from('git:' + secret, 'utf8'), 'base64')
}
}
}
test('resolvePusher: PAT in HTTP Basic resolves to the owning userId', async (t) => {
globalThis.__pgv3TestCrypto = (await import('hypercore-crypto')).default
const svc = {
sessions: null,
identityRegistry: makeRegistry({ tokenUserId: 'user-abc' })
}
const resolvePusher = makeResolvePusher(svc)
const userId = await resolvePusher(basicReq(TOKEN))
t.is(userId, 'user-abc', 'PAT push is attributed to the token owner')
})
test('resolvePusher: browser session still resolves (the branch that worked)', async (t) => {
globalThis.__pgv3TestCrypto = (await import('hypercore-crypto')).default
const svc = {
// SessionStore contract: extractIdentity does sessions.get(token).
sessions: { get: (tok) => (tok === 'sess-token' ? { publicKey: 'sessionpub' } : null) },
identityRegistry: makeRegistry({ sessionUserId: 'user-session' })
}
const resolvePusher = makeResolvePusher(svc)
const req = { headers: { authorization: 'Bearer sess-token' } }
t.is(await resolvePusher(req), 'user-session', 'session push attributed to its user')
})
test('resolvePusher: unknown / absent credentials stay unattributed', async (t) => {
globalThis.__pgv3TestCrypto = (await import('hypercore-crypto')).default
const svc = { sessions: null, identityRegistry: makeRegistry({}) }
const resolvePusher = makeResolvePusher(svc)
t.is(await resolvePusher(basicReq(TOKEN)), null, 'unknown token → null')
t.is(await resolvePusher({ headers: {} }), null, 'no credentials → null')
})