+2
-1
.example.env
+2
-1
.example.env
-185
src/knot-client.js
-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
+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
+1
src/server.js
+1
src/worker.js
+1
src/worker.js
-16
test/knot-client-test.js
-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);