Static site hosting via tangled

Use fetch instead of client

+2 -1
.example.env
··· 2 2 # note these aren't secrets, just config 3 3 KNOT_DOMAIN = "knot.gracekind.net" 4 4 OWNER_DID = "did:plc:p572wxnsuoogcrhlfrlizlrb" 5 - REPO_NAME = "tangled-pages-example" 5 + REPO_NAME = "tangled-pages-example" 6 + BRANCH = "main"
-185
src/knot-client.js
··· 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 - 7 - class 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 - 183 - export function createUnsignedClient(domain, dev = false, verbose = false) { 184 - return new UnsignedClient(domain, dev, verbose); 185 - }
+11 -8
src/pages-service.js
··· 1 - import { createUnsignedClient } from "./knot-client.js"; 2 1 import { getContentTypeForExtension, trimLeadingSlash } from "./helpers.js"; 3 2 import path from "node:path"; 4 3 import yaml from "yaml"; ··· 70 69 domain, 71 70 ownerDid, 72 71 repoName, 72 + branch, 73 73 configFilepath = "pages_config.yaml", 74 74 verbose = false, 75 75 fileCacheExpirationSeconds = 10, 76 76 }) { 77 + this.domain = domain; 77 78 this.ownerDid = ownerDid; 78 79 this.repoName = repoName; 80 + this.branch = branch; 79 81 this.configFilepath = configFilepath; 80 82 this.verbose = verbose; 81 - this.client = createUnsignedClient(domain, false, verbose); 82 83 this.fileCache = new FileCache({ 83 84 expirationSeconds: fileCacheExpirationSeconds, 84 85 }); ··· 115 116 return cachedContent; 116 117 } 117 118 // todo error handling? 118 - const blob = await this.client.blob( 119 - this.ownerDid, 120 - this.repoName, 121 - "main", 122 - trimLeadingSlash(filename) 123 - ); 119 + const url = `https://${this.domain}/${this.ownerDid}/${ 120 + this.repoName 121 + }/blob/${this.branch}/${trimLeadingSlash(filename)}`; 122 + if (this.verbose) { 123 + console.log(`Fetching ${url}`); 124 + } 125 + const res = await fetch(url); 126 + const blob = await res.json(); 124 127 const content = blob.contents; 125 128 this.fileCache.set(filename, content); 126 129 return content;
+1
src/server.js
··· 8 8 domain: process.env.KNOT_DOMAIN, 9 9 ownerDid: process.env.OWNER_DID, 10 10 repoName: process.env.REPO_NAME, 11 + branch: process.env.BRANCH, 11 12 verbose: process.env.NODE_ENV === "development", 12 13 }); 13 14
+1
src/worker.js
··· 11 11 domain: env.KNOT_DOMAIN, 12 12 ownerDid: env.OWNER_DID, 13 13 repoName: env.REPO_NAME, 14 + branch: env.BRANCH, 14 15 verbose: env.NODE_ENV === "development", 15 16 }); 16 17 }
-16
test/knot-client-test.js
··· 1 - import { createUnsignedClient } from "../src/knot-client.js"; 2 - 3 - const OWNER_DID = "did:plc:p572wxnsuoogcrhlfrlizlrb"; 4 - const REPO_NAME = "tangled-pages-example"; 5 - const KNOT_DOMAIN = "knot.gracekind.net"; 6 - 7 - const client = createUnsignedClient(KNOT_DOMAIN); 8 - 9 - const blob = await client.blob( 10 - OWNER_DID, 11 - REPO_NAME, 12 - "main", 13 - "public/index.html" 14 - ); 15 - 16 - console.log(blob);