node-git-server logonode-git-server

Async Authentication

Real deployments rarely have hard-coded credentials — permissions live in a database. The whole request pipeline is async-aware, so you can await inside authenticate and even resolve the repository directory asynchronously.

Await inside the user callback

The callback passed to user() may be async. Do your lookup, then call next():

server.js
import { Git } from "@batchfy/node-git-server"
 
const repos = new Git("./repos", {
    authenticate: ({ type, repo, user }, next) => {
        user(async (username, password) => {
            try {
                await checkPermission({ username, password, repo, action: type })
                await next()
            } catch (error) {
                await next(error)
            }
        })
    },
})

checkPermission is your own function — query a users table, verify a password hash, and check whether the user may fetch or push this repository:

permissions.js
export async function checkPermission({ username, password, repo, action }) {
    const user = await db.user.findByName(username)
    if (!user || !(await verifyHash(password, user.passwordHash))) {
        throw new Error("invalid credentials")
    }
 
    const canWrite = await db.access.canWrite(user.id, repo)
    if (action === "push" && !canWrite) {
        throw new Error("write access denied")
    }
}

Because the check throws on failure, the catch turns it into next(error) — a clean 401 for the git client.

Async repository resolution

The first argument to new Git() may be a function that returns a string or a Promise<string>. Use it to resolve a repository path from a database — for example mapping a username/project slug to a project id on disk:

const repos = new Git(
    async (repo) => {
        const projectId = await db.project.resolveId(repo)
        return `/srv/git/${projectId}.git`
    },
    {
        autoCreate: false,
        authenticate: ({ type, repo, user }, next) => {
            user(async (username, password) => {
                try {
                    await checkPermission({ username, password, repo, action: type })
                    await next()
                } catch (error) {
                    await next(error)
                }
            })
        },
    }
)

The resolver is awaited everywhere the server needs a path, so a single async source of truth drives both routing and storage.

These Git methods are async and honour the resolver:

  • await repos.exists(repo) → Promise<boolean>
  • await repos.list() → Promise<string[]>
  • await repos.create(repo) → Promise<void>

See the Git API reference for details.