<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Darshit Khandelwal</title>
    <description>The latest articles on DEV Community by Darshit Khandelwal (@darshit2308).</description>
    <link>https://dev.to/darshit2308</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3984308%2Fe0cd52d3-4766-4ca8-976b-f690d710cef8.png</url>
      <title>DEV Community: Darshit Khandelwal</title>
      <link>https://dev.to/darshit2308</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darshit2308"/>
    <language>en</language>
    <item>
      <title>LFDT: Week 5 Report</title>
      <dc:creator>Darshit Khandelwal</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:51:14 +0000</pubDate>
      <link>https://dev.to/darshit2308/lfdt-week-5-report-1c2c</link>
      <guid>https://dev.to/darshit2308/lfdt-week-5-report-1c2c</guid>
      <description>&lt;h1&gt;
  
  
  Week 5: Actually Issuing the Credential
&lt;/h1&gt;

&lt;p&gt;Firstly before starting, apologies to everyone who might be following with me, I was busy with my internship interviews, (I had an interview with Google, I would talk about that in some other blog in detail).&lt;br&gt;
So, I was unable to do much of the work, but, now I am back on track with full energy and motivation !&lt;br&gt;
The first four weeks were about building the foundation , the schema, the DID, the GPG verification, the identity binding. Week 5 is where the foundation stops being theoretical. This is the week a real signed credential actually gets issued into a contributor's wallet.&lt;/p&gt;

&lt;p&gt;Here's everything that happened.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem This Week Solves
&lt;/h2&gt;

&lt;p&gt;At the end of Week 4, we had a &lt;code&gt;ContributorBinding&lt;/code&gt; record in the database that said: "this person's GitHub account, GPG key, and Heka wallet are all confirmed as belonging to the same human."&lt;/p&gt;

&lt;p&gt;That's meaningful, but it's still just a database row. It lives entirely inside Heka's infrastructure. A wallet can't hold it. A verifier can't check it independently without calling Heka's API. It's not portable, and it's not self-contained.&lt;/p&gt;

&lt;p&gt;What we actually want is a &lt;strong&gt;Verifiable Credential&lt;/strong&gt; — a cryptographically signed document the contributor holds in their own wallet, which any verifier can check without calling home.&lt;/p&gt;

&lt;p&gt;Week 5 is how we get there.&lt;/p&gt;


&lt;h2&gt;
  
  
  What is OID4VCI?
&lt;/h2&gt;

&lt;p&gt;OpenID for Verifiable Credential Issuance (OID4VCI) is the protocol that defines how an issuer hands a credential to a wallet. It's the handshake between Heka (the issuer) and the contributor's wallet (the holder).&lt;/p&gt;

&lt;p&gt;The short version of how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The wallet discovers what credentials an issuer can issue, via a &lt;code&gt;.well-known/openid-credential-issuer&lt;/code&gt; metadata document&lt;/li&gt;
&lt;li&gt;The issuer creates a &lt;strong&gt;credential offer&lt;/strong&gt; — a URI starting with &lt;code&gt;openid-credential-offer://&lt;/code&gt; that the wallet scans or follows&lt;/li&gt;
&lt;li&gt;The wallet authenticates and presents proof of key possession&lt;/li&gt;
&lt;li&gt;The issuer signs the credential and delivers it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This week implemented steps 1 and 2. Steps 3 and 4 are handled by the existing Credo/AFJ framework already wired into &lt;code&gt;heka-identity-service&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Static Issuer: One Global Issuer for the Prototype
&lt;/h2&gt;

&lt;p&gt;One design decision I want to explain upfront: we are not building multi-tenant dynamic issuer provisioning. This is a prototype, and the goal is to validate the end-to-end flow cleanly without admin overhead.&lt;/p&gt;

&lt;p&gt;Instead, there is a single &lt;strong&gt;static global issuer&lt;/strong&gt; — the pre-existing demo tenant (the same &lt;code&gt;DEMO_USER&lt;/code&gt; account already used in &lt;code&gt;heka-auth-service&lt;/code&gt;). On every application startup, a bootstrap sequence runs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the demo user's wallet → get their Credo tenant ID&lt;/li&gt;
&lt;li&gt;Open their tenant agent&lt;/li&gt;
&lt;li&gt;Check if an OID4VCI issuer record exists for their &lt;code&gt;did:hedera&lt;/code&gt; DID&lt;/li&gt;
&lt;li&gt;If not → create it (including the &lt;code&gt;GithubContributorCredential&lt;/code&gt; profile)&lt;/li&gt;
&lt;li&gt;If yes → check if &lt;code&gt;GithubContributorCredential&lt;/code&gt; is already in the supported list&lt;/li&gt;
&lt;li&gt;If not → add it (idempotent)&lt;/li&gt;
&lt;li&gt;If yes → log and skip&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire sequence is &lt;strong&gt;idempotent&lt;/strong&gt; by design. Restarting the application never creates duplicate issuers or duplicate credential configurations. The bootstrap can fail gracefully (missing wallet, missing DID) without crashing the application — it just logs a warning.&lt;/p&gt;

&lt;p&gt;This is the same pattern used by the &lt;code&gt;PrepareWalletService&lt;/code&gt; in this codebase. When the infrastructure is ready, everything self-configures.&lt;/p&gt;


&lt;h2&gt;
  
  
  The GithubContributorCredential Profile
&lt;/h2&gt;

&lt;p&gt;The credential type registered in the issuer metadata is &lt;code&gt;GithubContributorCredentialSdJwt&lt;/code&gt;, with a VCT (Verifiable Credential Type) URI of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://hiero.ledger.org/vct/GithubContributorCredential
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This URI is how wallets and verifiers identify what kind of credential they're dealing with. It's the same identifier defined in the Week 2 schema.&lt;/p&gt;

&lt;p&gt;The credential profile declares all five claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;githubAccountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;githubUsername&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;gpgFingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;walletId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  SD-JWT: Selective Disclosure in Practice
&lt;/h2&gt;

&lt;p&gt;The credential format is &lt;strong&gt;SD-JWT VC&lt;/strong&gt; (Selective Disclosure JWT Verifiable Credential). The "selective disclosure" part is what makes this interesting.&lt;/p&gt;

&lt;p&gt;A standard JWT credential puts all its claims in the open. Anyone who receives it can see everything. An SD-JWT VC lets the holder choose which claims to reveal and which to keep private, on a per-presentation basis.&lt;/p&gt;

&lt;p&gt;The "disclosure frame" is what controls this. It's a configuration that tells the signing framework which claims should be placed in cryptographically blinded digest groups (&lt;code&gt;_sd&lt;/code&gt;) rather than revealed directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Week 2 disclosure policy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disclosureFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;_sd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;githubUsername&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpgFingerprint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this means in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Always in the credential?&lt;/th&gt;
&lt;th&gt;Can the holder hide it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;githubAccountId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Always revealed&lt;/td&gt;
&lt;td&gt;No — it's the identity anchor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verifiedAt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Always revealed&lt;/td&gt;
&lt;td&gt;No — it's the audit timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;walletId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Always revealed&lt;/td&gt;
&lt;td&gt;No — it's the wallet binding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;githubUsername&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Digest only&lt;/td&gt;
&lt;td&gt;✅ Yes — holder can choose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gpgFingerprint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Digest only&lt;/td&gt;
&lt;td&gt;✅ Yes — holder can choose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The design intent: a contributor should be able to prove they are a verified Hiero contributor (by showing their numeric account ID and verification timestamp) without necessarily revealing their GitHub handle or GPG key fingerprint to every verifier. The holder controls what gets shared.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deterministic Verification Method Selection
&lt;/h2&gt;

