fix(desktop): derive default clone URL for relay-hosted repos (#2166)

Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: npub1hwqy0rnujtl25dzmlhn8qwux4kr8sjhas3ugltx9j5dm5dwkp2dsqjhytw <bb80478e7c92feaa345bfde6703b86ad86784afd84788facc5951bba35d60a9b@pending-seed.communities.buzz.xyz>
This commit is contained in:
Tyler
2026-07-20 08:49:44 -04:00
committed by GitHub
co-authored by npub1hwqy0rnujtl25dzmlhn8qwux4kr8sjhas3ugltx9j5dm5dwkp2dsqjhytw
parent 8024ce72ef
commit 21dbd32645
4 changed files with 141 additions and 5 deletions
+25 -4
View File
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as React from "react";
import { relayClient } from "@/shared/api/relayClient";
import { getCachedRelayOrigin } from "@/shared/lib/mediaUrl";
import { signRelayEvent } from "@/shared/api/tauri";
import { getIdentity } from "@/shared/api/tauriIdentity";
import {
@@ -37,6 +38,7 @@ import type {
RelayEvent,
} from "@/shared/api/types";
import { summarizeProjectActivityEvents } from "./projectActivity.mjs";
import { effectiveCloneUrls } from "./lib/projectCloneUrl";
import type { ProjectIssue } from "./projectIssues.mjs";
import { projectIssueEventsToIssues } from "./projectIssues.mjs";
import type {
@@ -172,11 +174,27 @@ function isDeletedByA(project: Project, deletionEvents: RelayEvent[]): boolean {
);
}
export function eventToProject(event: RelayEvent): Project {
/**
* Converts a kind:30617 repo announcement into a `Project`.
*
* `relayOrigin` is the resolved relay HTTP origin (from `getCachedRelayOrigin`)
* used to synthesize a canonical clone URL when the announcement omits an
* explicit `clone` tag. Callers outside the relay-connected app (e.g. unit
* tests) may omit it, in which case no default is derived.
*/
export function eventToProject(
event: RelayEvent,
relayOrigin?: string | null,
): Project {
const d = getTag(event, "d") ?? event.id;
const name = getTag(event, "name") || d;
const description = getTag(event, "description") || event.content || "";
const cloneUrls = getCloneUrls(event);
const cloneUrls = effectiveCloneUrls(
getCloneUrls(event),
relayOrigin,
event.pubkey,
d,
);
const webUrl = getTag(event, "web") ?? null;
const setupUsers = getAllTags(event, "auth");
const contributors = [...new Set([...getAllTags(event, "p"), ...setupUsers])];
@@ -235,7 +253,7 @@ export async function fetchProjects(): Promise<Project[]> {
]);
return dedup(events)
.map(eventToProject)
.map((event) => eventToProject(event, getCachedRelayOrigin()))
.filter(
(project) =>
!isHiddenLocally(project) && !isDeletedByA(project, deletionEvents),
@@ -273,7 +291,10 @@ async function fetchProject(projectId: string): Promise<Project | null> {
const deduped = dedup(events).filter(
(event) => !owner || event.pubkey.toLowerCase() === owner,
);
const project = deduped.length > 0 ? eventToProject(deduped[0]) : null;
const project =
deduped.length > 0
? eventToProject(deduped[0], getCachedRelayOrigin())
: null;
if (!project) {
return null;
}
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { deriveRelayCloneUrl, effectiveCloneUrls } from "./projectCloneUrl.ts";
const OWNER = "a".repeat(64);
const ORIGIN = "https://relay.example";
test("deriveRelayCloneUrl builds the canonical relay-hosted path", () => {
assert.equal(
deriveRelayCloneUrl(ORIGIN, OWNER, "flappy-bee"),
`${ORIGIN}/git/${OWNER}/flappy-bee`,
);
});
test("deriveRelayCloneUrl lowercases the owner pubkey", () => {
const upper = "A".repeat(64);
assert.equal(
deriveRelayCloneUrl(ORIGIN, upper, "repo"),
`${ORIGIN}/git/${OWNER}/repo`,
);
});
test("deriveRelayCloneUrl tolerates a trailing slash on the origin", () => {
assert.equal(
deriveRelayCloneUrl(`${ORIGIN}/`, OWNER, "repo"),
`${ORIGIN}/git/${OWNER}/repo`,
);
});
test("deriveRelayCloneUrl fails closed on an unresolved origin", () => {
assert.equal(deriveRelayCloneUrl(null, OWNER, "repo"), null);
assert.equal(deriveRelayCloneUrl(undefined, OWNER, "repo"), null);
assert.equal(deriveRelayCloneUrl("", OWNER, "repo"), null);
});
test("deriveRelayCloneUrl declines a non-hex or wrong-length owner", () => {
assert.equal(deriveRelayCloneUrl(ORIGIN, "short", "repo"), null);
assert.equal(deriveRelayCloneUrl(ORIGIN, "z".repeat(64), "repo"), null);
});
test("deriveRelayCloneUrl declines a missing repo id", () => {
assert.equal(deriveRelayCloneUrl(ORIGIN, OWNER, ""), null);
});
test("effectiveCloneUrls honors explicit clone URLs over the derived default", () => {
const explicit = ["https://github.com/octocat/hello"];
assert.deepEqual(
effectiveCloneUrls(explicit, ORIGIN, OWNER, "repo"),
explicit,
);
});
test("effectiveCloneUrls derives a default when none is advertised", () => {
assert.deepEqual(effectiveCloneUrls([], ORIGIN, OWNER, "flappy-bee"), [
`${ORIGIN}/git/${OWNER}/flappy-bee`,
]);
});
test("effectiveCloneUrls returns empty when no default can be derived", () => {
assert.deepEqual(effectiveCloneUrls([], null, OWNER, "repo"), []);
});
@@ -0,0 +1,52 @@
/**
* Deriving a default clone URL for a NIP-34 repo announcement that omits an
* explicit `clone` tag.
*
* Buzz relays serve their own git repositories at a canonical path
* `<relay-origin>/git/<owner-pubkey>/<repo-id>` which is exactly the shape the
* Rust `validate_clone_url` gate enforces. When an announcement carries no
* `clone` tag (e.g. it was created via `buzz repos create` without `--clone`),
* the desktop would otherwise have no URL to fetch from, so the project detail
* view comes up empty. Synthesizing the canonical relay-hosted URL lets those
* repositories load while still deferring to any explicit clone URLs.
*/
/**
* Builds the canonical relay-hosted clone URL for a repository, or `null` when
* the inputs cannot produce a valid URL (unresolved relay origin, missing owner
* pubkey, or missing repo id). Fails closed rather than emitting a broken URL.
*
* `relayOrigin` is expected to be a bare origin (scheme + host, e.g.
* `https://relay.example`); a trailing slash is tolerated.
*/
export function deriveRelayCloneUrl(
relayOrigin: string | null | undefined,
owner: string,
dtag: string,
): string | null {
if (!relayOrigin || !owner || !dtag) return null;
// The Rust validator requires a 64-char hex owner pubkey; anything else is
// not a relay-hosted repo we can address, so decline rather than guess.
if (!/^[0-9a-fA-F]{64}$/.test(owner)) return null;
const origin = relayOrigin.replace(/\/+$/, "");
return `${origin}/git/${owner.toLowerCase()}/${dtag}`;
}
/**
* Returns the effective clone URLs for a project: the explicitly advertised
* ones when present, otherwise a single-element list holding the derived
* relay-hosted default (or an empty list when no default can be derived).
*
* Explicit `clone` tags always win NIP-34 permits pointing `clone` at an
* external host (e.g. GitHub), which must not be overridden.
*/
export function effectiveCloneUrls(
cloneUrls: string[],
relayOrigin: string | null | undefined,
owner: string,
dtag: string,
): string[] {
if (cloneUrls.length > 0) return cloneUrls;
const derived = deriveRelayCloneUrl(relayOrigin, owner, dtag);
return derived ? [derived] : [];
}
@@ -7,6 +7,7 @@ import {
projectsQueryKey,
} from "@/features/projects/hooks";
import { relayClient } from "@/shared/api/relayClient";
import { getCachedRelayOrigin } from "@/shared/lib/mediaUrl";
import { signRelayEvent } from "@/shared/api/tauri";
import { getIdentity } from "@/shared/api/tauriIdentity";
import { KIND_REPO_ANNOUNCEMENT } from "@/shared/constants/kinds";
@@ -77,7 +78,7 @@ async function createProject(input: CreateProjectInput): Promise<Project> {
"Failed to create project.",
);
return eventToProject(event);
return eventToProject(event, getCachedRelayOrigin());
}
/** Mutation that creates a project and inserts it into the projects cache. */