Static site hosting via tangled
1// https://tangled.sh/@tangled.sh/core/blob/master/knotclient/unsigned.go 2// Converted to JavaScript by Claude Code 3// Changes: 4// - Added blob method 5// - Added verbose option 6 7class UnsignedClient { 8 constructor(domain, dev = false, verbose = false) { 9 this.baseUrl = new URL(`${dev ? "http" : "https"}://${domain}`); 10 this.verbose = verbose; 11 } 12 13 async newRequest(method, endpoint, query = null, body = null) { 14 const url = new URL(endpoint, this.baseUrl); 15 16 if (query) { 17 for (const [key, value] of Object.entries(query)) { 18 url.searchParams.append(key, value); 19 } 20 } 21 22 const options = { 23 method, 24 headers: { 25 "Content-Type": "application/json", 26 }, 27 signal: AbortSignal.timeout(5000), // 5 second timeout 28 }; 29 30 if (body) { 31 options.body = typeof body === "string" ? body : JSON.stringify(body); 32 } 33 34 return { url: url.toString(), options }; 35 } 36 37 async doRequest(url, options) { 38 try { 39 if (this.verbose) { 40 console.log("Request:", url); 41 } 42 43 const response = await fetch(url, options); 44 45 if (!response.ok && response.status !== 404 && response.status !== 400) { 46 throw new Error(`HTTP error! status: ${response.status}`); 47 } 48 49 const text = await response.text(); 50 return { 51 status: response.status, 52 data: text ? JSON.parse(text) : null, 53 }; 54 } catch (error) { 55 console.error("Request error:", error); 56 throw error; 57 } 58 } 59 60 async index(ownerDid, repoName, ref = "") { 61 const endpoint = ref 62 ? `/${ownerDid}/${repoName}/tree/${ref}` 63 : `/${ownerDid}/${repoName}`; 64 65 const { url, options } = await this.newRequest("GET", endpoint); 66 const response = await this.doRequest(url, options); 67 return response.data; 68 } 69 70 async log(ownerDid, repoName, ref, page = 0) { 71 const endpoint = `/${ownerDid}/${repoName}/log/${encodeURIComponent(ref)}`; 72 const query = { 73 page: page.toString(), 74 per_page: "60", 75 }; 76 77 const { url, options } = await this.newRequest("GET", endpoint, query); 78 const response = await this.doRequest(url, options); 79 return response.data; 80 } 81 82 async branches(ownerDid, repoName) { 83 const endpoint = `/${ownerDid}/${repoName}/branches`; 84 85 const { url, options } = await this.newRequest("GET", endpoint); 86 const response = await this.doRequest(url, options); 87 return response.data; 88 } 89 90 async tags(ownerDid, repoName) { 91 const endpoint = `/${ownerDid}/${repoName}/tags`; 92 93 const { url, options } = await this.newRequest("GET", endpoint); 94 const response = await this.doRequest(url, options); 95 return response.data; 96 } 97 98 async branch(ownerDid, repoName, branch) { 99 const endpoint = `/${ownerDid}/${repoName}/branches/${encodeURIComponent( 100 branch 101 )}`; 102 103 const { url, options } = await this.newRequest("GET", endpoint); 104 const response = await this.doRequest(url, options); 105 return response.data; 106 } 107 108 async defaultBranch(ownerDid, repoName) { 109 const endpoint = `/${ownerDid}/${repoName}/branches/default`; 110 111 const { url, options } = await this.newRequest("GET", endpoint); 112 const response = await this.doRequest(url, options); 113 return response.data; 114 } 115 116 async capabilities() { 117 const endpoint = "/capabilities"; 118 119 const { url, options } = await this.newRequest("GET", endpoint); 120 const response = await this.doRequest(url, options); 121 return response.data; 122 } 123 124 async compare(ownerDid, repoName, rev1, rev2) { 125 const endpoint = `/${ownerDid}/${repoName}/compare/${encodeURIComponent( 126 rev1 127 )}/${encodeURIComponent(rev2)}`; 128 129 const { url, options } = await this.newRequest("GET", endpoint); 130 131 try { 132 const response = await fetch(url, options); 133 134 if (response.status === 404 || response.status === 400) { 135 throw new Error("Branch comparisons not supported on this knot."); 136 } 137 138 if (!response.ok) { 139 throw new Error("Failed to create request."); 140 } 141 142 const text = await response.text(); 143 return JSON.parse(text); 144 } catch (error) { 145 console.error("Failed to compare across branches"); 146 throw new Error("Failed to compare branches."); 147 } 148 } 149 150 async repoLanguages(ownerDid, repoName, ref) { 151 const endpoint = `/${ownerDid}/${repoName}/languages/${encodeURIComponent( 152 ref 153 )}`; 154 155 try { 156 const { url, options } = await this.newRequest("GET", endpoint); 157 const response = await fetch(url, options); 158 159 if (response.status !== 200) { 160 console.warn("Failed to calculate languages", response.status); 161 return {}; 162 } 163 164 const text = await response.text(); 165 return JSON.parse(text); 166 } catch (error) { 167 console.error("Error fetching repo languages:", error); 168 throw error; 169 } 170 } 171 172 async blob(ownerDid, repoName, ref, filePath) { 173 const endpoint = `/${ownerDid}/${repoName}/blob/${encodeURIComponent( 174 ref 175 )}/${filePath}`; 176 177 const { url, options } = await this.newRequest("GET", endpoint); 178 const response = await this.doRequest(url, options); 179 return response.data; 180 } 181} 182 183export function createUnsignedClient(domain, dev = false, verbose = false) { 184 return new UnsignedClient(domain, dev, verbose); 185}