&lt;p&gt;When signing the SD-JWT VC, you need to select which verification method (which key) from the issuer DID document to sign with. If you pick differently each time, different verifiers may not be able to reproduce the check.&lt;/p&gt;

&lt;p&gt;The approach here is simple and consistent with how the rest of &lt;code&gt;heka-identity-service&lt;/code&gt; handles this: take the first verification method from the resolved DID document. For a &lt;code&gt;did:hedera&lt;/code&gt; DID with a single key pair (which is the standard setup for the demo tenant), this is always the same key. Deterministic, predictable, no surprise.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Credential Offer Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /v2/contributor-credential/offer
{ "githubAccountId": "12345678" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Looks up the &lt;code&gt;ContributorBinding&lt;/code&gt; for the given GitHub account ID&lt;/li&gt;
&lt;li&gt;Throws &lt;code&gt;404&lt;/code&gt; if no binding exists (contributor hasn't completed OAuth login)&lt;/li&gt;
&lt;li&gt;Throws &lt;code&gt;409&lt;/code&gt; if the binding exists but GPG verification is incomplete&lt;/li&gt;
&lt;li&gt;Resolves the demo tenant's &lt;code&gt;did:hedera&lt;/code&gt; DID&lt;/li&gt;
&lt;li&gt;Builds the SD-JWT VC payload from the binding fields&lt;/li&gt;
&lt;li&gt;Applies the Week 2 disclosure frame&lt;/li&gt;
&lt;li&gt;Calls Credo's &lt;code&gt;createCredentialOffer()&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;Returns the &lt;code&gt;openid-credential-offer://&lt;/code&gt; URI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A key implementation detail on step 7: I'm calling &lt;code&gt;tenantAgent.openid4vc.issuer.createCredentialOffer()&lt;/code&gt; directly rather than routing through &lt;code&gt;OpenId4VcIssuanceSessionService.offer()&lt;/code&gt;. The reason is that the session service unconditionally calls &lt;code&gt;statusListService.getOrCreate(authInfo)&lt;/code&gt; — which needs a full &lt;code&gt;User&lt;/code&gt; entity from the database — before checking whether the credential even uses revocation. SD-JWT VCs don't support revocation (it's in the spec). Routing around the status list check for SD-JWT VCs is actually the architecturally correct thing to do here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Bugs Found During Review
&lt;/h2&gt;

&lt;p&gt;I did a deep review pass before calling Week 5 done. Two real bugs caught:&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: Wrong enum value (silent failure)
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;updateIssuerMetadata&lt;/code&gt; call was using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;  &lt;span class="c1"&gt;// ❌ Wrong&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual enum is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;UpdateIssuerSupportedCredentialsAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Add&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// lowercase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The switch statement in &lt;code&gt;issuer.service.ts&lt;/code&gt; was hitting the default branch and silently doing nothing. The credential config would never have been registered on existing issuers. Fixed to use the proper enum import.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: Fake AuthInfo crashing the status list service
&lt;/h3&gt;

&lt;p&gt;Earlier I was constructing a minimal auth-info object to pass to the session service. The status list service does &lt;code&gt;em.find(CredentialStatusList, { owner: authInfo.user })&lt;/code&gt; — and &lt;code&gt;authInfo.user&lt;/code&gt; was undefined, which would have been a runtime crash on every credential offer request. Fixed by bypassing the session service entirely (see step 7 above).&lt;/p&gt;

&lt;p&gt;Both bugs were caught by reading the actual source of every dependency — not just the types. TypeScript alone won't catch a structural type that's missing a field when the missing field is only accessed at runtime inside a service you're calling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test Coverage
&lt;/h2&gt;

&lt;p&gt;11 unit tests covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bootstrap creates issuer when demo wallet exists and no issuer is registered&lt;/li&gt;
&lt;li&gt;Bootstrap skips when credential config already registered&lt;/li&gt;
&lt;li&gt;Bootstrap logs warning without throwing when demo wallet is missing&lt;/li&gt;
&lt;li&gt;Bootstrap uses the correct &lt;code&gt;'add'&lt;/code&gt; enum value (explicit assertion)&lt;/li&gt;
&lt;li&gt;Credential offer returns a valid &lt;code&gt;openid-credential-offer://&lt;/code&gt; URI&lt;/li&gt;
&lt;li&gt;Payload carries all five Week 2 claims with correct values&lt;/li&gt;
&lt;li&gt;Disclosure frame contains exactly the two selectively disclosable claims&lt;/li&gt;
&lt;li&gt;Non-selectively-disclosable claims are absent from &lt;code&gt;_sd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verification method from DID document is passed through (deterministic selection)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NotFoundException&lt;/code&gt; when no binding exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ConflictException&lt;/code&gt; when binding is GPG-unverified&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NotFoundException&lt;/code&gt; when demo tenant wallet is missing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;307 total tests, all passing.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Five Weeks Connect
&lt;/h2&gt;

&lt;p&gt;Looking back at the full arc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Week 1:&lt;/strong&gt; Project setup, infrastructure, development environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2:&lt;/strong&gt; Credential schema, disclosure policy, DID on Hedera testnet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3:&lt;/strong&gt; GPG challenge-response — cryptographic proof of key ownership&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 4:&lt;/strong&gt; Identity binding — tying GPG key, GitHub account, and Heka wallet together permanently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 5:&lt;/strong&gt; OID4VCI issuance — turning that binding into a real signed credential a wallet can hold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each week was a necessary prerequisite for the next. You can't issue a credential without a schema (Week 2). You can't bind an identity without verifying the key (Week 3). You can't issue a meaningful credential without a completed binding (Week 4).&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Week 6 moves into the Web Wallet UI and the PR-gating flow — the two pieces that make the credential actually useful. A contributor will be able to present their &lt;code&gt;GithubContributorCredential&lt;/code&gt; when opening a PR, and the GitHub App will check the verification status before allowing the PR to be merged.&lt;/p&gt;

&lt;p&gt;The credential is now real. Next week it starts doing work.&lt;/p&gt;

&lt;p&gt;See you then.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of my LFDT Mentorship 2026 blog series. Each week I write about what I built, what I learned, and what surprised me. If you're working on decentralized identity, verifiable credentials, or open source supply chain security, I'd love to hear from you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>learning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 4 - LFX Mentorship</title>
      <dc:creator>Darshit Khandelwal</dc:creator>
      <pubDate>Tue, 14 Jul 2026 17:31:55 +0000</pubDate>
      <link>https://dev.to/darshit2308/week-4-lfx-mentorship-5bko</link>
      <guid>https://dev.to/darshit2308/week-4-lfx-mentorship-5bko</guid>
      <description>&lt;p&gt;A quick note before I start: I was running slightly behind schedule, so I combined Weeks 3 and 4 into one push. The good news is that what came out the other side is more complete than either week would have been in isolation — the two pieces fit together naturally, and building them back-to-back made the design tighter.&lt;/p&gt;

&lt;p&gt;Here's everything that happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem These Two Weeks Solve
&lt;/h2&gt;

&lt;p&gt;Before Week 3, we had a schema on paper and a DID on the testnet. But nothing was actually &lt;em&gt;checking&lt;/em&gt; anything. No one was being verified. No PR was being gated.&lt;/p&gt;

&lt;p&gt;The gap was this: how do you take "this person claims to be darshit2308 on GitHub" and turn it into something a machine can trust cryptographically?&lt;/p&gt;

&lt;p&gt;That's what Week 3 answered. And Week 4 answered the follow-up question: once you've verified the GPG key, how do you bind that proof permanently to a real identity — one that ties together their GitHub account, their GPG key, and their Heka wallet — so the system doesn't have to re-verify every single time?&lt;/p&gt;

&lt;p&gt;Let's go through both.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 3: The Cryptographic Foundation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How GPG Challenge-Response Works (and Why We're Doing It This Way)
&lt;/h3&gt;

