DEV Community

Darshit Khandelwal
Darshit Khandelwal

Posted on

Week 3 :The First Real Code

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:

  1. The contributor asks Heka for a challenge (a random string called a nonce)
  2. Heka generates a nonce and ties it to their GitHub username
  3. The contributor signs that nonce using their GPG private key (which never leaves their machine)
  4. They send the signature back to Heka
  5. Heka fetches the contributor's public GPG key directly from github.com/:username.gpg and 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Receive a webhook when someone opens a PR
  2. Ask Heka whether that contributor is verified
  3. 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
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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)