/** * Git utilities for parsing and validating tangled.org remote URLs */ import { isValidHandle, isValidTangledDid } from './validation.js'; export interface ParsedTangledRemote { owner: string; ownerType: 'did' | 'handle'; name: string; protocol: 'ssh' | 'https'; } // The SSH user names the account that pushes (git, or a handle), never the // repository, so any user is accepted; only the host and the path matter. const SSH_USER = '[A-Za-z0-9._-]+'; const SSH_SHORTHAND = new RegExp(`^${SSH_USER}@tangled\\.org:(.+)$`); const SSH_URL = new RegExp(`^ssh://${SSH_USER}@tangled\\.org/(.+)$`); const HTTPS_URL = /^https:\/\/tangled\.org\/(.+)$/; /** The protocol and repository path of a tangled.org remote, or null. */ function remotePath(url: string): { protocol: 'ssh' | 'https'; path: string } | null { const https = url.match(HTTPS_URL); if (https) return { protocol: 'https', path: https[1] }; const ssh = url.match(SSH_URL) ?? url.match(SSH_SHORTHAND); if (ssh) return { protocol: 'ssh', path: ssh[1] }; return null; } /** * Check if a Git remote URL is a tangled.org URL: HTTPS, or SSH as any user * (`git@tangled.org:…`, `alice.bsky.social@tangled.org:…`, `ssh://…@tangled.org/…`). */ export function isTangledRemote(url: string): boolean { return remotePath(url) !== null; } /** * Parse a tangled.org Git remote URL to extract owner and repo name * @param url - Git remote URL * @returns Parsed remote info or null if not a valid tangled URL */ export function parseTangledRemote(url: string): ParsedTangledRemote | null { const remote = remotePath(url); if (!remote) return null; const { protocol } = remote; let { path } = remote; // Remove trailing slashes path = path.replace(/\/+$/, ''); // Remove .git extension if present path = path.replace(/\.git$/, ''); // Tangled's hosted knot accepts a bare repository DID as a Git remote, // e.g. git@tangled.org:did:plc:... . This does not encode the human // owner/repo slug, but it is still a valid Tangled repository remote. if (isValidTangledDid(path)) { return { owner: path, ownerType: 'did', name: path, protocol, }; } // Split path into owner and repo name const parts = path.split('/'); if (parts.length < 2) { return null; } const owner = parts[0]; const name = parts[1]; // Validate that we have both parts if (!owner || !name) { return null; } // Determine owner type based on format let ownerType: 'did' | 'handle'; if (owner.startsWith('did:plc:')) { ownerType = 'did'; // Validate DID format if (!isValidTangledDid(owner)) { return null; } } else { ownerType = 'handle'; // Validate handle format if (!isValidHandle(owner)) { return null; } } return { owner, ownerType, name, protocol, }; }