&lt;p&gt;The core idea is simple but elegant. Your GitHub profile can optionally have GPG public keys attached to it. Anyone can see them. What only you can do is sign something with the matching private key — which never leaves your machine.&lt;/p&gt;

&lt;p&gt;So the verification flow goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You ask Heka for a challenge — a random one-time string called a nonce&lt;/li&gt;
&lt;li&gt;Heka generates the nonce, ties it to your GitHub username, and starts a 5-minute countdown&lt;/li&gt;
&lt;li&gt;You run one command locally: &lt;code&gt;echo "&amp;lt;nonce&amp;gt;" | gpg --clearsign&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You send the signed output back to Heka&lt;/li&gt;
&lt;li&gt;Heka fetches your public GPG key directly from &lt;code&gt;github.com/your-username.gpg&lt;/code&gt; and checks the math&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the signature is valid, it proves two things at once: you control the private key, and that private key is registered to your GitHub account. You can't fake either of those.&lt;/p&gt;

&lt;p&gt;This is not a new idea — it's the same mechanism behind SSH authentication and code signing. We're applying it to contributor identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the Module
&lt;/h3&gt;

&lt;p&gt;The GPG module lives inside &lt;code&gt;heka-identity-service/src/gpg-challenge/&lt;/code&gt; as a self-contained NestJS module. Three endpoints:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;POST /request&lt;/code&gt;&lt;/strong&gt; — Generate a challenge. Takes a GitHub username, returns a nonce. Under the hood, it uses &lt;code&gt;crypto.randomBytes(32)&lt;/code&gt; — 256 bits of OS-level randomness — and stores it with a 5-minute expiry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;POST /verify&lt;/code&gt;&lt;/strong&gt; — Verify a signed challenge. This is where the interesting work happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;GET /status&lt;/code&gt;&lt;/strong&gt; — Check if a GitHub username is verified.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Security Detail I'm Most Proud Of: Burn Before Verify
&lt;/h3&gt;

&lt;p&gt;The replay attack scenario goes like this: someone intercepts a valid signed payload in transit, and tries submitting it again after you've already verified. If we mark the nonce as consumed &lt;em&gt;after&lt;/em&gt; verification succeeds, and the process crashes between those two steps, the attacker can replay the same payload and it'll pass again.&lt;/p&gt;

&lt;p&gt;The fix is simple but requires thinking about failure modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consumed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challengeRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntityManager&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// This happens FIRST&lt;/span&gt;
&lt;span class="c1"&gt;// Only after the DB write commits do we touch the network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nonce is marked consumed and flushed to the database &lt;em&gt;before&lt;/em&gt; any network call or cryptographic work happens. Even if the server crashes mid-verification, the nonce is permanently dead. The contributor just needs to request a fresh one.&lt;/p&gt;

&lt;p&gt;This is called "burn before verify" and it eliminates the replay window entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Handling: Every Failure Path
&lt;/h3&gt;

&lt;p&gt;One thing I wanted to get right early was making errors actually useful to contributors. There are 10 distinct failure scenarios in this module, and each one returns a specific HTTP status and a message that tells you what to do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What went wrong&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Challenge ID doesn't exist&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonce already used (replay blocked)&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Challenge expired&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No GPG key on your GitHub profile&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub rate limiting us&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub unreachable&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPG key body is empty&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key format is unparseable&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signed message is malformed&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature is mathematically wrong&lt;/td&gt;
&lt;td&gt;200 &lt;code&gt;{ verified: false }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last one is deliberate — a failed signature isn't a server error, it's a valid answer from the verification system. It returns 200 with &lt;code&gt;verified: false&lt;/code&gt; rather than throwing an exception.&lt;/p&gt;

&lt;h3&gt;
  
  
  25 Tests, All Passing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;16 unit tests&lt;/strong&gt; covering every failure path above, with the burn-before-verify ordering explicitly tested (the test verifies that &lt;code&gt;flush()&lt;/code&gt; is called before the GitHub API mock fires)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9 integration tests&lt;/strong&gt; against a real PostgreSQL instance, covering the full HTTP stack including replay via double-submit and expired record insertion via ORM&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The GitHub App: Making PRs Actually Respond
&lt;/h3&gt;

&lt;p&gt;The GitHub App lives in a separate repository (GitHub integration code stays separate from &lt;code&gt;heka-identity-service&lt;/code&gt;, as agreed in the architecture). It's built on Probot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Probot?&lt;/strong&gt; Two things I would have had to build myself are handled automatically:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Webhook signature validation&lt;/em&gt;: Every incoming request from GitHub is signed with HMAC-SHA256. Probot validates that signature before any application code runs. Invalid signature → 400, nothing fires.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GitHub App authentication&lt;/em&gt;: GitHub Apps authenticate using JWT tokens that expire every 10 minutes. Probot manages the entire JWT lifecycle. I never touch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The async acknowledgment problem&lt;/strong&gt;: GitHub requires your webhook endpoint to respond with HTTP 200 within 10 seconds. But checking Heka, creating a check run, and doing DID resolution can take longer than that under real network conditions. Probot solves this cleanly — it returns 200 immediately, and fires application listeners asynchronously through its internal event emitter. The listeners are not in the response path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;: GitHub can and does redeliver webhook events. Without idempotency, you'd end up with multiple check runs for the same commit cluttering the PR view. Before creating a new check run, the app looks up whether one already exists for the exact commit SHA and check name. If a non-queued run exists, it exits early. The key is using the SHA (not the PR number) — a new push to the same PR produces a new SHA and correctly triggers a new check, while a retried webhook delivery hits the guard and does nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End-to-end proof&lt;/strong&gt;: The full flow was validated live on the &lt;code&gt;Heka-Webhook-Fixture&lt;/code&gt; repository. A PR opened, Heka's identity check fired, and the check run appeared in the GitHub UI. That closed the loop on Week 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 4: Binding the Proof to a Real Identity
&lt;/h2&gt;

&lt;p&gt;Week 3 answered "can you prove you own this GPG key?" Week 4 answered the follow-up question: "okay, but now what do we actually &lt;em&gt;do&lt;/em&gt; with that proof?"&lt;/p&gt;

