diff --git a/web/src/lib/api/repoCreationTargets.test.ts b/web/src/lib/api/repoCreationTargets.test.ts index ff8227a12..8cff13a3d 100644 --- a/web/src/lib/api/repoCreationTargets.test.ts +++ b/web/src/lib/api/repoCreationTargets.test.ts @@ -68,6 +68,14 @@ describe("availableKnots", () => { uri: "at://did:plc:owner/sh.tangled.knot.member/one", value: { domain: "member.example" } }, + { + uri: "at://did:web:member.example/sh.tangled.bobbin.knotMember/did:plc:alice", + value: { + subject: "did:plc:alice", + addedBy: "did:plc:owner", + createdAt: "2026-06-01T00:00:00.000Z" + } + }, { uri: "at://did:plc:owner/sh.tangled.knot.member/two", value: { domain: "owned.example" } diff --git a/web/src/lib/api/repoCreationTargets.ts b/web/src/lib/api/repoCreationTargets.ts index 48290f377..f079158d2 100644 --- a/web/src/lib/api/repoCreationTargets.ts +++ b/web/src/lib/api/repoCreationTargets.ts @@ -5,7 +5,7 @@ import { jsonGet } from "$lib/api/_request"; import { mainSchema as listKnotsSchema } from "$lib/api/lexicons/types/sh/tangled/knot/listKnots"; import { mainSchema as listSpindleMembersSchema } from "$lib/api/lexicons/types/sh/tangled/spindle/listMembers"; import { mainSchema as listSpindlesSchema } from "$lib/api/lexicons/types/sh/tangled/spindle/listSpindles"; -import { rkeyFromUri } from "$lib/api/uri"; +import { isDid, parseResourceUri } from "@atcute/lexicons/syntax"; interface Page { items: T[]; @@ -33,6 +33,46 @@ const knotDomain = (value: unknown): string | null => { return typeof value.domain === "string" && value.domain ? value.domain : null; }; +const repoFromUri = (uri: unknown): string | null => { + if (typeof uri !== "string") return null; + try { + return parseResourceUri(uri).repo; + } catch { + return null; + } +}; + +const recordName = (uri: unknown): string | null => { + if (typeof uri !== "string") return null; + try { + return parseResourceUri(uri).rkey ?? null; + } catch { + return null; + } +}; + +const knotHostFromDid = (did: unknown): string | null => { + if (!isDid(did) || !did.startsWith("did:web:")) return null; + const host = did + .slice("did:web:".length) + .replace(/%3A/gi, ":") + .replace(/\.+$/, "") + .toLowerCase(); + return host || null; +}; + +const knotMembershipHost = (item: unknown): string | null => { + if (typeof item !== "object" || item === null) return null; + const record = item as { uri?: unknown; value?: unknown }; + const host = knotHostFromDid(repoFromUri(record.uri)); + if (host) return host; + if (record.value !== undefined) { + const domain = knotDomain(record.value); + if (domain) return domain; + } + return null; +}; + const spindleInstance = (value: unknown): string | null => { if (typeof value !== "object" || value === null || !("instance" in value)) return null; return typeof value.instance === "string" && value.instance ? value.instance : null; @@ -41,11 +81,15 @@ const spindleInstance = (value: unknown): string | null => { const targetNames = ( owned: RecordItem[], memberships: RecordItem[], - memberName: (value: unknown) => string | null + memberName: (item: RecordItem) => string | null ): string[] => { - const names = new Set(owned.map((item) => rkeyFromUri(item.uri))); + const names = new Set(); + for (const item of owned) { + const name = recordName(item.uri); + if (name) names.add(name); + } for (const item of memberships) { - const name = memberName(item.value); + const name = memberName(item); if (name) names.add(name); } return [...names].sort((a, b) => a.localeCompare(b)); @@ -68,7 +112,7 @@ export const availableKnots = async (ctx: BobbinContext, did: string): Promise => { @@ -88,5 +132,5 @@ export const availableSpindles = async (ctx: BobbinContext, did: string): Promis ) ) ]); - return targetNames(owned, memberships, spindleInstance); + return targetNames(owned, memberships, (item) => spindleInstance(item.value)); };