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}