&lt;p&gt;The answer is a &lt;code&gt;ContributorBinding&lt;/code&gt; — a record that ties together three things permanently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The GitHub OAuth account (with its numeric account ID)&lt;/li&gt;
&lt;li&gt;The verified GPG fingerprint&lt;/li&gt;
&lt;li&gt;The Heka wallet&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why all three? Because each one alone is insufficient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub account alone is spoofable&lt;/li&gt;
&lt;li&gt;GPG key alone doesn't prove which GitHub account you are&lt;/li&gt;
&lt;li&gt;Wallet alone is just a key pair with no human identity attached&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The binding is what makes the proof durable. Once created, the system can verify you on future PRs without requiring a fresh GPG signature every single time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Changed in the Database
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;GpgChallenge&lt;/code&gt; entity was upgraded to track not just &lt;code&gt;githubUsername&lt;/code&gt;, but also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;githubAccountId&lt;/code&gt; — the immutable integer ID from GitHub (users can change their handle, but this never changes, which is how we handle username renames without losing their verification status)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;walletId&lt;/code&gt; — the Heka wallet of the authenticated user who requested the challenge&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Audit Logging: Every State Transition is Recorded
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ContributorOnboardingModule&lt;/code&gt; is now wired directly into the GPG verification flow. Every meaningful event gets recorded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;recordChallengeRequested&lt;/code&gt; — fires when a challenge is generated, against the contributor's wallet profile&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recordProofAccepted&lt;/code&gt; — fires when verification succeeds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recordProofRejected&lt;/code&gt; — fires when verification fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters for maintainers. If something goes wrong with a contributor's verification status, there's a full audit trail of what happened and when.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Wallet Enforcement Check
&lt;/h3&gt;

&lt;p&gt;This one is a subtle but important security detail. When a contributor submits their GPG signature for verification, we now check that the &lt;code&gt;walletId&lt;/code&gt; submitting the request matches the &lt;code&gt;walletId&lt;/code&gt; that originally requested the challenge.&lt;/p&gt;

&lt;p&gt;Why does this matter? Without it, someone could theoretically observe that wallet A requested a challenge for username X, then try to claim that verification from wallet B. The wallet enforcement check closes that gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Status Endpoint Rework
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/status&lt;/code&gt; endpoint in Week 3 checked the GPG challenges table directly — if a challenge was verified, you're good. That's fine for a prototype, but it has a flaw: it only tells you the GPG proof succeeded. It doesn't tell you whether the full binding (GPG key + GitHub account + Heka wallet) was actually completed.&lt;/p&gt;

&lt;p&gt;In Week 4 the endpoint was refactored to check for a finalized &lt;code&gt;ContributorBinding&lt;/code&gt; record instead. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;verified: true&lt;/code&gt; now means the GPG key, GitHub account, and Heka wallet are all tied together and confirmed&lt;/li&gt;
&lt;li&gt;There's no state where partial verification gets treated as complete&lt;/li&gt;
&lt;li&gt;The GitHub App can trust the status endpoint as a real signal, not just an intermediate step&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How the Two Weeks Connect
&lt;/h2&gt;

&lt;p&gt;The progression makes more sense when you see it as one arc:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3&lt;/strong&gt; built the cryptographic primitive: "this person controls this GPG key."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4&lt;/strong&gt; turned that primitive into a durable identity claim: "this GPG key, this GitHub account, and this Heka wallet all belong to the same person."&lt;/p&gt;

&lt;p&gt;Without Week 3, you have no cryptographic proof. Without Week 4, that proof evaporates after each verification and doesn't attach to anything persistent. Together, they're the foundation that Weeks 5 and beyond (OID4VCI issuance, the Web Wallet, PR-gating) are built on top of.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Week 5 is OID4VCI issuer metadata and SD-JWT VC issuance — taking the &lt;code&gt;GithubContributorCredential&lt;/code&gt; schema from Week 2 and wiring it into Heka's OpenID4VC issuance flow so contributors actually receive a signed credential into their wallet.&lt;/p&gt;

&lt;p&gt;The schema design, the DID infrastructure, the GPG verification, and the identity binding are all in place. Next week is where it starts issuing real credentials.&lt;/p&gt;

&lt;p&gt;See you then.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of my LFDT Mentorship 2026 blog series. Each week I write about what I built, what I learned, and what surprised me. If you're working on decentralized identity or open source supply chain security, I'd love to hear from you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>devjournal</category>
      <category>opensource</category>
      <category>web3</category>
    </item>
    <item>
      <title>Week 3 :The First Real Code</title>
      <dc:creator>Darshit Khandelwal</dc:creator>
      <pubDate>Mon, 06 Jul 2026 10:45:05 +0000</pubDate>
      <link>https://dev.to/darshit2308/week-3-the-first-real-code-2fml</link>
      <guid>https://dev.to/darshit2308/week-3-the-first-real-code-2fml</guid>
      <description>&lt;p&gt;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 &lt;code&gt;heka-identity-service&lt;/code&gt;, and the initial skeleton of the GitHub App that will eventually gate pull requests.&lt;/p&gt;

&lt;p&gt;Let me walk through both.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Week Was About
&lt;/h2&gt;

&lt;p&gt;The plan for Week 3 had two parallel tracks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track A — Inside &lt;code&gt;heka-identity-service&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Port the GPG challenge-response flow from the MVP prototype into a proper NestJS module&lt;/li&gt;
&lt;li&gt;Cryptographically secure nonces, 5-minute TTL, single-use enforcement&lt;/li&gt;
&lt;li&gt;Full error handling for every failure scenario&lt;/li&gt;
&lt;li&gt;Unit and integration tests covering all paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Track B — In the separate GitHub App repo:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the initial Probot adapter skeleton&lt;/li&gt;
&lt;li&gt;Webhook signature validation and GitHub App authentication&lt;/li&gt;
&lt;li&gt;Async acknowledgment pattern so GitHub doesn't time out waiting for us&lt;/li&gt;
&lt;li&gt;Idempotent check run creation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's go through both in detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Track A: The GPG Challenge Module
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a GPG Challenge-Response, and why does it matter?
&lt;/h3&gt;

&lt;p&gt;Before I explain what I built, let me quickly explain what problem this solves.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The contributor asks Heka for a challenge (a random string called a nonce)&lt;/li&gt;
&lt;li&gt;Heka generates a nonce and ties it to their GitHub username&lt;/li&gt;
&lt;li&gt;The contributor signs that nonce using their &lt;strong&gt;GPG private key&lt;/strong&gt; (which never leaves their machine)&lt;/li&gt;
&lt;li&gt;They send the signature back to Heka&lt;/li&gt;
&lt;li&gt;Heka fetches the contributor's &lt;strong&gt;public GPG key&lt;/strong&gt; directly from &lt;code&gt;github.com/:username.gpg&lt;/code&gt; and verifies the signature mathematically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This is the same mechanism used by SSH authentication, code signing, and certificate issuance — it's a well-understood cryptographic primitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  The NestJS Module Structure
&lt;/h3&gt;

&lt;p&gt;The flow now lives inside &lt;code&gt;heka-identity-service/src/gpg-challenge/&lt;/code&gt; as a self-contained NestJS module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module is imported in &lt;code&gt;app.module.ts&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating Secure Nonces
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 256 bits of entropy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses Node's built-in &lt;code&gt;crypto&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The nonce is stored alongside the contributor's &lt;code&gt;githubUsername&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 5-Minute TTL and Single-Use Enforcement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTL is enforced server-side at verification time — not by trusting the client's clock.&lt;/p&gt;

