Last week was all about schemas, fixtures, and design work. This week, the first real production code went into the Hiero codebase. Two parallel tracks running at the same time: a full NestJS GPG challenge module inside heka-identity-service, and the initial skeleton of the GitHub App that will eventually gate pull requests.
Let me walk through both.
What This Week Was About
The plan for Week 3 had two parallel tracks:
Track A — Inside heka-identity-service:
- Port the GPG challenge-response flow from the MVP prototype into a proper NestJS module
- Cryptographically secure nonces, 5-minute TTL, single-use enforcement
- Full error handling for every failure scenario
- Unit and integration tests covering all paths
Track B — In the separate GitHub App repo:
- Build the initial Probot adapter skeleton
- Webhook signature validation and GitHub App authentication
- Async acknowledgment pattern so GitHub doesn't time out waiting for us
- Idempotent check run creation
Let's go through both in detail.
Track A: The GPG Challenge Module
What is a GPG Challenge-Response, and why does it matter?
Before I explain what I built, let me quickly explain what problem this solves.
The whole point of this project is to replace "GitHub says this person is who they claim to be" with "this person can cryptographically prove it." The way we do that is through a challenge-response flow:
- The contributor asks Heka for a challenge (a random string called a nonce)
- Heka generates a nonce and ties it to their GitHub username
- The contributor signs that nonce using their GPG private key (which never leaves their machine)
- They send the signature back to Heka
- Heka fetches the contributor's public GPG key directly from
github.com/:username.gpgand verifies the signature mathematically
If verification passes, it proves two things simultaneously: the person controls the GPG private key, and that private key is registered to their GitHub account. You can't fake either of those without physically having the key.
This is the same mechanism used by SSH authentication, code signing, and certificate issuance — it's a well-understood cryptographic primitive.
The NestJS Module Structure
The flow now lives inside heka-identity-service/src/gpg-challenge/ as a self-contained NestJS module:
src/gpg-challenge/
gpg-challenge.module.ts ← Module boundary
gpg-challenge.entity.ts ← Database schema for challenge sessions
gpg-challenge.service.ts ← All the business logic
gpg-challenge.controller.ts ← HTTP surface with Swagger docs
dto/ ← Typed, validated request bodies
The module is imported in app.module.ts and registered in the ORM config for migrations, so it's a proper first-class citizen of the Heka codebase — not a bolted-on addon.
Generating Secure Nonces
const nonce = crypto.randomBytes(32).toString('hex'); // 256 bits of entropy
This uses Node's built-in crypto module, which pulls randomness from the OS-level CSPRNG (Cryptographically Secure Pseudo-Random Number Generator). The result is 64 hexadecimal characters — genuinely unpredictable, impossible to brute-force.
The nonce is stored alongside the contributor's githubUsername, so it's cryptographically bound to a specific account. You can't take a nonce issued to one user and sign it as a different user.
The 5-Minute TTL and Single-Use Enforcement
const expiresAt = new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + 5);
The TTL is enforced server-side at verification time — not by trusting the client's clock.
The more interesting part is burn-before-verify: the challenge is marked as consumed in the database before any network or cryptographic work happens:
challenge.consumed = true;
await this.challengeRepo.getEntityManager().flush(); // Persisted BEFORE anything else
Why does the order matter? If we verified first and then marked consumed, and the process crashed between those two steps, an attacker who intercepted the signed payload could replay it — the DB would still say "not consumed." By burning the nonce first, even a mid-verification crash leaves the nonce permanently invalidated. This is the correct implementation of replay attack prevention.
Error Handling: Every Failure Path Covered
One thing I'm particularly happy with this week is how comprehensive the error handling is. There are 10 distinct failure scenarios, each with its own HTTP status code and a human-readable message:
| Scenario | HTTP Status |
|---|---|
| Unknown challenge ID | 404 |
| Replay attempt (already consumed) | 400 |
| Expired challenge | 400 |
| GitHub 404 (no GPG key on profile) | 400 |
| GitHub rate limit (403/429) | 429 |
| GitHub unreachable (network failure) | 503 |
| Empty GPG key body | 400 |
| Unparseable public key | 400 |
| Unparseable armored message | 400 |
| Signature math fails | 200 { verified: false }
|
That last one is intentional — a failed signature isn't a server error, it's a valid "no" from the verification system.
The Test Suite
Unit tests (16 tests):
- Every failure path above is tested in isolation
- The burn-before-verify ordering is verified — the test confirms that
flush()is called before the GitHub API mock fires - No real database or network required — uses deep mocks throughout
Integration/E2E tests (9 tests):
- Runs against a real PostgreSQL instance with the full NestJS HTTP stack
- Covers all 3 endpoints, DTO validation, replay via double-submit, and expired record insertion
Combined: 25 tests, all passing.
Track B: The GitHub App Skeleton
What the GitHub App Actually Does
The GitHub App sits in a separate repository (as decided with my mentor — GitHub integration code stays separate from heka-identity-service). Its job, at the end of the mentorship, will be to:
- Receive a webhook when someone opens a PR
- Ask Heka whether that contributor is verified
- Post a passing or failing check run to the PR
This week's scope was just the skeleton — the infrastructure that makes the above possible. The actual Heka verification call comes in a later week.
Why Probot?
Probot is a Node.js framework specifically designed for building GitHub Apps. It handles two things that would otherwise be painful to implement manually:
Webhook signature validation: Every incoming webhook request from GitHub is signed using HMAC-SHA256. Probot validates that signature before any application code runs. If it's invalid, Probot returns 400 and nothing fires. This is a security guarantee I get for free.
GitHub App authentication: GitHub Apps authenticate using JWT tokens derived from a private key, and those tokens expire every 10 minutes. Probot manages the JWT lifecycle automatically — I never touch it directly.
The Async Acknowledgment Pattern
GitHub has a hard rule: your webhook endpoint must respond with HTTP 200 within 10 seconds, or GitHub marks the delivery as failed and schedules a retry.
The problem is that checking Heka, creating a check run, and resolving a DID can easily take more than 10 seconds under real network conditions. If we handle everything synchronously in the webhook handler, we'll be timing out constantly.
Probot solves this cleanly: it responds 200 to GitHub immediately on receipt, and fires the application listeners asynchronously through its internal event emitter. The listeners are not in the response path.
// Probot guarantees:
// - HTTP 200 returned to GitHub immediately on receipt (before async
// listeners complete), satisfying the 10-second delivery timeout
So GitHub gets its 200, and our actual logic runs in the background.
Idempotent Check Runs
This one is important. GitHub can and does re-deliver webhook events — if your server was temporarily unreachable, GitHub will retry. Without idempotency, you'd end up with multiple check runs for the same commit, cluttering the PR UI.
The fix: before creating a new check run, we check whether one already exists for the exact commit SHA and check name. If a non-queued run already exists, we return early:
const existing = await context.octokit.checks.listForRef({
...context.repo(),
ref: sha,
check_name: CHECK_NAME,
});
The key is using the SHA (not the PR number) as the identifier. A new commit push to the same PR produces a new SHA, which correctly triggers a new check run. A retry of the same webhook event produces the same SHA, which hits the idempotency guard and does nothing.
One Honest Gap
The GitHub App code handles all the right events, the idempotency logic is correct, and the tests pass — but I haven't yet done a live end-to-end test where I actually open a PR in the webhook fixture repository and watch the check run appear in the GitHub UI.
That's the one remaining evidence item not yet checked off for this week. It'll be verified and documented before Week 4 work begins.
What Surprised Me This Week
The burn-before-verify ordering. I initially wrote the code to verify first and mark consumed after. The issue with that is subtle — it feels more "logical" to verify before committing anything, but it creates a tiny window where a replayed nonce could succeed if the process crashes between verification and the consumed flag being set. Flipping the order eliminates that window entirely. Small detail, but the kind of thing that separates a working prototype from a production system.
The Probot architecture. I expected to have to wire up the async response pattern manually, but Probot's design means I get it for free. Understanding why it works that way (the event emitter is separate from the HTTP response path) made it click.
Looking Ahead
Week 4 is GitHub OAuth and contributor binding — implementing the actual GitHub login flow in Heka and persisting the verified contributor binding (account ID, username, wallet ID, GPG fingerprint) with audit events for every state transition. That's where the schema fixtures from Week 2 start being used in anger.
The PR for this week's work is under work, since the code is really a lot to process when reviewed by mentors, so i am trying to find a way, to clean and modularise the code a little bit. If the PR gets ready, i will update this blog.
See you in Week 4!
This post is part of my LFDT Mentorship 2026 blog series. Each week I write about what I built, what I learned, and what actually surprised me along the way.
Top comments (0)