+104
-43
proxy.js
+104
-43
proxy.js
···
1
1
import { createServer } from "node:http";
2
+
import { parseArgs } from "node:util";
3
+
4
+
const { values } = parseArgs({
5
+
args: process.argv.slice(2),
6
+
options: {
7
+
port: { type: "string", default: "3000" },
8
+
did: { type: "string" },
9
+
rkey: { type: "string" },
10
+
},
11
+
});
2
12
3
-
import { Client, simpleFetchHandler } from "@atcute/client";
13
+
const did = values.did,
14
+
collection = "com.jakelazaroff.test",
15
+
rkey = values.rkey;
16
+
17
+
if (!did) {
18
+
console.error("--did is required.");
19
+
process.exit(1);
20
+
}
4
21
5
-
const did = "did:plc:vrrdgcidwpvn4omvn7uuufoo";
6
-
const pds = "https://bsky.social";
22
+
if (!rkey) {
23
+
console.error("--rkey is required.");
24
+
process.exit(1);
25
+
}
7
26
8
-
const handler = simpleFetchHandler({ service: pds });
9
-
const rpc = new Client({ handler });
27
+
const doc = await resolveDid(did);
28
+
const pds = doc.service[0].serviceEndpoint;
10
29
11
30
const server = createServer(async (req, res) => {
31
+
process.stdout.write(`${req.method} ${req.url} `);
32
+
if (req.method !== "GET") {
33
+
res.statusCode = 405;
34
+
res.end("Method not supported");
35
+
return;
36
+
}
37
+
12
38
// TODO: keep leading slash
13
39
const path = req.url.slice(1);
14
40
15
-
const { data } = await rpc.get("com.atproto.repo.getRecord", {
16
-
params: {
17
-
repo: did,
18
-
collection: "com.jakelazaroff.test",
19
-
rkey: "jakelazaroff.com",
20
-
},
21
-
});
41
+
const record = await getRecord(pds, did, collection, rkey);
22
42
23
-
for (const asset of data.value.assets) {
43
+
for (const asset of record.value.assets) {
24
44
if (asset.path !== path) continue;
25
45
26
-
const url = `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${asset.file.ref.$link}`;
27
-
const response = await fetch(url);
46
+
try {
47
+
const blob = await getBlob(pds, did, asset.file.ref.$link);
28
48
29
-
if (!response.ok) {
30
-
res.statusCode = response.status;
31
-
res.end(`Failed to fetch blob: ${response.statusText}`);
32
-
return;
33
-
}
49
+
if (!blob.ok) {
50
+
console.error(`Upstream error ${blob.status}: ${blob.statusText}`);
51
+
throw new Error("Response not ok");
52
+
}
34
53
35
-
const contentType = response.headers.get("content-type");
36
-
if (contentType) res.setHeader("content-type", contentType);
54
+
if (!blob.body) {
55
+
console.error(`Blob body missing`);
56
+
throw new Error("Blob body missing");
57
+
}
37
58
38
-
if (response.body) {
39
-
const reader = response.body.getReader();
59
+
const contentType = blob.headers.get("content-type") || asset.file.mimeType;
60
+
res.setHeader("content-type", contentType);
40
61
41
-
try {
42
-
while (true) {
43
-
const { done, value } = await reader.read();
44
-
if (done) break;
45
-
res.write(value);
46
-
}
47
-
res.end();
48
-
return;
49
-
} catch (error) {
50
-
res.statusCode = 500;
51
-
res.end("Error streaming blob");
52
-
return;
62
+
const reader = blob.body.getReader();
63
+
while (true) {
64
+
const { done, value } = await reader.read();
65
+
if (done) break;
66
+
res.write(value);
53
67
}
54
-
} else {
55
-
res.statusCode = 500;
56
-
res.end("No response body");
68
+
69
+
res.end();
70
+
process.stdout.write(`${res.statusCode} \n`);
71
+
return;
72
+
} catch (error) {
73
+
res.statusCode = 502;
74
+
res.end("Error streaming blob");
75
+
process.stdout.write(`${res.statusCode} \n`);
57
76
return;
58
77
}
59
78
}
60
79
61
80
res.statusCode = 404;
62
81
res.end("Not Found");
82
+
process.stdout.write(`${res.statusCode} \n`);
63
83
});
64
84
65
-
const PORT = 3000;
85
+
const port = Number.parseInt(values.port) || 3000;
86
+
server.listen(port, () => {
87
+
console.log(`Server running at http://localhost:${port}`);
88
+
console.log(`Proxying to at://${did}/${collection}/${rkey}`);
89
+
console.log("");
90
+
});
91
+
92
+
/** @param {string} did */
93
+
async function resolveDid(did) {
94
+
let url;
95
+
if (did.startsWith("did:web:")) url = `https://${did.slice(8)}/.well-known/did.json`;
96
+
else if (did.startsWith("did:plc:")) url = `https://plc.directory/${did}`;
97
+
else throw new Error(`Unsupported did: ${did}`);
98
+
99
+
const res = await fetch(url);
100
+
const doc = await res.json();
101
+
return doc;
102
+
}
66
103
67
-
server.listen(PORT, () => {
68
-
console.log(`Server running at http://localhost:${PORT}/`);
69
-
});
104
+
/**
105
+
* @param {string} pds
106
+
* @param {string} did
107
+
* @param {string} collection
108
+
* @param {string} rkey
109
+
*/
110
+
async function getRecord(pds, did, collection, rkey) {
111
+
const url = new URL(`${pds}/xrpc/com.atproto.repo.getRecord`);
112
+
url.searchParams.set("repo", did);
113
+
url.searchParams.set("collection", collection);
114
+
url.searchParams.set("rkey", rkey);
115
+
const response = await fetch(url);
116
+
return await response.json();
117
+
}
118
+
119
+
/**
120
+
* @param {string} pds
121
+
* @param {string} did
122
+
* @param {string} collection
123
+
* @param {string} rkey
124
+
*/
125
+
async function getBlob(pds, did, cid) {
126
+
const url = new URL(`${pds}/xrpc/com.atproto.sync.getBlob`);
127
+
url.searchParams.set("did", did);
128
+
url.searchParams.set("cid", cid);
129
+
return await fetch(url);
130
+
}