&lt;p&gt;The more interesting part is &lt;strong&gt;burn-before-verify&lt;/strong&gt;: the challenge is marked as consumed in the database &lt;em&gt;before&lt;/em&gt; any network or cryptographic work happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consumed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challengeRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntityManager&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Persisted BEFORE anything else&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Handling: Every Failure Path Covered
&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;HTTP Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unknown challenge ID&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay attempt (already consumed)&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expired challenge&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub 404 (no GPG key on profile)&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub rate limit (403/429)&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub unreachable (network failure)&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty GPG key body&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unparseable public key&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unparseable armored message&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature math fails&lt;/td&gt;
&lt;td&gt;200 &lt;code&gt;{ verified: false }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last one is intentional — a failed signature isn't a server error, it's a valid "no" from the verification system.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Test Suite
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; (16 tests):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every failure path above is tested in isolation&lt;/li&gt;
&lt;li&gt;The burn-before-verify ordering is verified — the test confirms that &lt;code&gt;flush()&lt;/code&gt; is called before the GitHub API mock fires&lt;/li&gt;
&lt;li&gt;No real database or network required — uses deep mocks throughout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Integration/E2E tests&lt;/strong&gt; (9 tests):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs against a real PostgreSQL instance with the full NestJS HTTP stack&lt;/li&gt;
&lt;li&gt;Covers all 3 endpoints, DTO validation, replay via double-submit, and expired record insertion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined: 25 tests, all passing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Track B: The GitHub App Skeleton
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What the GitHub App Actually Does
&lt;/h3&gt;

&lt;p&gt;The GitHub App sits in a separate repository (as decided with my mentor — GitHub integration code stays separate from &lt;code&gt;heka-identity-service&lt;/code&gt;). Its job, at the end of the mentorship, will be to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive a webhook when someone opens a PR&lt;/li&gt;
&lt;li&gt;Ask Heka whether that contributor is verified&lt;/li&gt;
&lt;li&gt;Post a passing or failing check run to the PR&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Probot?
&lt;/h3&gt;

&lt;p&gt;Probot is a Node.js framework specifically designed for building GitHub Apps. It handles two things that would otherwise be painful to implement manually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhook signature validation&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub App authentication&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Async Acknowledgment Pattern
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Probot guarantees:&lt;/span&gt;
&lt;span class="c1"&gt;//   - HTTP 200 returned to GitHub immediately on receipt (before async&lt;/span&gt;
&lt;span class="c1"&gt;//     listeners complete), satisfying the 10-second delivery timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So GitHub gets its 200, and our actual logic runs in the background.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotent Check Runs
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listForRef&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;check_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CHECK_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Honest Gap
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Surprised Me This Week
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The burn-before-verify ordering.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Probot architecture.&lt;/strong&gt; I expected to have to wire up the async response pattern manually, but Probot's design means I get it for free. Understanding &lt;em&gt;why&lt;/em&gt; it works that way (the event emitter is separate from the HTTP response path) made it click.&lt;/p&gt;




&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
See you in Week 4!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devjournal</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Week -02</title>
      <dc:creator>Darshit Khandelwal</dc:creator>
      <pubDate>Tue, 30 Jun 2026 14:21:36 +0000</pubDate>
      <link>https://dev.to/darshit2308/week-02-1bol</link>
      <guid>https://dev.to/darshit2308/week-02-1bol</guid>
      <description>&lt;h1&gt;
  
  
  Building the GitHubContributorCredential: SD-JWT Design and Hedera DID Verification
&lt;/h1&gt;

