diff --git a/.changeset/fix-clone-remote-did.md b/.changeset/fix-clone-remote-did.md new file mode 100644 index 0000000..6718e76 --- /dev/null +++ b/.changeset/fix-clone-remote-did.md @@ -0,0 +1,10 @@ +--- +'@enbox/gitd': patch +--- + +fix: skip local daemon when cloning repos owned by a different DID + +The local daemon resolver now checks `ownerDid` in the lockfile and +only routes to `localhost` when the requested DID matches the daemon +owner. Previously, cloning any DID would hit the local daemon — which +does not have the remote user's repos — and fail with 404. diff --git a/src/cli/commands/serve.ts b/src/cli/commands/serve.ts index 943d8be..99ada16 100644 --- a/src/cli/commands/serve.ts +++ b/src/cli/commands/serve.ts @@ -295,7 +295,7 @@ export async function serveCommand(ctx: AgentContext, args: string[]): Promise => { diff --git a/src/daemon/lockfile.ts b/src/daemon/lockfile.ts index ddd50fe..f8a1cbe 100644 --- a/src/daemon/lockfile.ts +++ b/src/daemon/lockfile.ts @@ -2,7 +2,7 @@ * Daemon lockfile — discovery mechanism for the local gitd server. * * When `gitd serve` starts, it writes a JSON lockfile to - * `~/.enbox/daemon.lock` containing `{ pid, port, startedAt }`. + * `~/.enbox/daemon.lock` containing `{ pid, port, startedAt, ownerDid }`. * `git-remote-did` reads this file to discover a running local daemon * and resolve `did::` remotes to `http://localhost:/...` instead * of performing DID document resolution. @@ -35,6 +35,9 @@ export type DaemonLock = { /** The gitd version that started this daemon (for upgrade detection). */ version?: string; + + /** The DID of the identity that owns this daemon. */ + ownerDid?: string; }; // --------------------------------------------------------------------------- @@ -51,12 +54,13 @@ export function lockfilePath(): string { // --------------------------------------------------------------------------- /** Write the daemon lockfile. Overwrites any existing file. */ -export function writeLockfile(port: number, version?: string): void { +export function writeLockfile(port: number, version?: string, ownerDid?: string): void { const lock: DaemonLock = { pid : process.pid, port, startedAt : new Date().toISOString(), ...(version ? { version } : {}), + ...(ownerDid ? { ownerDid } : {}), }; const path = lockfilePath(); mkdirSync(dirname(path), { recursive: true }); diff --git a/src/git-remote/resolve.ts b/src/git-remote/resolve.ts index b85822f..6d97e60 100644 --- a/src/git-remote/resolve.ts +++ b/src/git-remote/resolve.ts @@ -134,6 +134,15 @@ async function resolveLocalDaemon(did: string, repo?: string): Promise { expect(lock!.version).toBeUndefined(); removeLockfile(); }); + + it('should write ownerDid to lockfile when provided', () => { + writeLockfile(9418, '1.0.0', 'did:dht:owner123'); + const lock = readLockfile(); + expect(lock).not.toBeNull(); + expect(lock!.ownerDid).toBe('did:dht:owner123'); + removeLockfile(); + }); + + it('should omit ownerDid when not provided', () => { + writeLockfile(9418, '1.0.0'); + const lock = readLockfile(); + expect(lock).not.toBeNull(); + expect(lock!.ownerDid).toBeUndefined(); + removeLockfile(); + }); }); // --------------------------------------------------------------------------- diff --git a/tests/git-remote.spec.ts b/tests/git-remote.spec.ts index 5eb354d..0b1c84c 100644 --- a/tests/git-remote.spec.ts +++ b/tests/git-remote.spec.ts @@ -462,7 +462,7 @@ describe('resolveGitEndpoint with local daemon', () => { server.close(); }); - it('should resolve via local daemon when lockfile exists', async () => { + it('should resolve via local daemon when lockfile has no ownerDid (backwards compat)', async () => { const result = await resolveGitEndpoint('did:dht:abc123', 'my-repo'); expect(result.source).toBe('LocalDaemon'); expect(result.url).toBe(`http://localhost:${port}/did:dht:abc123/my-repo`); @@ -474,3 +474,48 @@ describe('resolveGitEndpoint with local daemon', () => { expect(result.url).toBe(`http://localhost:${port}/did:dht:abc123`); }); }); + +// --------------------------------------------------------------------------- +// Local daemon DID ownership check +// --------------------------------------------------------------------------- + +describe('resolveGitEndpoint skips local daemon for non-owner DID', () => { + let server: ReturnType; + let port: number; + + const ownerDid = 'did:dht:localowner'; + const remoteDid = 'did:dht:remoteuser'; + + beforeAll(async () => { + server = createServer((_req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok' })); + }); + await new Promise((resolve) => { + server.listen(0, () => { + port = (server.address() as any).port; + resolve(); + }); + }); + // Write a lockfile with ownerDid set. + writeLockfile(port, '1.0.0', ownerDid); + }); + + afterAll(() => { + removeLockfile(); + server.close(); + }); + + it('should use local daemon when requested DID matches ownerDid', async () => { + const result = await resolveGitEndpoint(ownerDid, 'my-repo'); + expect(result.source).toBe('LocalDaemon'); + expect(result.url).toBe(`http://localhost:${port}/${ownerDid}/my-repo`); + }); + + it('should skip local daemon when requested DID differs from ownerDid', async () => { + // The remote DID is not resolvable, so this should throw after + // skipping the local daemon — confirming it didn't short-circuit. + await expect(resolveGitEndpoint(remoteDid, 'their-repo')) + .rejects.toThrow(); + }); +});