Static site hosting via tangled

Use fetch instead of client

+2 -1
.example.env
···
# note these aren't secrets, just config
KNOT_DOMAIN = "knot.gracekind.net"
OWNER_DID = "did:plc:p572wxnsuoogcrhlfrlizlrb"
-
REPO_NAME = "tangled-pages-example"
+
REPO_NAME = "tangled-pages-example"
+
BRANCH = "main"
-185
src/knot-client.js
···
-
// https://tangled.sh/@tangled.sh/core/blob/master/knotclient/unsigned.go
-
// Converted to JavaScript by Claude Code
-
// Changes:
-
// - Added blob method
-
// - Added verbose option
-
-
class UnsignedClient {
-
constructor(domain, dev = false, verbose = false) {
-
this.baseUrl = new URL(`${dev ? "http" : "https"}://${domain}`);
-
this.verbose = verbose;
-
}
-
-
async newRequest(method, endpoint, query = null, body = null) {
-
const url = new URL(endpoint, this.baseUrl);
-
-
if (query) {
-
for (const [key, value] of Object.entries(query)) {
-
url.searchParams.append(key, value);
-
}
-
}
-
-
const options = {
-
method,
-
headers: {
-
"Content-Type": "application/json",
-
},
-
signal: AbortSignal.timeout(5000), // 5 second timeout
-
};
-
-
if (body) {
-
options.body = typeof body === "string" ? body : JSON.stringify(body);
-
}
-
-
return { url: url.toString(), options };
-
}
-
-
async doRequest(url, options) {
-
try {
-
if (this.verbose) {
-
console.log("Request:", url);
-
}
-
-
const response = await fetch(url, options);
-
-
if (!response.ok && response.status !== 404 && response.status !== 400) {
-
throw new Error(`HTTP error! status: ${response.status}`);
-
}
-
-
const text = await response.text();
-
return {
-
status: response.status,
-
data: text ? JSON.parse(text) : null,
-
};
-
} catch (error) {
-
console.error("Request error:", error);
-
throw error;
-
}
-
}
-
-
async index(ownerDid, repoName, ref = "") {
-
const endpoint = ref
-
? `/${ownerDid}/${repoName}/tree/${ref}`
-
: `/${ownerDid}/${repoName}`;
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
-
async log(ownerDid, repoName, ref, page = 0) {
-
const endpoint = `/${ownerDid}/${repoName}/log/${encodeURIComponent(ref)}`;
-
const query = {
-
page: page.toString(),
-
per_page: "60",
-
};
-
-
const { url, options } = await this.newRequest("GET", endpoint, query);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
-
async branches(ownerDid, repoName) {
-
const endpoint = `/${ownerDid}/${repoName}/branches`;
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
-
async tags(ownerDid, repoName) {
-
const endpoint = `/${ownerDid}/${repoName}/tags`;
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
-
async branch(ownerDid, repoName, branch) {
-
const endpoint = `/${ownerDid}/${repoName}/branches/${encodeURIComponent(
-
branch
-
)}`;
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
-
async defaultBranch(ownerDid, repoName) {
-
const endpoint = `/${ownerDid}/${repoName}/branches/default`;
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
-
async capabilities() {
-
const endpoint = "/capabilities";
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
-
async compare(ownerDid, repoName, rev1, rev2) {
-
const endpoint = `/${ownerDid}/${repoName}/compare/${encodeURIComponent(
-
rev1
-
)}/${encodeURIComponent(rev2)}`;
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
-
try {
-
const response = await fetch(url, options);
-
-
if (response.status === 404 || response.status === 400) {
-
throw new Error("Branch comparisons not supported on this knot.");
-
}
-
-
if (!response.ok) {
-
throw new Error("Failed to create request.");
-
}
-
-
const text = await response.text();
-
return JSON.parse(text);
-
} catch (error) {
-
console.error("Failed to compare across branches");
-
throw new Error("Failed to compare branches.");
-
}
-
}
-
-
async repoLanguages(ownerDid, repoName, ref) {
-
const endpoint = `/${ownerDid}/${repoName}/languages/${encodeURIComponent(
-
ref
-
)}`;
-
-
try {
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await fetch(url, options);
-
-
if (response.status !== 200) {
-
console.warn("Failed to calculate languages", response.status);
-
return {};
-
}
-
-
const text = await response.text();
-
return JSON.parse(text);
-
} catch (error) {
-
console.error("Error fetching repo languages:", error);
-
throw error;
-
}
-
}
-
-
async blob(ownerDid, repoName, ref, filePath) {
-
const endpoint = `/${ownerDid}/${repoName}/blob/${encodeURIComponent(
-
ref
-
)}/${filePath}`;
-
-
const { url, options } = await this.newRequest("GET", endpoint);
-
const response = await this.doRequest(url, options);
-
return response.data;
-
}
-
}
-
-
export function createUnsignedClient(domain, dev = false, verbose = false) {
-
return new UnsignedClient(domain, dev, verbose);
-
}
+11 -8
src/pages-service.js
···
-
import { createUnsignedClient } from "./knot-client.js";
import { getContentTypeForExtension, trimLeadingSlash } from "./helpers.js";
import path from "node:path";
import yaml from "yaml";
···
domain,
ownerDid,
repoName,
+
branch,
configFilepath = "pages_config.yaml",
verbose = false,
fileCacheExpirationSeconds = 10,
}) {
+
this.domain = domain;
this.ownerDid = ownerDid;
this.repoName = repoName;
+
this.branch = branch;
this.configFilepath = configFilepath;
this.verbose = verbose;
-
this.client = createUnsignedClient(domain, false, verbose);
this.fileCache = new FileCache({
expirationSeconds: fileCacheExpirationSeconds,
});
···
return cachedContent;
}
// todo error handling?
-
const blob = await this.client.blob(
-
this.ownerDid,
-
this.repoName,
-
"main",
-
trimLeadingSlash(filename)
-
);
+
const url = `https://${this.domain}/${this.ownerDid}/${
+
this.repoName
+
}/blob/${this.branch}/${trimLeadingSlash(filename)}`;
+
if (this.verbose) {
+
console.log(`Fetching ${url}`);
+
}
+
const res = await fetch(url);
+
const blob = await res.json();
const content = blob.contents;
this.fileCache.set(filename, content);
return content;
+1
src/server.js
···
domain: process.env.KNOT_DOMAIN,
ownerDid: process.env.OWNER_DID,
repoName: process.env.REPO_NAME,
+
branch: process.env.BRANCH,
verbose: process.env.NODE_ENV === "development",
});
+1
src/worker.js
···
domain: env.KNOT_DOMAIN,
ownerDid: env.OWNER_DID,
repoName: env.REPO_NAME,
+
branch: env.BRANCH,
verbose: env.NODE_ENV === "development",
});
}
-16
test/knot-client-test.js
···
-
import { createUnsignedClient } from "../src/knot-client.js";
-
-
const OWNER_DID = "did:plc:p572wxnsuoogcrhlfrlizlrb";
-
const REPO_NAME = "tangled-pages-example";
-
const KNOT_DOMAIN = "knot.gracekind.net";
-
-
const client = createUnsignedClient(KNOT_DOMAIN);
-
-
const blob = await client.blob(
-
OWNER_DID,
-
REPO_NAME,
-
"main",
-
"public/index.html"
-
);
-
-
console.log(blob);