&lt;p&gt;This week's goals were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define the complete &lt;code&gt;GitHubContributorCredential&lt;/code&gt; schema, including contributor DID, GitHub username, GitHub numeric account ID, verified GPG fingerprint, issuer DID, issuance time, expiry, and credential status reference.&lt;/li&gt;
&lt;li&gt;Design the SD-JWT disclosure policy and determine which claims are always disclosed versus selectively disclosed.&lt;/li&gt;
&lt;li&gt;Create example credential payloads and schema fixtures that can be reused throughout implementation and testing.&lt;/li&gt;
&lt;li&gt;Validate the credential model with unit tests covering valid and invalid credential subjects.&lt;/li&gt;
&lt;li&gt;Create and resolve a &lt;code&gt;did:hedera&lt;/code&gt; DID on Hedera testnet using the operator credentials agreed during Week 1.&lt;/li&gt;
&lt;li&gt;Capture proof of successful DID creation and resolution through HashScan and resolver outputs.&lt;/li&gt;
&lt;li&gt;Implement deterministic verification-method selection so signing operations never depend on array ordering such as &lt;code&gt;verificationMethod[0]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add tests covering DID resolution and verification-method selection behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how the week went, day by day.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Defining the GitHubContributorCredential schema
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GitHubContributorCredential&lt;/code&gt; holds every field our system needs to identify a contributor. To keep it usable by the Heka backend service, we split the fields into two categories: &lt;strong&gt;standard VC metadata&lt;/strong&gt; (the outer wrapper) and the &lt;strong&gt;credential subject&lt;/strong&gt; (the actual identity claims).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard VC metadata&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issuer DID&lt;/strong&gt; — the &lt;code&gt;did:hedera&lt;/code&gt; identifier of the Heka platform issuing the credential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuance time&lt;/strong&gt; — the ISO 8601 timestamp of when the proof was generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry&lt;/strong&gt; — when the credential needs to be renewed (e.g., six months or a year from issuance).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential status reference&lt;/strong&gt; — a pointer to the revocation registry on the Hedera network, so Heka can revoke the credential if a contributor's keys are compromised.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Credential subject (the contributor claims)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contributor DID&lt;/strong&gt; — the developer's personal &lt;code&gt;did:hedera&lt;/code&gt; identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub username&lt;/strong&gt; — the human-readable handle (e.g., &lt;code&gt;darshit2308&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub numeric account ID&lt;/strong&gt; — the immutable integer ID GitHub assigns to every account. This matters because a user can change their handle, but the numeric ID never changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verified GPG fingerprint&lt;/strong&gt; — the cryptographic fingerprint of the key used to sign commits, proving the holder controls the private key registered against their GitHub account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Putting it all together, here's the schema draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.w3.org/2018/credentials/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://w3id.org/security/suites/ed25519-2020/v1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"VerifiableCredential"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHubContributorCredential"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"did:hedera:testnet:z6MkhaXgBZDvotDkL5257faiztiuC2ZXOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuanceDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-28T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expirationDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-06-28T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"credentialStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://heka.network/status/123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CredentialStatusList2020"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"credentialSubject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"did:hedera:testnet:z6Mkq..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"githubUsername"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"darshit2308"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"githubAccountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4115704&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gpgFingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3AA5C34371567BD2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick walkthrough for anyone new to verifiable credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@context&lt;/code&gt; tells any parser "read this using W3C Verifiable Credentials rules" — it gives the document its vocabulary.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; is just a unique identifier for the credential document itself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt; declares what kind of credential this is.&lt;/li&gt;
&lt;li&gt;Everything else maps to the eight fields described above.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note for later:&lt;/strong&gt; &lt;code&gt;CredentialStatusList2020&lt;/code&gt; isn't an officially standardized status type — the current W3C recommendation is the &lt;a href="https://www.w3.org/TR/vc-bitstring-status-list/" rel="noopener noreferrer"&gt;Bitstring Status List&lt;/a&gt; spec (&lt;code&gt;BitstringStatusListEntry&lt;/code&gt;). Worth revisiting before this goes to production, even if &lt;code&gt;CredentialStatusList2020&lt;/code&gt; stays as an internal placeholder for now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Designing the SD-JWT disclosure policy
&lt;/h2&gt;

&lt;p&gt;Quick primer on SD-JWT (Selective Disclosure JWT) for anyone unfamiliar: it lets a credential holder reveal only the claims required to verify something, without exposing everything else. A simple analogy — a bouncer checking you're over 18 doesn't need your phone number or home address, just proof of age.&lt;/p&gt;

&lt;p&gt;So we split &lt;code&gt;GitHubContributorCredential&lt;/code&gt; into two buckets: fields that are &lt;strong&gt;always disclosed&lt;/strong&gt; (mandatory for the system to function) and fields that are &lt;strong&gt;selectively disclosed&lt;/strong&gt; (hidden by default, to protect the developer's privacy).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always disclosed&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issuer DID&lt;/strong&gt; — Heka needs to know who issued the credential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuance &amp;amp; expiry dates&lt;/strong&gt; — Heka needs to verify the time limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential status&lt;/strong&gt; — Heka must be able to check whether it's been revoked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributor DID (&lt;code&gt;sub&lt;/code&gt;)&lt;/strong&gt; — the developer's public identifier on the Hedera network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Selectively disclosed&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub username&lt;/strong&gt; — a developer may want to prove they're an authorized LFDT contributor without permanently linking their Hedera DID to a public GitHub handle anyone could scrape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub numeric account ID&lt;/strong&gt; — kept hidden unless the backend specifically needs to map the DID to an exact GitHub account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verified GPG fingerprint&lt;/strong&gt; — only revealed when a repository explicitly needs it to verify a signed commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Example payloads and schema fixtures
&lt;/h2&gt;

&lt;p&gt;Since we won't reveal every field by default, the SD-JWT payload replaces the hidden fields with an &lt;code&gt;_sd&lt;/code&gt; (selective disclosure) array of hashes. A first pass looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"did:hedera:testnet:z6MkhaXgBZDvotDkL5257faiztiuC2ZXOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1782604800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1814140800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"did:hedera:testnet:z6Mkq..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHubContributorCredential"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_sd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"JzYx0... (hash of githubUsername)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Qp2Lm... (hash of githubAccountId)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Xy9Rq... (hash of gpgFingerprint)"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_sd_alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha-256"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Public (always-disclosed) fields: &lt;code&gt;iss&lt;/code&gt; (issuer), &lt;code&gt;iat&lt;/code&gt; (issuance time), &lt;code&gt;exp&lt;/code&gt; (expiry), &lt;code&gt;sub&lt;/code&gt; (contributor DID), and &lt;code&gt;vct&lt;/code&gt; (the credential type — since it's not in &lt;code&gt;_sd&lt;/code&gt;, it's visible by default too).&lt;/p&gt;

&lt;p&gt;Hidden fields: username, account ID, GPG fingerprint.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_sd_alg&lt;/code&gt; declares exactly which hash function was used, so the verifier knows how to check the disclosures later.&lt;/p&gt;

&lt;p&gt;The verifier can't just brute-force the hidden fields from the hashes, since that would defeat the point — anyone could disclose everything by guessing. So each hidden claim is paired with a random salt before hashing: the disclosure is really &lt;code&gt;hash(salt + claim_name + claim_value)&lt;/code&gt;, and only the holder can produce the matching salt+value pair when asked to disclose it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where this landed
&lt;/h3&gt;

&lt;p&gt;The finished schema fixture (JSON Schema for the credential subject):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json-schema.org/draft/2020-12/schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHubContributorCredentialSubject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The exact rules for Heka GitHub Contributor claims"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^did:hedera:.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The developer's Hedera DID"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"githubUsername"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"githubAccountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gpgFingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^[0-9A-F]{16,40}$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Uppercase HEX fingerprint"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"githubUsername"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"githubAccountId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpgFingerprint"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; the &lt;code&gt;gpgFingerprint&lt;/code&gt; pattern allows 16–40 hex characters. A full GPG fingerprint is conventionally 40 hex characters (SHA-1, 160-bit); a 16-character value is really a GPG &lt;em&gt;key ID&lt;/em&gt;, not a fingerprint. Worth tightening this to exactly 40 characters unless we genuinely want to accept short key IDs too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the final credential payload fixture, now including &lt;code&gt;credentialStatus&lt;/code&gt; so it matches the "always disclosed" policy from Section 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"did:hedera:testnet:z6MkhaXgBZDvotDkL5257faiztiuC2ZXOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1782604800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1814140800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"did:hedera:testnet:z6MkqExampleDeveloperDID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHubContributorCredential"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"credentialStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://heka.network/status/123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CredentialStatusList2020"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_sd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"JzYx0aB9... (hash of 'githubUsername': 'darshit2308')"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Qp2LmC4... (hash of 'githubAccountId': 4115704)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Xy9RqZ1... (hash of 'gpgFingerprint': '3AA5C34371567BD2')"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_sd_alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha-256"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These live in the repo as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;heka-identity-service/src/credentials/fixtures/github-contributor-credential.schema.json&lt;/code&gt; — the JSON Schema defining what a valid &lt;code&gt;GitHubContributorCredential&lt;/code&gt; subject looks like, the same way a government ID schema defines which fields (name, date of birth, etc.) must be present.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;heka-identity-service/src/credentials/fixtures/mock-sd-jwt-payload.json&lt;/code&gt; — a mock &lt;code&gt;GitHubContributorCredential&lt;/code&gt; for a fictional contributor, used across tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Validating the model with Jest
&lt;/h2&gt;

&lt;p&gt;This was my first time writing Jest tests, so a quick summary of what I learned.&lt;/p&gt;

&lt;p&gt;We use &lt;strong&gt;AJV&lt;/strong&gt; (Another JSON &lt;em&gt;Schema&lt;/em&gt; Validator) to check whether a piece of JSON data conforms to a given JSON Schema — for example, confirming &lt;code&gt;githubAccountId&lt;/code&gt; is actually an integer, as the schema requires.&lt;/p&gt;

&lt;p&gt;The tests live in &lt;code&gt;heka-identity-service/src/credentials/credential-subject.spec.ts&lt;/code&gt;. The &lt;code&gt;.spec.ts&lt;/code&gt; suffix is Jest's convention for a specification (test) file, and its job is to verify the schema behaves correctly against both valid and invalid inputs.&lt;/p&gt;

&lt;p&gt;A small helper does the heavy lifting of turning a fixture file into a usable object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadFixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the test suite exercises as many valid and invalid permutations of the credential subject as we could think of — missing fields, wrong types, malformed DIDs, malformed fingerprints — to make sure AJV rejects exactly what it should and accepts exactly what it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. did:hedera DID creation and deterministic verification-method selection
&lt;/h2&gt;

&lt;p&gt;Using the operator credentials agreed on in Week 1, we created and resolved a &lt;code&gt;did:hedera&lt;/code&gt; DID on Hedera testnet, with proof of creation and resolution captured via HashScan and the resolver output.&lt;/p&gt;

&lt;p&gt;We also implemented deterministic verification-method selection, so signing operations always pick a specific, well-defined verification method rather than relying on array position (e.g., &lt;code&gt;verificationMethod[0]&lt;/code&gt;), which isn't guaranteed to be stable. Tests now cover both DID resolution and this selection logic.&lt;/p&gt;

&lt;p&gt;The code-level detail for both of these — along with the full schema, fixtures, and Jest suite above — is in the PR linked below; it's a better reference than a paraphrase here.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;PR for reference:&lt;/strong&gt; &lt;a href="https://github.com/hiero-ledger/heka-identity-platform/pull/179" rel="noopener noreferrer"&gt;hiero-ledger/heka-identity-platform#179&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>devjournal</category>
      <category>github</category>
      <category>web3</category>
    </item>
    <item>
      <title>Week - 1 (Phase 1)</title>
      <dc:creator>Darshit Khandelwal</dc:creator>
      <pubDate>Mon, 22 Jun 2026 14:10:41 +0000</pubDate>
      <link>https://dev.to/darshit2308/week-1-phase-1-fa6</link>
      <guid>https://dev.to/darshit2308/week-1-phase-1-fa6</guid>
      <description>&lt;h1&gt;
  
  
  Week 1 of My LFDT Mentorship: Architecture, Hedera, and Getting Everything Running
&lt;/h1&gt;

&lt;p&gt;Welcome to the first post of my LFDT (Linux Foundation Decentralized Trust) mentorship blog! Each week I'll be writing about what I built, what I learned, and — honestly — what confused me along the way.&lt;/p&gt;

&lt;p&gt;Week 1 was all about &lt;strong&gt;planning, architecture decisions, and setting up the groundwork&lt;/strong&gt; before any real code gets written. No flashy features yet, but a lot of important decisions that will shape everything that comes after. Let's get into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Week at a Glance
&lt;/h2&gt;

&lt;p&gt;Here's a quick summary of everything on my plate this week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Align the implementation plan with my mentor and confirm the interpretation of Issue #87&lt;/li&gt;
&lt;li&gt;Finalize the target architecture for the whole project&lt;/li&gt;
&lt;li&gt;Sort out the Hedera testnet setup and operator credentials&lt;/li&gt;
&lt;li&gt;Understand the revocation strategy and its scope during mentorship&lt;/li&gt;
&lt;li&gt;Set up the local Heka development environment&lt;/li&gt;
&lt;li&gt;Install and configure a GitHub App for local testing&lt;/li&gt;
&lt;li&gt;Create a webhook fixture repository for replaying PR events&lt;/li&gt;
&lt;li&gt;Prepare a PR slicing plan for incremental delivery&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Aligning with the Mentor &amp;amp; Locking Down the Plan
&lt;/h2&gt;

&lt;p&gt;The week started with a sync with my mentor to get the implementation plan nailed down. The good news is, the week-by-week plan has been finalized after a proper discussion, and the interpretation of &lt;strong&gt;Issue #87&lt;/strong&gt; has been confirmed. With the plan locked in, I can move forward with confidence in the coming weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deciding the Target Architecture
&lt;/h2&gt;

&lt;p&gt;One of the bigger decisions this week was figuring out &lt;em&gt;where&lt;/em&gt; everything actually lives, contributor onboarding, GitHub integration, credential issuance, DID management, and verification policy logic all need a home.&lt;/p&gt;

&lt;p&gt;Here's what we landed on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;heka-identity-service&lt;/code&gt;&lt;/strong&gt; — This is where the core modules live: contributor onboarding, credential issuance, DID management, and verification policy logic. All of this gets built as a dedicated module within this repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A separate GitHub repository&lt;/strong&gt; — The GitHub integration code (the Probot app, webhook handling, check-run posting) lives in its own repo, completely separate from &lt;code&gt;heka-identity-service&lt;/code&gt;. This keeps Heka clean and focused on identity, and makes the GitHub-specific code easier to extend or extract later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are still a few grey areas to iron out over the next couple of weeks, but the overall direction is clear and agreed upon.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding Hedera: Consensus Nodes vs Mirror Nodes
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you're already familiar with Hedera's architecture, feel free to skip ahead to the next section!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before I explain how Hedera fits into this project, let me share something I was confused about early on: &lt;strong&gt;what's the difference between a consensus node and a mirror node?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike older blockchains (like Ethereum) that use a single node type for everything, Hedera splits the workload into two completely different node types to keep the network fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Consensus Node — The Write Database
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;This is where you send new transactions (like creating an account or logging a DID document to the ledger).&lt;/li&gt;
&lt;li&gt;Its job is to receive the transaction, verify it, agree on a timestamp, and write it to the ledger as fast as possible.&lt;/li&gt;
&lt;li&gt;Because it's hyper-focused on speed and new writes, it's not great at answering questions about old or historical data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Mirror Node — The Read Database
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The mirror node sits quietly alongside the consensus node and constantly copies (mirrors) every approved transaction.&lt;/li&gt;
&lt;li&gt;Its only job is to take that raw blockchain data and organize it into a highly searchable traditional database (like PostgreSQL), and expose a REST API that developers can query.&lt;/li&gt;
&lt;li&gt;Important caveat: you &lt;strong&gt;cannot send new transactions&lt;/strong&gt; to the mirror node — it's strictly read-only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How This Applies to Our Project
&lt;/h3&gt;

&lt;p&gt;In practice, the two node types serve two completely different purposes in our system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writing (creating/updating a DID):&lt;/strong&gt; Our backend uses the &lt;strong&gt;consensus node&lt;/strong&gt;, authorized via operator credentials from our &lt;code&gt;.env&lt;/code&gt;. The SDK signs the transaction automatically using the operator key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading (showing transaction history or verifying an identity):&lt;/strong&gt; The frontend makes a standard HTTP GET request directly to the &lt;strong&gt;mirror node's REST API&lt;/strong&gt; — completely bypassing the consensus node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our &lt;code&gt;.env&lt;/code&gt; will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HEDERA_NETWORK=
OPERATOR_ID=
OPERATOR_KEY=
NODE_IP=
MIRROR_NODE_IP=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  HBAR Balance &amp;amp; Revocation Strategy
&lt;/h2&gt;

&lt;p&gt;After clarifying with my mentor Alexander, I now have the credentials for the operator account. The account comes loaded with enough test HBAR tokens to cover everything I need during development — creating, updating, and querying DIDs. So the token balance won't be a blocker at all.&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;revocation front&lt;/strong&gt;: implementing a full, hardened VC revocation mechanism is out of scope for this mentorship. The focus right now is on building a working prototype, so revocation will either be tackled in the second half of the mentorship or saved for post-mentorship work. For now, the expectation is lightweight documentation and runbook-level treatment — not a working mechanism.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up the Local Dev Environment
&lt;/h2&gt;

&lt;p&gt;This was the most hands-on part of the week. Getting the local Heka environment running required wiring together three tools: &lt;strong&gt;Nginx&lt;/strong&gt;, &lt;strong&gt;Ngrok&lt;/strong&gt;, and a &lt;strong&gt;GitHub App&lt;/strong&gt;. Let me walk through how they fit together.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Nginx Routing Rules
&lt;/h3&gt;

&lt;p&gt;Nginx listens on &lt;strong&gt;Port 8080&lt;/strong&gt; and applies two simple routing rules to every incoming request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rule 1: Route GitHub Webhooks → Probot GitHub App (Port 3000)&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/webhook&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Rule 2: Route everything else → Heka Core Backend (Port 3001)&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain English:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the request is to &lt;code&gt;/api/webhook&lt;/code&gt; → forward it to the GitHub App on Port 3000&lt;/li&gt;
&lt;li&gt;Everything else → forward it to the main Heka backend on Port 3001&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Ngrok Is Necessary
&lt;/h3&gt;

&lt;p&gt;Here's the catch: Nginx only listens on localhost. But when GitHub fires a webhook event, "localhost" from GitHub's perspective means &lt;em&gt;GitHub's own servers&lt;/em&gt; — not your machine. It would send the event into the void, find nothing, and drop it silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ngrok&lt;/strong&gt; solves this by creating a public tunnel from your local Port 8080 directly to the internet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ngrok gives you a temporary public URL — something like &lt;code&gt;https://something.ngrok-free.app&lt;/code&gt;. You paste this into your GitHub App's webhook URL settings. Now the entire flow works end-to-end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub fires a PR event
        ↓
Hits the public Ngrok URL
        ↓
Ngrok tunnels it to Port 8080 on your local machine
        ↓
Nginx reads the request and applies the two routing rules
        ↓
Correct service handles it (Port 3000 or 3001)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A one-liner to remember the whole pipeline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GitHub labels it and sends it → Ngrok blindly transports it → Nginx reads it and diverts it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Webhook Fixture Repository
&lt;/h2&gt;

&lt;p&gt;To avoid opening real pull requests every single time I want to test webhook handling, I created a dedicated fixture repository for generating and replaying PR events during development.&lt;/p&gt;

&lt;p&gt;You can find it here: &lt;a href="https://github.com/darshit2308/Heka-Webhook-Fixture" rel="noopener noreferrer"&gt;darshit2308/Heka-Webhook-Fixture&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The PR Slicing Plan
&lt;/h2&gt;

&lt;p&gt;To keep development incremental and mentor-reviewable, I've planned out the PR delivery sequence. This is tentative for now, but here's the rough order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ADRs and credential profile documentation&lt;/li&gt;
&lt;li&gt;Deterministic DID verification method helper and Hedera secret validation&lt;/li&gt;
&lt;li&gt;GPG challenge lifecycle module with tests&lt;/li&gt;
&lt;li&gt;GitHub App adapter skeleton and check-run creation &lt;em&gt;(separate repo from Heka, per mentor direction)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;GitHub OAuth and contributor binding&lt;/li&gt;
&lt;li&gt;Contributor credential OID4VCI support&lt;/li&gt;
&lt;li&gt;Heka Web Wallet prototype &lt;em&gt;(lives inside the Heka repository; non-Askar key management)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Heka Web UI onboarding flow&lt;/li&gt;
&lt;li&gt;Repository config parser and policy schema&lt;/li&gt;
&lt;li&gt;OID4VP verification integration &lt;em&gt;(Web Wallet as the primary target)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Lifecycle and recovery documentation &lt;em&gt;(runbooks; no revocation implementation)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;E2E test suite, docs, and final demo artifacts&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;Week 1 was heavy on planning, decisions, and setup — but that's exactly how it should be. A solid foundation now means fewer surprises (and fewer painful rewrites) later. Starting next week, the real implementation begins: ADRs, the DID helper utilities, and the first PR going out the door.&lt;/p&gt;

&lt;p&gt;Stay tuned for Week 2!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of my ongoing LFDT Mentorship 2026 blog series. Feel free to reach out if you have questions or want to discuss anything covered here!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>devjournal</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Starting My LFX Mentorship Journey: Building Contributor Identity Verification for Hiero</title>
      <dc:creator>Darshit Khandelwal</dc:creator>
      <pubDate>Sun, 14 Jun 2026 19:25:09 +0000</pubDate>
      <link>https://dev.to/darshit2308/starting-my-lfx-mentorship-journey-building-contributor-identity-verification-for-hiero-583</link>
      <guid>https://dev.to/darshit2308/starting-my-lfx-mentorship-journey-building-contributor-identity-verification-for-hiero-583</guid>
      <description>&lt;p&gt;Today, June 15th, 2026, I officially start my 6-month LFX mentorship under the Linux Foundation Decentralized Trust (LFDT) program. I will be working on a project called Hiero Contributor Identity Verification Prototype, under the guidance of my mentor Alexander Shenshin.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;## What am I building, and why does it matter? *&lt;/em&gt;&lt;br&gt;
In open-source projects, anyone can open a pull request. But how does a project maintainer know that the person submitting code is actually who they claim to be? Right now, most projects simply trust GitHub usernames, which can be faked, compromised, or shared.&lt;br&gt;
This project solves that by building a real identity verification layer on top of GitHub's contribution workflow. When a contributor opens a pull request, instead of just trusting their username, the system cryptographically verifies their identity using:&lt;/p&gt;

&lt;p&gt;Decentralized Identifiers (DIDs) anchored on the Hedera blockchain&lt;br&gt;
Verifiable Credentials issued through the OID4VCI standard&lt;br&gt;
GPG cryptographic signatures tied to their GitHub account&lt;br&gt;
Verifiable Presentations verified at pull request time through OID4VP&lt;/p&gt;

&lt;p&gt;In simple terms, a contributor proves they are who they say they are, once, and that proof travels with every pull request they open, automatically. (There is another alternative, that is, instead of storing the proof, we can create the proof of user's validity on dynamically, after every Pull Request opened. Although, this is an open question and would be clarified by the end of week-1).&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;## What will I be documenting here? *&lt;/em&gt;&lt;br&gt;
I plan to post weekly updates throughout this mentorship. Each post will cover what I built that week, the architectural decisions I made and, more importantly, why I made them, and what I learned or found difficult.&lt;br&gt;
Specifically, I will be tracking:&lt;/p&gt;

&lt;p&gt;1) Weekly progress updates on implementation&lt;br&gt;
2) Architecture Decision Records explaining key technical choices&lt;br&gt;
3) Demo videos after every major milestone&lt;br&gt;
4) Honest reflections on blockers, mistakes, and lessons learned&lt;/p&gt;

&lt;p&gt;I have some experience working with decentralized identity standards like OID4VCI and OID4VP from my previous projects, so i will try to explain the concepts too in as much detail as possible, to help the community. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## How can these posts be useful to you&lt;/strong&gt;&lt;br&gt;
These posts can be a genuine learning chance for individuals who are thinking to study concepts like DIDs, and dive into blockchain.&lt;br&gt;
If you are working on decentralized identity, Hedera, open-source tooling, or LFX mentorship yourself, you can follow along. &lt;/p&gt;

&lt;p&gt;Thanks a lot for reading until here!!&lt;br&gt;
We can connect here: &lt;br&gt;
linkedin: &lt;a href="https://www.linkedin.com/in/darshit-khandelwal-49bb25288" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/darshit-khandelwal-49bb25288&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/darshit2308" rel="noopener noreferrer"&gt;https://github.com/darshit2308&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
