For the narrowed public repo-level GitHub replacement scope, see REPO_LEVEL_MVP_PLAN.md. That plan is the guardrail and E2E MVP contract for repo, branch, issue, pull request, and moderation work before CI, private repos, organization/team permissions, or broad GitHub API compatibility.
- Problem Statement
- Prior Art
- Design Principles
- Protocol Architecture
- 4.1 Repository Protocol
- 4.2 Issues Protocol
- 4.3 Patches Protocol
- 4.4 CI/CD Protocol
- 4.5 Releases Protocol
- 4.6 Package Registry Protocol
- 4.7 Social Protocol
- 4.8 Notifications Protocol
- 4.9 Wiki Protocol
- 4.10 Organization Protocol
- DID-Addressed Git Remotes
- Decentralized Bundle Storage
- DID-Scoped Package Registry
- Namespace-Based Contribution Model
- Indexer Integration
- Identity & Access Control
- Technical Challenges & Mitigations
- Implementation Roadmap
- Directory Structure
Git is decentralized. GitHub is not.
GitHub centralizes five layers on top of git that don't need to be centralized:
| Layer | GitHub's approach | gitd approach |
|---|---|---|
| Identity | GitHub usernames, bound to github.com | DIDs (self-sovereign, portable) |
| Access control | GitHub org/team/collaborator ACLs | DWN protocol roles with cryptographic auth |
| Social artifacts | Issues, PRs, reviews stored on GitHub servers | DWN records on the repo owner's DWN |
| Package hosting | npm/GitHub Packages, global namespace | DID-scoped packages, $immutable guarantees |
| Discovery | GitHub search, trending, explore | Indexer services (decentralized, competitive) |
The goal: replace GitHub's centralized social layer with DWN protocols, while keeping git's native transport for code. Every user owns their data. Every contribution is cryptographically signed. No single point of failure or control.
The most mature attempt at decentralizing the forge.
Architecture: All social artifacts (issues, patches/PRs) are stored inside the Git repo as "Collaborative Objects" (COBs) under refs/cobs/. COBs are CRDTs — concurrent edits form a DAG that is reduced in topological order. Identity uses did:key (Ed25519). Replication uses a custom gossip protocol over Noise XK.
What works: Storing social artifacts in Git is elegant — you get replication, integrity, and offline-first for free. The CRDT-over-DAG approach handles concurrent edits cleanly.
What doesn't work:
- No encryption at rest — only "private repos" via allow-list replication, not actual encryption
- No fine-grained access control — you seed the whole repo or you don't. No "read issues but not code" or "triage role vs. maintainer"
- Discovery relies on gossip with well-connected seed nodes. No DHT or global resolution
- No organization/team structure — individual-delegate-centric
- No package registry or CI integration at the protocol level
Federation via ActivityPub. Forgejo (Gitea fork) is implementing it.
What works: Leverages the fediverse ecosystem. Cross-server issue creation works.
What doesn't: It's federation, not decentralization. Server operators control identity and availability. If your instance goes down, your identity goes with it. Not offline-first. No encryption.
Tools that store issue/review data inside Git repositories. Validate the "social artifacts in Git" approach but don't address identity, discovery, or access control.
| Capability | Radicle | ForgeFed | DWN |
|---|---|---|---|
| Identity | did:key |
Server-bound URIs | did:dht / did:jwk (rotatable, with services) |
| Discovery | Gossip + seed nodes | WebFinger | DID resolution to DWN service endpoints |
| Access control | Delegate threshold | Server-level ACL | Protocol-level $actions with roles, per-record |
| Encryption | None | None | Record-level JWE (encryptionRequired) |
| Offline-first | Yes (Git) | No | Yes (DWN sync engine) |
| Extensibility | COB types | ActivityPub vocab | Protocol definitions (composable via uses) |
The fundamental rule: you write to your own DWN, not someone else's. Alice creates issues on her DWN tagged with Bob's repo DID. Bob's DWN only accepts writes from users he's explicitly granted roles to (maintainer, triager, contributor). This eliminates spam by design — there is no { who: 'anyone', can: ['create'] } on any writable protocol path.
External contributions (from strangers) follow one of these patterns:
- Permission grants: Bob issues a scoped, revocable grant to Alice's DID after she requests access
- Submissions inbox: A protocol path with
$recordLimitand proof-of-work, where anyone can propose but the owner curates - Indexer-mediated: Alice writes to her DWN, the indexer surfaces it to Bob, Bob "accepts" by writing a corresponding record to his own DWN
Rather than one monolithic protocol, gitd uses 11 composable protocols. Each handles one domain (repo, issues, patches, etc.) and references the others via uses for cross-protocol role authorization. This mirrors how @enbox/protocols composes ListsDefinition with SocialGraphDefinition.
$ref wrapping: The DWN SDK requires composing protocols to include a $ref node in their structure that references the foreign protocol's root type. For gitd, all 5 protocols that compose with forge-repo (issues, patches, ci, releases, wiki) wrap their top-level type inside a repo: { $ref: 'repo:repo' } node. This means protocolPaths include the repo/ prefix (e.g., 'repo/issue' instead of 'issue'). At write time, the parentContextId of the $ref child (e.g., the issue) is set to the contextId of the referenced repo record, establishing the cross-protocol link without needing a repoRecordId tag.
$immutable is used aggressively for audit trails: status changes, reviews, revisions, release assets, package versions. These records cannot be silently edited — if you need to change your mind, you create a new record. This provides the same guarantees as git's content-addressed storage, extended to social artifacts.
Git objects use Git's native transport protocol (smart HTTP). DWN handles the social layer. The two are connected by the DID — the DID document advertises both DWN endpoints and git transport endpoints. This is pragmatic: git's pack protocol is highly optimized and battle-tested. Storing git objects as DWN records would sacrifice all that performance for no real benefit.
Features that require global aggregation (star counts, cross-DWN search, trending repos, external contributions) are handled by indexer services. Indexers crawl published DWN records, build materialized views, and expose APIs. Multiple competing indexers can exist — no single indexer is authoritative. The DWN protocols support indexers in three ways:
- Read-only: Indexers query published DWN records
- Write-back: The DWN owner grants an indexer a scoped write permission to populate curated views
- Compute modules (future): Periodic computation that runs on the DWN itself
All protocols use the https://enbox.id/protocols/forge/ namespace. Each protocol definition follows the @enbox/protocols pattern: data types, SchemaMap, raw ProtocolDefinition, and typed protocol via defineProtocol().
The foundational protocol. Defines repository metadata, collaborator roles, and repo-level resources.
export const ForgeRepoDefinition = {
protocol : 'https://enbox.id/protocols/forge/repo',
published : true,
types : {
repo : { schema: 'https://enbox.id/schemas/forge/repo', dataFormats: ['application/json'] },
settings : { schema: 'https://enbox.id/schemas/forge/settings', dataFormats: ['application/json'] },
readme : { dataFormats: ['text/markdown', 'text/plain'] },
license : { dataFormats: ['text/plain'] },
bundle : { dataFormats: ['application/x-git-bundle'] },
maintainer : { schema: 'https://enbox.id/schemas/forge/collaborator', dataFormats: ['application/json'] },
triager : { schema: 'https://enbox.id/schemas/forge/collaborator', dataFormats: ['application/json'] },
contributor : { schema: 'https://enbox.id/schemas/forge/collaborator', dataFormats: ['application/json'] },
topic : { schema: 'https://enbox.id/schemas/forge/topic', dataFormats: ['application/json'] },
webhook : { schema: 'https://enbox.id/schemas/forge/webhook', dataFormats: ['application/json'], encryptionRequired: true },
},
structure: {
repo: {
$recordLimit : { max: 1, strategy: 'reject' },
$actions : [{ who: 'anyone', can: ['read'] }],
$tags: {
$requiredTags : ['name', 'visibility'],
$allowUndefinedTags : false,
name : { type: 'string', maxLength: 100 },
visibility : { type: 'string', enum: ['public', 'private'] },
defaultBranch : { type: 'string' },
language : { type: 'string' },
archived : { type: 'boolean' },
},
maintainer: {
$role : true,
$actions : [{ who: 'anyone', can: ['read'] }],
$tags : { $requiredTags: ['did'], $allowUndefinedTags: false, did: { type: 'string' } },
},
triager: {
$role : true,
$actions : [{ who: 'anyone', can: ['read'] }],
$tags : { $requiredTags: ['did'], $allowUndefinedTags: false, did: { type: 'string' } },
},
contributor: {
$role : true,
$actions : [{ who: 'anyone', can: ['read'] }],
$tags : { $requiredTags: ['did'], $allowUndefinedTags: false, did: { type: 'string' } },
},
readme: {
$recordLimit : { max: 1, strategy: 'reject' },
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'repo/maintainer', can: ['create', 'update'] },
],
},
license: {
$recordLimit : { max: 1, strategy: 'reject' },
$actions : [{ who: 'anyone', can: ['read'] }],
},
topic: {
$actions: [
{ who: 'anyone', can: ['read'] },
{ role: 'repo/maintainer', can: ['create', 'delete'] },
],
$tags: { $requiredTags: ['name'], $allowUndefinedTags: false, name: { type: 'string', maxLength: 50 } },
},
bundle: {
$squash : true,
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'repo/maintainer', can: ['create', 'squash'] },
],
$tags: {
$requiredTags : ['tipCommit', 'isFull'],
$allowUndefinedTags : false,
tipCommit : { type: 'string' },
isFull : { type: 'boolean' },
refCount : { type: 'integer' },
size : { type: 'integer' },
},
},
settings: {
$recordLimit : { max: 1, strategy: 'reject' },
// Owner-only (no $actions for non-owner = owner-only)
},
webhook: {
// Owner-only, encrypted at rest (webhook secrets are sensitive)
},
},
},
} as const satisfies ProtocolDefinition;Data shapes:
export type RepoData = {
name : string;
description? : string;
defaultBranch : string;
homepage? : string;
dwnEndpoints : string[]; // DWN URLs for social artifacts
gitEndpoints? : string[]; // Git transport URLs (smart HTTP)
};
export type CollaboratorData = {
did : string;
alias? : string;
};Key decisions:
- Repo is a singleton (
$recordLimit: { max: 1 }). Updated in place for description changes. - Three role tiers: maintainer (full write), triager (issue management), contributor (patches/wiki). Mirrors GitHub's permission model.
- Webhook uses
encryptionRequired: true— webhook URLs and secrets are sensitive. - Tags enable querying — filter repos by language, visibility, archive status.
export const ForgeIssuesDefinition = {
protocol : 'https://enbox.id/protocols/forge/issues',
published : true,
uses : { repo: 'https://enbox.id/protocols/forge/repo' },
types : {
issue : { schema: 'https://enbox.id/schemas/forge/issue', dataFormats: ['application/json'] },
comment : { schema: 'https://enbox.id/schemas/forge/comment', dataFormats: ['application/json'] },
reaction : { schema: 'https://enbox.id/schemas/forge/reaction', dataFormats: ['application/json'] },
label : { schema: 'https://enbox.id/schemas/forge/label', dataFormats: ['application/json'] },
statusChange : { schema: 'https://enbox.id/schemas/forge/status-change', dataFormats: ['application/json'] },
assignment : { schema: 'https://enbox.id/schemas/forge/assignment', dataFormats: ['application/json'] },
},
structure: {
repo: {
$ref: 'repo:repo',
issue: {
$actions: [
{ role: 'repo:repo/contributor', can: ['create', 'read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'read', 'update', 'delete'] },
{ role: 'repo:repo/triager', can: ['create', 'read', 'co-update'] },
{ who: 'author', of: 'repo/issue', can: ['create', 'update'] },
],
$tags: {
$requiredTags : ['status'],
$allowUndefinedTags : false,
status : { type: 'string', enum: ['open', 'closed'] },
priority : { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
milestone : { type: 'string' },
},
reaction: {
$actions: [
{ role: 'repo:repo/contributor', can: ['create', 'read', 'delete'] },
{ role: 'repo:repo/maintainer', can: ['create', 'read', 'delete'] },
],
$tags: {
$requiredTags : ['emoji'],
$allowUndefinedTags : false,
emoji : { type: 'string', maxLength: 10 },
},
},
comment: {
$actions: [
{ role: 'repo:repo/contributor', can: ['create', 'read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'read', 'delete'] },
{ who: 'author', of: 'repo/issue/comment', can: ['create', 'update', 'delete'] },
],
reaction: {
$actions: [
{ role: 'repo:repo/contributor', can: ['create', 'read', 'delete'] },
{ role: 'repo:repo/maintainer', can: ['create', 'read', 'delete'] },
],
$tags: {
$requiredTags : ['emoji'],
$allowUndefinedTags : false,
emoji : { type: 'string', maxLength: 10 },
},
},
},
label: {
$immutable : true,
$actions : [
{ role: 'repo:repo/contributor', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'delete'] },
{ role: 'repo:repo/triager', can: ['create', 'delete'] },
],
$tags: {
$requiredTags : ['name', 'color'],
$allowUndefinedTags : false,
name : { type: 'string' },
color : { type: 'string' },
},
},
statusChange: {
$immutable : true,
$actions : [
{ role: 'repo:repo/contributor', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create'] },
{ role: 'repo:repo/triager', can: ['create'] },
{ who: 'author', of: 'repo/issue', can: ['create'] },
],
$tags: {
$requiredTags : ['from', 'to'],
$allowUndefinedTags : false,
from : { type: 'string', enum: ['open', 'closed'] },
to : { type: 'string', enum: ['open', 'closed'] },
},
},
assignment: {
$actions: [
{ role: 'repo:repo/contributor', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'delete'] },
{ role: 'repo:repo/triager', can: ['create', 'delete'] },
],
$tags: {
$requiredTags : ['assigneeDid'],
$allowUndefinedTags : false,
assigneeDid : { type: 'string' },
},
},
},
},
},
} as const satisfies ProtocolDefinition;Data shapes:
export type IssueData = {
title : string;
body : string; // Markdown
number : number; // Human-friendly sequential number (owner-assigned)
};
export type CommentData = { body: string };
export type StatusChangeData = { reason?: string };Key decisions:
- No
{ who: 'anyone', can: ['create'] }— only users with a contributor or maintainer role can create issues directly on the repo owner's DWN. External issue reports live on the reporter's own DWN (see Section 8). statusChangeandlabelare$immutable— these are audit events, not editable records.$refwrapping — therepo: { $ref: 'repo:repo' }node links issues to a specific repo context viaparentContextId, replacing the previousrepoRecordIdtag approach. ProtocolPaths include therepo/prefix (e.g.,'repo/issue','repo/issue/comment').- Cross-protocol roles via
repo:repo/maintainerreference the forge-repo protocol. who: 'author'paths must use the full protocolPath including therepo/prefix (e.g.,'repo/issue', not'issue').- Triager uses
co-updateinstead ofupdate— triagers are not the author, so they needco-updatefor non-author updates.
Pull requests, code review, and merge tracking.
export const ForgePatchesDefinition = {
protocol : 'https://enbox.id/protocols/forge/patches',
published : true,
uses : { repo: 'https://enbox.id/protocols/forge/repo' },
types : {
patch : { schema: 'https://enbox.id/schemas/forge/patch', dataFormats: ['application/json'] },
revision : { schema: 'https://enbox.id/schemas/forge/revision', dataFormats: ['application/json'] },
review : { schema: 'https://enbox.id/schemas/forge/review', dataFormats: ['application/json'] },
reviewComment : { schema: 'https://enbox.id/schemas/forge/review-comment', dataFormats: ['application/json'] },
statusChange : { schema: 'https://enbox.id/schemas/forge/patch-status-change', dataFormats: ['application/json'] },
mergeResult : { schema: 'https://enbox.id/schemas/forge/merge-result', dataFormats: ['application/json'] },
},
structure: {
repo: {
$ref: 'repo:repo',
patch: {
$actions: [
{ role: 'repo:repo/contributor', can: ['create', 'read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'read', 'update', 'delete'] },
{ who: 'author', of: 'repo/patch', can: ['create', 'update'] },
],
$tags: {
$requiredTags : ['status', 'baseBranch'],
$allowUndefinedTags : false,
status : { type: 'string', enum: ['draft', 'open', 'closed', 'merged'] },
baseBranch : { type: 'string' },
headBranch : { type: 'string' },
sourceDid : { type: 'string' }, // For cross-DWN patches: author's DID
},
revision: {
$immutable : true,
$actions : [
{ role: 'repo:repo/contributor', can: ['read'] },
{ who: 'author', of: 'repo/patch', can: ['create'] },
],
$tags: {
$requiredTags : ['headCommit', 'baseCommit'],
$allowUndefinedTags : false,
headCommit : { type: 'string' },
baseCommit : { type: 'string' },
commitCount : { type: 'integer', minimum: 1 },
},
},
review: {
$immutable : true,
$actions : [
{ role: 'repo:repo/contributor', can: ['create', 'read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'read'] },
],
$tags: {
$requiredTags : ['verdict'],
$allowUndefinedTags : false,
verdict : { type: 'string', enum: ['approve', 'reject', 'comment'] },
revisionRecordId : { type: 'string' },
},
reviewComment: {
$actions: [
{ role: 'repo:repo/contributor', can: ['create', 'read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'read', 'update', 'delete'] },
{ who: 'author', of: 'repo/patch/review/reviewComment', can: ['create', 'update', 'delete'] },
],
$tags: {
$allowUndefinedTags : true,
path : { type: 'string' },
line : { type: 'integer' },
side : { type: 'string', enum: ['left', 'right'] },
},
},
},
statusChange: {
$immutable : true,
$actions : [
{ role: 'repo:repo/contributor', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create'] },
{ who: 'author', of: 'repo/patch', can: ['create'] },
],
},
mergeResult: {
$immutable : true,
$recordLimit : { max: 1, strategy: 'reject' },
$actions : [
{ role: 'repo:repo/contributor', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create'] },
],
$tags: {
$requiredTags : ['mergeCommit', 'strategy'],
$allowUndefinedTags : false,
mergeCommit : { type: 'string' },
strategy : { type: 'string', enum: ['merge', 'squash', 'rebase'] },
},
},
},
},
},
} as const satisfies ProtocolDefinition;Data shapes:
export type PatchData = {
title : string;
body : string; // Markdown
number : number;
};
export type RevisionData = {
description? : string; // "Addressed review feedback"
diffStat : { additions: number; deletions: number; filesChanged: number };
};
export type ReviewData = { body?: string };
export type ReviewCommentData = {
body : string;
diffHunk? : string; // Diff hunk context for rendering
};
export type MergeResultData = { mergedBy: string };Key decisions:
revisionis$immutable— each force-push creates a new revision. Reviewers need to see what they reviewed.reviewis$immutable— verdicts cannot be silently changed. Submit a new review to change your mind.mergeResulthas$recordLimit: { max: 1 }— a patch can only be merged once.reviewCommentsupports inline code review viapath/line/sidetags.- Cross-DWN patches use
sourceDidto indicate where the branch lives (the fork author's DWN).
export const ForgeCiDefinition = {
protocol : 'https://enbox.id/protocols/forge/ci',
published : true,
uses : { repo: 'https://enbox.id/protocols/forge/repo' },
types : {
checkSuite : { schema: 'https://enbox.id/schemas/forge/check-suite', dataFormats: ['application/json'] },
checkRun : { schema: 'https://enbox.id/schemas/forge/check-run', dataFormats: ['application/json'] },
artifact : { dataFormats: ['application/octet-stream', 'application/gzip', 'application/zip'] },
},
structure: {
repo: {
$ref: 'repo:repo',
checkSuite: {
$actions: [
{ role: 'repo:repo/contributor', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'update', 'delete'] },
],
$tags: {
$requiredTags : ['commitSha', 'status'],
$allowUndefinedTags : false,
commitSha : { type: 'string' },
status : { type: 'string', enum: ['queued', 'in_progress', 'completed'] },
conclusion : { type: 'string', enum: ['success', 'failure', 'cancelled', 'skipped'] },
branch : { type: 'string' },
},
checkRun: {
$actions: [
{ role: 'repo:repo/contributor', can: ['read'] },
{ who: 'author', of: 'repo/checkSuite', can: ['create', 'update', 'delete'] },
],
$tags: {
$requiredTags : ['name', 'status'],
$allowUndefinedTags : false,
name : { type: 'string' },
status : { type: 'string', enum: ['queued', 'in_progress', 'completed'] },
conclusion : { type: 'string', enum: ['success', 'failure', 'cancelled', 'skipped'] },
},
artifact: {
$actions : [
{ role: 'repo:repo/contributor', can: ['read'] },
{ who: 'author', of: 'repo/checkSuite', can: ['create', 'delete'] },
],
$tags: {
$requiredTags : ['name'],
$allowUndefinedTags : false,
name : { type: 'string' },
size : { type: 'integer' },
contentType : { type: 'string' },
digest : { type: 'string' },
expired : { type: 'boolean' },
expiresAt : { type: 'string' },
},
$size: { max: 104857600 }, // 100MB per artifact
},
},
},
},
},
} as const satisfies ProtocolDefinition;Key insight: CI bots are DID-bearing agents. A CI provider has its own DID and is added as a maintainer to the repo. The bot writes checkSuite and checkRun records. This is analogous to GitHub Apps with installation-scoped permissions.
Check runs are mutable (status transitions: queued -> in_progress -> completed). This is one of the few places where mutable records are needed.
export const ForgeReleasesDefinition = {
protocol : 'https://enbox.id/protocols/forge/releases',
published : true,
uses : { repo: 'https://enbox.id/protocols/forge/repo' },
types : {
release : { schema: 'https://enbox.id/schemas/forge/release', dataFormats: ['application/json'] },
asset : { dataFormats: ['application/octet-stream', 'application/gzip', 'application/zip', 'application/x-tar'] },
signature : { dataFormats: ['application/pgp-signature', 'application/json'] },
},
structure: {
repo: {
$ref: 'repo:repo',
release: {
$actions: [
{ who: 'anyone', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'update', 'delete'] },
],
$tags: {
$requiredTags : ['tagName'],
$allowUndefinedTags : false,
tagName : { type: 'string' },
commitSha : { type: 'string' },
prerelease : { type: 'boolean' },
draft : { type: 'boolean' },
},
asset: {
$immutable : true, // Published binaries must not be silently replaced (supply chain security)
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'delete'] },
],
$tags: {
$requiredTags : ['filename', 'contentType'],
$allowUndefinedTags : false,
filename : { type: 'string' },
contentType : { type: 'string' },
size : { type: 'integer' },
},
},
signature: {
$immutable : true,
$recordLimit : { max: 1, strategy: 'reject' },
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create'] },
],
},
},
},
},
} as const satisfies ProtocolDefinition;$immutable on asset is critical for supply chain security. Once a release binary is published, its data cannot be mutated. Deletion is still possible (via $actions), but the content integrity is guaranteed while the record exists.
export const ForgeRegistryDefinition = {
protocol : 'https://enbox.id/protocols/forge/registry',
published : true,
types : {
package : { schema: 'https://enbox.id/schemas/forge/package', dataFormats: ['application/json'] },
version : { schema: 'https://enbox.id/schemas/forge/package-version', dataFormats: ['application/json'] },
tarball : { dataFormats: ['application/gzip', 'application/octet-stream'] },
attestation : { schema: 'https://enbox.id/schemas/forge/attestation', dataFormats: ['application/json'] },
},
structure: {
package: {
$actions: [{ who: 'anyone', can: ['read'] }],
// Only owner can create packages (no create in $actions = owner-only)
$tags: {
$requiredTags : ['name', 'ecosystem'],
$allowUndefinedTags : false,
name : { type: 'string', maxLength: 214 },
ecosystem : { type: 'string', enum: ['npm', 'cargo', 'pip', 'go'] },
description : { type: 'string' },
},
version: {
$immutable : true, // Once published, version metadata is permanent
$actions : [
{ who: 'anyone', can: ['read'] },
{ who: 'author', of: 'package', can: ['create'] },
],
$tags: {
$requiredTags : ['semver'],
$allowUndefinedTags : false,
semver : { type: 'string' },
deprecated : { type: 'boolean' },
},
tarball: {
$immutable : true, // Package bytes are permanent
$recordLimit : { max: 1, strategy: 'reject' },
$actions : [
{ who: 'anyone', can: ['read'] },
{ who: 'author', of: 'package', can: ['create'] },
],
},
attestation: {
$immutable : true,
$actions : [
{ who: 'anyone', can: ['read'] },
// Third-party attestors need a permission grant from the package owner
],
},
},
},
},
} as const satisfies ProtocolDefinition;Advantages over npm:
- No registry takeover: the publisher's DID owns the package. No one can unpublish your package from your DWN.
- No name squatting: names are scoped to DIDs.
did:dht:alice/utilsanddid:dht:bob/utilscoexist. - Cryptographic provenance: every version is signed by the publisher's DID. No separate signing ceremony.
$immutableguarantees: published versions cannot be mutated. The protocol enforces it.
export const ForgeSocialDefinition = {
protocol : 'https://enbox.id/protocols/forge/social',
published : true,
types : {
star : { schema: 'https://enbox.id/schemas/forge/star', dataFormats: ['application/json'] },
follow : { schema: 'https://enbox.id/schemas/forge/follow', dataFormats: ['application/json'] },
activity : { schema: 'https://enbox.id/schemas/forge/activity', dataFormats: ['application/json'] },
},
structure: {
star: {
// Stars live on the STARRER's DWN, not the repo owner's.
// Owner-only (you star repos on your own DWN)
$tags: {
$requiredTags : ['repoDid', 'repoRecordId'],
$allowUndefinedTags : false,
repoDid : { type: 'string' },
repoRecordId : { type: 'string' },
},
},
follow: {
// Follows live on the FOLLOWER's DWN
$tags: {
$requiredTags : ['targetDid'],
$allowUndefinedTags : false,
targetDid : { type: 'string' },
},
},
activity: {
// Activity feed on the actor's DWN
$actions: [{ who: 'anyone', can: ['read'] }],
$tags: {
$requiredTags : ['type'],
$allowUndefinedTags : true,
type : {
type : 'string',
enum : ['push', 'issue_open', 'issue_close', 'patch_open', 'patch_merge', 'release', 'star', 'fork'],
},
},
},
},
} as const satisfies ProtocolDefinition;Key insight: Stars and follows live on the actor's DWN, not the target's. This means "how many stars does this repo have?" requires an indexer to aggregate across DWNs. This is a deliberate trade-off: data sovereignty over convenience. Indexers solve the aggregation problem (see Section 9).
export const ForgeNotificationsDefinition = {
protocol : 'https://enbox.id/protocols/forge/notifications',
published : false, // Private — notifications are personal
types : {
notification: { schema: 'https://enbox.id/schemas/forge/notification', dataFormats: ['application/json'] },
},
structure: {
notification: {
// Only the DWN owner can read/update/delete.
// Trusted agents (maintainers of repos you contribute to) get permission grants to create.
$tags: {
$requiredTags : ['type', 'read'],
$allowUndefinedTags : true,
type : {
type : 'string',
enum : ['mention', 'review_request', 'assignment', 'ci_failure', 'patch_merged', 'issue_comment', 'review'],
},
read : { type: 'boolean' },
repoDid : { type: 'string' },
repoRecordId : { type: 'string' },
sourceRecordId : { type: 'string' },
},
},
},
} as const satisfies ProtocolDefinition;published: false — notifications are private to the owner. Notification senders (e.g., the repo's DWN when someone is mentioned) need a scoped permission grant.
export const ForgeWikiDefinition = {
protocol : 'https://enbox.id/protocols/forge/wiki',
published : true,
uses : { repo: 'https://enbox.id/protocols/forge/repo' },
types : {
page : { schema: 'https://enbox.id/schemas/forge/wiki-page', dataFormats: ['text/markdown'] },
pageHistory : { schema: 'https://enbox.id/schemas/forge/wiki-history', dataFormats: ['application/json'] },
},
structure: {
repo: {
$ref: 'repo:repo',
page: {
$actions: [
{ who: 'anyone', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create', 'update', 'delete'] },
{ role: 'repo:repo/contributor', can: ['create', 'update'] },
],
$tags: {
$requiredTags : ['slug', 'title'],
$allowUndefinedTags : false,
slug : { type: 'string' },
title : { type: 'string' },
},
pageHistory: {
$immutable : true,
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'repo:repo/maintainer', can: ['create'] },
{ role: 'repo:repo/contributor', can: ['create'] },
],
},
},
},
},
} as const satisfies ProtocolDefinition;Wiki pages are mutable (updated in place), but each edit creates an $immutable pageHistory record capturing the diff.
export const ForgeOrgDefinition = {
protocol : 'https://enbox.id/protocols/forge/org',
published : true,
types : {
org : { schema: 'https://enbox.id/schemas/forge/org', dataFormats: ['application/json'] },
owner : { schema: 'https://enbox.id/schemas/forge/org-member', dataFormats: ['application/json'] },
member : { schema: 'https://enbox.id/schemas/forge/org-member', dataFormats: ['application/json'] },
team : { schema: 'https://enbox.id/schemas/forge/team', dataFormats: ['application/json'] },
teamMember : { schema: 'https://enbox.id/schemas/forge/team-member', dataFormats: ['application/json'] },
},
structure: {
org: {
$recordLimit : { max: 1, strategy: 'reject' },
$actions : [{ who: 'anyone', can: ['read'] }],
owner: {
$role : true,
$actions : [{ who: 'anyone', can: ['read'] }],
$tags : { $requiredTags: ['did'], $allowUndefinedTags: false, did: { type: 'string' } },
},
member: {
$role : true,
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'org/owner', can: ['create', 'delete'] },
],
$tags: { $requiredTags: ['did'], $allowUndefinedTags: false, did: { type: 'string' } },
},
team: {
$actions: [
{ who: 'anyone', can: ['read'] },
{ role: 'org/owner', can: ['create', 'update', 'delete'] },
],
teamMember: {
$role : true,
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'org/owner', can: ['create', 'delete'] },
],
$tags: { $requiredTags: ['did'], $allowUndefinedTags: false, did: { type: 'string' } },
},
},
},
},
} as const satisfies ProtocolDefinition;Organizations are DIDs themselves. An org DID installs the forge protocols and manages repos. Teams provide sub-organization grouping with their own role scoping.
A git-remote-helper (named git-remote-did, invoked by Git when it sees the did:// scheme):
Step 1 — DID Resolution:
did:dht:abc123
-> resolve DID document
-> find service of type "DecentralizedWebNode"
-> find service of type "GitTransport"
-> extract endpoints
The DID document signals git capability via a dedicated service type:
{
"id": "did:dht:abc123",
"service": [
{
"id": "#dwn",
"type": "DecentralizedWebNode",
"serviceEndpoint": ["https://dwn1.example.com"]
},
{
"id": "#git",
"type": "GitTransport",
"serviceEndpoint": ["https://dwn1.example.com/git"]
}
]
}Step 2 — Locate Repository:
Query the DWN for the repo record:
RecordsQuery {
protocol : 'https://enbox.id/protocols/forge/repo',
protocolPath : 'repo',
filter : { tags: { name: '<repo-name>' } }
}
Step 3 — Git Transport (hybrid approach):
Git objects use native git smart HTTP protocol to the GitTransport endpoint. Social artifacts (issues, PRs, reviews) live in DWN records. This is pragmatic — git's pack protocol is highly optimized.
did:dht:abc123
-> /git/abc123/info/refs (smart HTTP git) <- git objects
-> /dwn (JSON-RPC DWN messages) <- social artifacts
Step 4 — Push Authentication:
git push
-> git-remote-did connects to GitTransport endpoint
-> server sends nonce challenge
-> helper signs nonce with user's DID Ed25519 key
-> server verifies: is this DID a 'repo/maintainer' for this repo?
-> if yes, accept the push via git smart HTTP
The DID is the identity for authorization. No SSH keys, no personal access tokens — just cryptographic proof of DID ownership.
While git objects stay in native git storage, branch ref pointers can be mirrored as lightweight DWN records:
// Track branch heads for subscription-based push notifications
types: {
ref: { schema: 'https://enbox.id/schemas/forge/git-ref', dataFormats: ['application/json'] }
}
// Data: { name: "refs/heads/main", target: "abc123def...", type: "branch" }This gives DWN's subscription system visibility into branch updates (real-time notifications when someone pushes to main), while keeping object storage efficient.
Git objects use native git transport for performance (Section 5), but this means a commodity git host must have the bare repo on disk to serve clones. What happens when a new host has never seen the repo before — or when a host restarts with an empty disk?
Solution: Store git bundles as DWN records. After each push, a git bundle is created and synced to the repo owner's DWN. Any host can reconstruct the bare repo by reading these bundle records from the DWN.
The bundle type is defined in ForgeRepoProtocol with $squash: true:
bundle: {
$squash : true,
$actions : [
{ who: 'anyone', can: ['read'] },
{ role: 'repo/maintainer', can: ['create', 'squash'] },
],
$tags: {
tipCommit : { type: 'string' }, // SHA of the tip commit
isFull : { type: 'boolean' }, // Full bundle (all refs) vs incremental
refCount : { type: 'integer' }, // Number of refs in the bundle
size : { type: 'integer' }, // Bundle file size in bytes
},
}Bundle records store binary application/x-git-bundle data. Tags provide queryable metadata without reading the bundle content.
After a successful git push, the bundle syncer (createBundleSyncer()) runs:
- Query existing bundles — check if any full or incremental bundles exist
- First push: create a full bundle (
git bundle create --all) containing all refs and all reachable objects - Subsequent pushes: create an incremental bundle containing only objects reachable from current refs but not from the last tip commit
- Every N pushes (default 5): create a squash bundle — a full bundle written with
squash: true, which atomically purges all older bundle records via DWN's$squashmechanism
The squash mechanism leverages DWN's $squash feature: a squash write creates a new record and atomically deletes all older siblings at the same protocol path and context. This prevents unbounded bundle accumulation without requiring manual garbage collection.
When a clone/fetch request arrives for a repo that doesn't exist on disk, the onRepoNotFound callback triggers restoreFromBundles():
- Query the owner's DWN for the most recent full bundle (
isFull: true, sorted bydateCreateddescending) - Clone from the full bundle —
git clone --bare <bundle-file>creates the bare repo - Apply incremental bundles — fetch all incrementals newer than the full bundle, in chronological order
- Serve the clone — the restored bare repo is now ready for git smart HTTP
This enables a fully stateless git host — repos are restored on-demand from DWN records. Host restarts, horizontal scaling, and disaster recovery all work automatically.
Bundle encryption follows repo visibility:
- Public repos: bundles are written unencrypted. They can be replicated via IPFS, cached at CDN edges, and read by anyone without DWN credentials. This maximizes availability and replication.
- Private repos: bundles are JWE-encrypted using the repo owner's X25519 key agreement key. Only the owner (and DID holders with appropriate key material) can decrypt. The protocol must be installed with
encryption: trueto inject$encryptionkeys.
The writer decides encryption per-write based on the visibility tag on the repo record. There is no separate "private bundle" type — it's the same bundle path with or without JWE wrapping.
Bundles use git's native git bundle format (application/x-git-bundle). This is self-describing, leverages all of git's delta compression and packfile optimizations, and is natively understood by git clone and git fetch. No custom serialization needed.
npm install did:dht:abc123/my-package@1.0.0
A custom npm resolver:
- Resolve DID:
did:dht:abc123-> DWN endpoints - Query package:
RecordsQuery { protocol: 'forge/registry', protocolPath: 'package', tags: { name: 'my-package', ecosystem: 'npm' } } - Query version:
RecordsQuery { protocolPath: 'package/version', parentId: <package-record-id>, tags: { semver: '1.0.0' } } - Fetch tarball:
RecordsRead { protocolPath: 'package/version/tarball', parentId: <version-record-id> }
Every DWN record is already signed by the author's DID. The tarball record's authorization field proves the publisher's identity cryptographically.
For additional verification, attestation records allow third-party co-signing:
{
attestorDid : 'did:dht:build-service-xyz',
claim : 'reproducible-build',
sourceCommit : 'abc123...',
sourceRepo : 'did:dht:abc123/repo-record-id',
}{
"dependencies": {
"did:dht:abc123/utils": "^1.0.0",
"did:dht:def456/crypto-lib": "~2.3.0"
}
}The resolver builds a trust chain: resolve each DID, verify tarball signatures match the DID, check attestation records for build reproducibility. The entire chain is verifiable without any central authority.
To enable native tooling (npm, go, docker) to consume DID-scoped packages without custom plugins, gitd provides local proxy shims that speak the native protocol of each tool:
npm registry shim — Implements the npm registry HTTP API. npm/bun/yarn/pnpm see a standard registry and install DID-scoped packages:
gitd shim npm --port 4873
npm install --registry=http://localhost:4873 @did:dht:abc123/my-pkg@1.0.0The DID is embedded in the npm scope: @did:dht:abc123 → DID did:dht:abc123. The shim maps npm API calls to DWN queries:
GET /@scope/name→ packument (all versions, dist-tags, tarball URLs)GET /@scope/name/version→ version-specific metadataGET /-/@scope/name/-/name-version.tgz→ tarball download from DWN
Go module proxy shim — Implements the GOPROXY protocol. Go tools use standard go get:
gitd shim go --port 4874
GOPROXY=http://localhost:4874 go get did.enbox.id/did:dht:abc123/my-mod@v1.0.0Module paths use did.enbox.id/ as a virtual domain prefix. The shim generates go.mod files with DID-scoped dependencies mapped to did.enbox.id/ paths:
GET /{module}/@v/list→ version listingGET /{module}/@v/{ver}.info→ version JSON (Version + Time)GET /{module}/@v/{ver}.mod→ generated go.modGET /{module}/@v/{ver}.zip→ module archive from DWNGET /{module}/@latest→ latest version info
OCI/Docker registry shim — Implements the OCI Distribution Spec v2. Docker/Podman pull images:
gitd shim oci --port 5555
docker pull localhost:5555/did:dht:abc123/my-image:v1.0.0Container images use the registry protocol with ecosystem: 'oci'. OCI manifests are stored as tarball records. The shim serves:
GET /v2/→ API version checkGET /v2/{name}/tags/list→ tag listingGET /v2/{name}/manifests/{ref}→ manifest by tag or digest (with SHA-256 content digest)HEAD /v2/{name}/manifests/{ref}→ existence checkGET /v2/{name}/blobs/{digest}→ blob download
All shims are read-only proxy servers that translate from their ecosystem's native protocol to DWN queries using the resolver module. They inherit all DWN guarantees: cryptographic provenance, immutability, and decentralized hosting.
GitHub allows anyone to open an issue or PR on any public repo. In DWN, unrestricted write access to someone else's DWN is a security risk (spam, abuse, storage exhaustion).
Alice's DWN Bob's DWN (repo owner)
+-----------------------+ +-----------------------+
| issue record | | issue records |
| tagged with: | indexer | from maintainers |
| repoDid: bob | ----------> | and triagers |
| repoRecordId: xyz | surfaces | |
+-----------------------+ +-----------------------+
For trusted collaborators (maintainers, triagers, contributors): They have role records on Bob's DWN and write directly. This is the fast path — no indexer needed.
For external contributors (strangers): They write to their own DWN, tagged with the target repo's DID and recordId. An indexer surfaces these to the repo owner. The owner can:
- Accept: write a corresponding record to their own DWN, linking to the original
- Ignore: the external issue stays on the reporter's DWN but isn't visible in the repo's canonical view
- Grant access: promote the stranger to contributor role for future direct writes
For submission inboxes (optional): A protocol path with $recordLimit and proof-of-work requirements where controlled writes from non-role-holders are accepted. This is a middle ground for repos that want to be more open without being fully permissionless.
- No spam by design — you can't flood someone else's DWN
- Data sovereignty — your contributions live on your DWN, even if the repo owner ignores them
- Natural reputation — indexers can surface contributions from known/reputable DIDs first
- Graceful degradation — works without indexers (direct role-based writes), indexers just improve discovery
Indexers are services that crawl published DWN records and build materialized views. They solve aggregation problems that don't have a protocol-level answer.
Pattern 1 — Read-only indexing: The indexer queries published (published: true) DWN records. No write access needed. Covers: repo discovery, star counts, cross-DWN search, trending repos, external contribution surfacing.
Pattern 2 — Write-back: The DWN owner grants the indexer a scoped permission to write curated views into the DWN. Example: an indexer that aggregates star counts writes a starCount record to Bob's DWN so Bob's UI can display it without querying the indexer at runtime.
Pattern 3 — Compute modules (future): Periodic computation that runs on the DWN itself. Example: every hour, a compute module counts stars from subscribed indexers and updates a local aggregate. This is a future DWN capability, not yet implemented.
| Problem | Indexer solution |
|---|---|
| "How many stars does this repo have?" | Crawl star records across DWNs, maintain count |
| "Find repos about X topic" | Crawl repo records with topic tags, build search index |
| "Show me external issues for my repo" | Crawl issue records tagged with my repo's DID |
| "Trending repos this week" | Track activity records, compute trending score |
| "Who depends on my package?" | Crawl package.json / dependency records |
Multiple competing indexers can exist — no single indexer is authoritative. Users choose which indexers to trust (similar to choosing a search engine). The DWN protocols don't depend on any specific indexer.
| Role | Protocol Path | Capabilities |
|---|---|---|
| Owner | DWN tenant (implicit) | Full control over all records |
| Maintainer | repo/maintainer ($role: true) |
Push, merge, manage issues/PRs, releases, CI |
| Triager | repo/triager ($role: true) |
Manage issues (label, assign, close), but cannot push or merge |
| Contributor | repo/contributor ($role: true) |
Create issues, submit patches, edit wiki |
| Reader | anyone with read action |
Read all published records |
The repo owner writes a role record with the collaborator's DID as recipient:
// Owner grants maintainer role to Alice
await agent.dwn.processRequest({
author : ownerDid,
target : ownerDid,
messageType : DwnInterface.RecordsWrite,
messageParams: {
protocol : 'https://enbox.id/protocols/forge/repo',
protocolPath : 'repo/maintainer',
parentContextId : repoContextId,
recipient : aliceDid,
dataFormat : 'application/json',
tags : { did: aliceDid },
},
dataStream: new Blob([JSON.stringify({ did: aliceDid, alias: 'alice' })]),
});Alice invokes this role via protocolRole: 'repo/maintainer' on subsequent operations.
- Alice creates a
reporecord on her own DWN with aforkedFromtag pointing to Bob's DID/repo - She clones the git data to her own DWN-adjacent git server
- She can push to her fork freely
- To propose changes back, she creates a
patchrecord on Bob's DWN (requires contributor role or permission grant)
Alice (contributor on Bob's repo) writes a patch record to Bob's DWN:
await agent.dwn.processRequest({
author : aliceDid,
target : bobDid, // Writing to Bob's DWN
messageType : DwnInterface.RecordsWrite,
messageParams: {
protocol : 'https://enbox.id/protocols/forge/patches',
protocolPath : 'repo/patch',
protocolRole : 'repo:repo/contributor',
parentContextId : bobsRepoContextId, // Links to repo via $ref
dataFormat : 'application/json',
tags : {
status : 'open',
baseBranch : 'main',
headBranch : 'feature-x',
sourceDid : aliceDid,
},
},
dataStream: new Blob([JSON.stringify({ title: 'Add feature X', body: '...' })]),
});Bob's client uses sourceDid to resolve Alice's DWN and fetch the git branch for review. Reviews and comments happen on Bob's DWN (where the patch lives).
GitHub issues have sequential numbers (#1, #2, ...). In a decentralized system with concurrent creation (even offline), sequential numbering requires coordination.
Mitigation: The repo owner assigns sequential numbers. The owner's DWN is the single writer for the number field. This creates a minor bottleneck but is acceptable — issue number assignment is low-frequency and the owner's DWN is always the authority for their repo.
DWN records support arbitrary data sizes via streaming. For a git LFS equivalent: LFS pointers in the git repo reference DWN record IDs. Binary data lives as records in a forge-lfs protocol with $size constraints.
The core bottleneck: querying thousands of records. Mitigations:
- Tag-based filtering pushes selectivity into the DWN (
tags: { status: 'open' }), combined withcontextId-scoped queries that leverage the$refrepo context - Pagination with cursors for large result sets
RecordsCountfor efficient counting without fetching full recordsRecordsSubscribefor real-time updates (no polling)- Client-side caching with subscription-based invalidation
DWN subscriptions (RecordsSubscribe over WebSocket) provide event notification when records are written. This is sufficient for forge use cases:
- New issue comment appeared: subscription delivers the event
- CI status changed: subscription delivers the update
- PR was merged: subscription delivers the event
All of these tolerate 100-500ms latency. Sub-second collaborative editing (Google Docs style, Figma cursors) would require ~16-50ms round-trip, which DWN's message processing pipeline doesn't support. But this is a v3+ concern (live code editing) — not needed for any core forge feature.
No "explore" page without an indexing service. This is a solved problem with indexers (Section 8). Multiple competing indexers can provide discovery. Initially, a single "official" indexer can bootstrap the ecosystem, similar to how npmjs.com bootstrapped npm.
Tools like VS Code, JetBrains IDEs, and gh CLI assume GitHub's REST/GraphQL API. Options:
- Centralized shim (temporary): a service that translates GitHub API calls to DWN protocol queries. Lets existing tools work during migration.
- Native integrations (long-term): VS Code extension, CLI tool (
gitd), web UI - Some won't matter: many GitHub-specific features (Actions YAML, Dependabot) are deeply coupled to GitHub's infrastructure. These get replaced by DWN-native equivalents, not shimmed.
A migration tool:
- Import repo metadata -> create
reporecord - Import issues -> create
issuerecords withcommentchildren - Import PRs -> create
patchrecords withreview/revisionchildren - Map GitHub usernames to DIDs (claim-based: "I am @alice on GitHub, here's my DID")
- Import releases -> create
releaserecords withassetchildren - Set up git remote -> push git objects to the DWN-adjacent git server
- Architecture document (this file)
- TypeScript protocol definitions for all 11 protocols
- JSON Schema files for each record type (34 schemas)
- Structural tests (220 tests, 615 assertions)
- Package setup (bun, TypeScript, ESLint, build)
The smallest useful forge — repos, issues, patches.
- forge-repo: repo CRUD, collaborator role management — protocol definition complete, integration-tested
- forge-issues: issue CRUD, comments, labels, status changes — protocol definition complete, integration-tested (cross-protocol roles via
$ref) - forge-patches: PR CRUD, revisions, reviews, merge results — protocol definition complete, integration-tested (cross-protocol roles via
$ref) - Integration tests against a real DWN instance — 15 tests covering repo, issues, patches, CI, and role revocation (242 total tests, 738 assertions)
-
$refwrapping: all 5 composing protocols updated withrepo: { $ref: 'repo:repo' }for cross-protocol role composition - CLI prototype:
gitd init,gitd issue create/list,gitd pr create/list,gitd whoami— 12 CLI tests (254 total, 762 assertions)
The most novel component.
- git-remote-did: git remote helper that resolves DIDs to git endpoints — DID URL parser (
did::anddid://forms), DID resolution with GitTransport/DWN service discovery, exec delegation togit-remote-https - GitTransport service type:
GitTransportServiceinterface,createGitTransportService()factory,isGitTransportService()/getGitTransportServices()query helpers - DWN server git sidecar:
GitBackend(bare repo management with DID-hashed paths),createGitHttpHandler()(smart HTTP v1: info/refs, upload-pack, receive-pack),createGitServer()(Node.js HTTP bridge), path prefix support, pluggable auth - Push authentication: DID-signed token scheme over HTTP Basic auth (
did-authusername, signed payload in password field),createPushAuthenticator()factory with signature verification + optional role-based authorization callbacks - git ref mirroring:
ForgeRefsProtocol— DWN protocol for branch/tag refs as records with$refcomposition, role-based write access (maintainers only), anyone-read for subscription-based push notifications - Decentralized bundle storage:
bundletype in ForgeRepoProtocol with$squash: true, post-push bundle sync (createBundleSyncer()), cold-start restore (restoreFromBundles()), public/private encryption model. See Section 6. - E2E bundle round-trip: push -> bundle sync to DWN -> delete bare repo -> restore from DWN bundles -> clone and verify content
- 453 total tests, 1147 assertions across 14 test files
CLI commands and tests for all 6 extended protocols — 53 CLI tests, 506 total tests, 1238 assertions.
- forge-ci: check suites, check runs, status updates — 6 CLI subcommands (status, list, show, create, run, update), 13 CLI tests
- forge-releases: release management, pre-releases — 3 CLI subcommands (create, show, list), 6 CLI tests
- forge-wiki: collaborative documentation with edit history — 4 CLI subcommands (create, show, edit, list), 8 CLI tests
- forge-org: organization/team management — 9 CLI subcommands (create, info, add-member, remove-member, list-members, add-owner, team create/list/add-member), 11 CLI tests
- forge-social: stars, follows — 6 CLI subcommands (star, unstar, stars, follow, unfollow, following), 11 CLI tests
- forge-notifications: inbox management — 3 CLI subcommands (list, read, clear), 8 CLI tests
Registry CLI with 5 subcommands and 20 CLI tests — 525 total tests, 1282 assertions.
- forge-registry: package publishing, version management, tarballs — 10 CLI subcommands, 20 CLI tests
- Attestation system: third-party build verification — attest, attestations, verify commands
- npm resolver: DID-scoped package resolution — resolve command, tarball fetch
- Dependency verification: trust chain validation — verify-deps command, recursive tree
- Indexer service: repo discovery, star aggregation, external contribution surfacing
- Web UI: read-only repo/issue/PR viewer
- VS Code extension: native IDE integration (separate repo, post-stabilization)
- GitHub migration tool: import repos, issues, PRs from GitHub
- GitHub API compatibility shim: read + write (583 endpoints — 327 GET, 256 POST/PATCH/PUT/DELETE)
- Package manager shims: npm registry proxy, Go module proxy, OCI/Docker registry proxy — 56 tests, 122 assertions
- Unified daemon:
ShimAdapterinterface, config-driven multi-shim process,gitd daemoncommand — 34 tests, 85 assertions
gitd/
├── PLAN.md # This document
├── README.md
├── LICENSE
├── package.json
├── tsconfig.json
├── eslint.config.js
├── src/
│ ├── index.ts # Barrel re-export of all protocols
│ ├── repo.ts # ForgeRepoDefinition + types
│ ├── issues.ts # ForgeIssuesDefinition + types
│ ├── patches.ts # ForgePatchesDefinition + types
│ ├── ci.ts # ForgeCiDefinition + types
│ ├── releases.ts # ForgeReleasesDefinition + types
│ ├── registry.ts # ForgeRegistryDefinition + types
│ ├── social.ts # ForgeSocialDefinition + types
│ ├── notifications.ts # ForgeNotificationsDefinition + types
│ ├── wiki.ts # ForgeWikiDefinition + types
│ ├── org.ts # ForgeOrgDefinition + types
│ ├── cli/ # CLI commands
│ │ ├── main.ts # Entry point, command dispatch
│ │ ├── agent.ts # Web5 agent connection
│ │ ├── flags.ts # Shared CLI flag utilities
│ │ ├── repo-context.ts # Shared repo record lookup
│ │ └── commands/ # Individual CLI commands
│ │ ├── clone.ts
│ │ ├── init.ts
│ │ ├── issue.ts
│ │ ├── log.ts
│ │ ├── patch.ts
│ │ ├── repo.ts
│ │ ├── serve.ts
│ │ └── setup.ts
│ ├── git-remote/ # Git remote helper
│ │ ├── index.ts # Barrel re-export
│ │ ├── main.ts # Entry point (git-remote-did binary)
│ │ ├── credential-helper.ts # Push credential generation
│ │ ├── credential-main.ts # Credential helper entry point
│ │ ├── parse-url.ts # DID URL parser (did::, did://)
│ │ ├── resolve.ts # DID resolution + endpoint discovery
│ │ └── service.ts # GitTransport DID service type utilities
│ ├── git-server/ # Git transport sidecar server
│ │ ├── index.ts # Barrel re-export
│ │ ├── auth.ts # DID-signed push authentication
│ │ ├── bundle-restore.ts # Restore bare repos from DWN bundle records
│ │ ├── bundle-sync.ts # Post-push bundle sync to DWN
│ │ ├── did-service.ts # DID service registration
│ │ ├── git-backend.ts # Bare repo management (init, upload-pack, receive-pack)
│ │ ├── http-handler.ts # Smart HTTP protocol handler (with onRepoNotFound)
│ │ ├── push-authorizer.ts # DWN-backed push authorization
│ │ ├── ref-sync.ts # Git ref → DWN record mirroring
│ │ ├── server.ts # Node.js HTTP server bridge
│ │ └── verify.ts # DID signature verification
│ └── github-shim/ # GitHub API compatibility shim
│ ├── index.ts # Barrel re-export
│ ├── helpers.ts # numericId, fromOpt, pagination, response builders
│ ├── server.ts # HTTP server, router, handleShimRequest
│ ├── repos.ts # GET /repos/:did/:repo
│ ├── issues.ts # Issues, labels, milestones, assignees, reactions
│ ├── pulls.ts # Pull requests, reviews, review comments, merge
│ ├── releases.ts # Releases and release assets
│ ├── commit-comments.ts # Commit comments
│ ├── deployments.ts # Deployments and deployment statuses
│ ├── repo-metadata.ts # Topics, languages, deploy keys, collaborators
│ ├── git-refs.ts # Branches, tags, refs, branch protection
│ ├── git-objects.ts # Git blobs, trees, commits, archives, contributors
│ ├── checks.ts # Statuses, check suites, check runs
│ ├── actions.ts # Actions workflows, workflow runs, jobs, logs, and artifacts
│ ├── webhooks.ts # Repository webhooks
│ ├── notifications.ts # Notifications and thread subscriptions
│ ├── stars.ts # Stars and stargazers
│ ├── follows.ts # Followers and following
│ ├── orgs.ts # Organizations, org members, teams, team members
│ ├── search.ts # GitHub-compatible search endpoints
│ └── users.ts # GET /users/:did
│ └── resolver/ # Package resolver + trust chain
│ ├── index.ts # Barrel re-export
│ ├── resolve.ts # DID-scoped package/version/tarball resolution
│ ├── verify.ts # Package integrity + attestation verification
│ └── trust-chain.ts # Recursive dependency trust chain validator
│ └── shims/ # Package manager shims
│ ├── index.ts # Barrel re-export (all shims)
│ ├── npm/ # npm registry shim
│ │ ├── index.ts
│ │ ├── registry.ts # npm API handlers, packument builder
│ │ └── server.ts # HTTP server
│ ├── go/ # Go module proxy shim
│ │ ├── index.ts
│ │ ├── proxy.ts # GOPROXY handlers, go.mod generator
│ │ └── server.ts # HTTP server
│ └── oci/ # OCI/Docker registry shim
│ ├── index.ts
│ ├── registry.ts # OCI Distribution API handlers
│ └── server.ts # HTTP server
│ └── daemon/ # Unified shim daemon
│ ├── index.ts # Barrel re-export
│ ├── adapter.ts # ShimAdapter interface + config types
│ ├── server.ts # DaemonServer lifecycle (start, stop, health)
│ └── adapters/ # Per-ecosystem adapter implementations
│ ├── index.ts # Adapter registry (builtinAdapters)
│ ├── github.ts # GitHub API adapter
│ ├── npm.ts # npm registry adapter
│ ├── go.ts # Go module proxy adapter
│ └── oci.ts # OCI/Docker registry adapter
├── schemas/ # JSON Schema files (34 files)
│ ├── repo/
│ ├── refs/
│ ├── issues/
│ ├── patches/
│ ├── ci/
│ ├── releases/
│ ├── registry/
│ ├── social/
│ ├── notifications/
│ ├── wiki/
│ └── org/
└── tests/
├── protocols.spec.ts # Structural validation tests (159 tests)
├── schemas.spec.ts # JSON schema validation tests
├── integration.spec.ts # DWN integration tests (15 tests)
├── cli.spec.ts # CLI command tests (48 tests)
├── e2e.spec.ts # E2E tests: transport + bundle round-trip (11 tests)
├── bundle-sync.spec.ts # Bundle sync unit tests (8 tests)
├── bundle-restore.spec.ts # Bundle restore unit tests (5 tests)
├── git-remote.spec.ts # Git remote helper + service type tests
├── git-server.spec.ts # Git sidecar server tests
├── git-auth.spec.ts # Push authentication tests
├── push-authorizer.spec.ts # DWN push authorization tests
├── ref-sync.spec.ts # Ref sync tests
├── verify.spec.ts # Signature verification tests
├── credential-helper.spec.ts # Credential helper tests
├── github-shim.spec.ts # GitHub API shim tests (394 tests)
├── resolver.spec.ts # Resolver, attestation, trust chain tests (41 tests)
├── shims.spec.ts # Package manager shim tests (56 tests)
├── daemon.spec.ts # Unified daemon tests (34 tests)
└── hardening.spec.ts # Production hardening security tests (38 tests)
Comprehensive security hardening applied across all server-facing code:
- Path traversal protection —
GitBackend.repoPath()validates repo names against/^[a-zA-Z0-9._-]+$/and verifies the resolved path stays within the base directory.parseRoute()in the HTTP handler applies the same validation. - XSS protection — All user-facing HTML output in the web UI is escaped via
esc(), including thenotFound()anddidError()error pages. - Port validation — All CLI commands use
parsePort()which validates port numbers are integers in the range 1–65535 and exits with a clear error for invalid input.
- Request body size limits — All HTTP servers enforce configurable maximum body sizes: 1 MB for JSON API bodies (GitHub shim, daemon adapters), 50 MB for git pack data (configurable via
maxBodySizeoption). Oversized requests receive413 Payload Too Large. - SSRF protection — DID-resolved service endpoint URLs are validated against a denylist of private/loopback IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10) and
localhost. Prevents attackers from crafting DID documents that point to internal infrastructure. - DID resolution timeouts — All DID resolution calls are wrapped with a 30-second timeout to prevent hanging on malicious or unreachable endpoints.
- Bearer token auth for API writes — The GitHub API shim supports optional Bearer token authentication for all mutating endpoints (POST, PATCH, PUT, DELETE). Configured via
GITD_API_TOKENenvironment variable. Uses constant-time comparison to prevent timing attacks. When no token is configured, access is open (local development mode). - Nonce replay protection — The push authenticator tracks used nonces in a time-bounded set. Replayed tokens are rejected. Expired nonces are evicted automatically (TTL = maxTokenAge + 60s clock skew).
- Indexer store size limits — The in-memory
IndexerStoreaccepts configurable limits (maxDids,maxRepos,maxStars,maxFollows) with FIFO eviction when capacity is reached. Defaults: 100K DIDs, 100K repos, 500K stars, 500K follows.
- Health endpoints — All standalone servers (git server, GitHub API shim, web UI) and all daemon adapter servers expose
GET /healthreturning{ "status": "ok", "service": "<name>" }for load balancer and monitoring integration.