+69
proxy.js
+69
proxy.js
···
1
+
import { createServer } from "node:http";
2
+
3
+
import { Client, simpleFetchHandler } from "@atcute/client";
4
+
5
+
const did = "did:plc:vrrdgcidwpvn4omvn7uuufoo";
6
+
const pds = "https://bsky.social";
7
+
8
+
const handler = simpleFetchHandler({ service: pds });
9
+
const rpc = new Client({ handler });
10
+
11
+
const server = createServer(async (req, res) => {
12
+
// TODO: keep leading slash
13
+
const path = req.url.slice(1);
14
+
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
+
});
22
+
23
+
for (const asset of data.value.assets) {
24
+
if (asset.path !== path) continue;
25
+
26
+
const url = `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${asset.file.ref.$link}`;
27
+
const response = await fetch(url);
28
+
29
+
if (!response.ok) {
30
+
res.statusCode = response.status;
31
+
res.end(`Failed to fetch blob: ${response.statusText}`);
32
+
return;
33
+
}
34
+
35
+
const contentType = response.headers.get("content-type");
36
+
if (contentType) res.setHeader("content-type", contentType);
37
+
38
+
if (response.body) {
39
+
const reader = response.body.getReader();
40
+
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;
53
+
}
54
+
} else {
55
+
res.statusCode = 500;
56
+
res.end("No response body");
57
+
return;
58
+
}
59
+
}
60
+
61
+
res.statusCode = 404;
62
+
res.end("Not Found");
63
+
});
64
+
65
+
const PORT = 3000;
66
+
67
+
server.listen(PORT, () => {
68
+
console.log(`Server running at http://localhost:${PORT}/`);
69
+
});
+7
-1
src/lib/oauth.ts
+7
-1
src/lib/oauth.ts
···
1
1
import { PUBLIC_HOST, PUBLIC_REDIRECT_HOST } from "$env/static/public";
2
2
3
-
import { configureOAuth } from "@atcute/oauth-browser-client";
3
+
import { Client } from "@atcute/client";
4
+
import { configureOAuth, OAuthUserAgent, type Session } from "@atcute/oauth-browser-client";
4
5
5
6
export const CLIENT_ID = `${PUBLIC_HOST}/client-metadata.json`;
6
7
export const REDIRECT_URI = `${PUBLIC_REDIRECT_HOST || PUBLIC_HOST}/oauth/callback`;
···
8
9
export function configure() {
9
10
configureOAuth({ metadata: { client_id: CLIENT_ID, redirect_uri: REDIRECT_URI } });
10
11
}
12
+
13
+
export function client(session: Session) {
14
+
const handler = new OAuthUserAgent(session);
15
+
return new Client({ handler });
16
+
}
+10
-2
src/routes/~/+layout.svelte
+10
-2
src/routes/~/+layout.svelte
···
17
17
}
18
18
</script>
19
19
20
-
<header>
21
-
<h1>hello, {data.name}</h1>
20
+
<header class="header">
21
+
<h1><a href="/~/">hello, {data.name}</a></h1>
22
22
<button onclick={logOut}>log out</button>
23
23
</header>
24
24
25
25
<div class="body">
26
26
{@render children?.()}
27
27
</div>
28
+
29
+
<style>
30
+
.header {
31
+
display: flex;
32
+
justify-content: space-between;
33
+
align-items: center;
34
+
}
35
+
</style>
+19
-11
src/routes/~/+page.svelte
+19
-11
src/routes/~/+page.svelte
···
1
1
<script lang="ts">
2
+
import { invalidate } from "$app/navigation";
3
+
2
4
import type {} from "@atcute/atproto";
3
5
import { Client } from "@atcute/client";
4
6
import { OAuthUserAgent } from "@atcute/oauth-browser-client";
···
10
12
11
13
async function createWebsite(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
12
14
e.preventDefault();
13
-
const form = new FormData(e.currentTarget);
15
+
const form = e.currentTarget;
16
+
const formdata = new FormData(e.currentTarget);
14
17
15
-
const name = form.get("name");
18
+
const name = formdata.get("name");
16
19
if (typeof name !== "string") throw new Error("invalid name");
17
20
18
-
await rpc.post("com.atproto.repo.putRecord", {
19
-
input: {
20
-
repo: data.did,
21
-
collection: "com.jakelazaroff.test",
22
-
rkey: name,
23
-
record: {},
24
-
},
21
+
const record = {
22
+
assets: [],
23
+
createdAt: new Date().toISOString(),
24
+
};
25
+
26
+
await rpc.post("com.atproto.repo.createRecord", {
27
+
input: { repo: data.did, collection: "com.jakelazaroff.test", rkey: name, record },
25
28
});
29
+
await invalidate("collection:com.jakelazaroff.test");
30
+
form.reset();
26
31
}
27
32
</script>
28
33
29
34
<form onsubmit={createWebsite}>
30
-
<input type="text" name="name" />
35
+
<input type="text" name="name" minlength={1} maxlength={512} pattern="[A-Za-z0-9.\-]+" />
31
36
<button>create</button>
32
37
</form>
33
38
34
39
<ul>
35
40
{#each data.records as record}
36
-
<li>{record.uri}</li>
41
+
{@const rkey = record.uri.split("/").at(-1) || ""}
42
+
<li>
43
+
<a href="/~/sites/{rkey}">{rkey}</a>
44
+
</li>
37
45
{/each}
38
46
</ul>
+4
-2
src/routes/~/+page.ts
+4
-2
src/routes/~/+page.ts
···
1
1
import type {} from "@atcute/atproto";
2
2
import { Client, isXRPCErrorPayload } from "@atcute/client";
3
-
import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
3
+
import { OAuthUserAgent } from "@atcute/oauth-browser-client";
4
4
5
5
import { configure } from "~/lib/oauth";
6
6
···
9
9
10
10
configure();
11
11
12
-
export const load: PageLoad = async ({ parent }) => {
12
+
export const load: PageLoad = async ({ parent, depends }) => {
13
+
depends("collection:com.jakelazaroff.test");
14
+
13
15
try {
14
16
const { did, session } = await parent();
15
17
+64
src/routes/~/sites/[name]/+page.svelte
+64
src/routes/~/sites/[name]/+page.svelte
···
1
+
<script lang="ts">
2
+
import { invalidate } from "$app/navigation";
3
+
4
+
import type {} from "@atcute/atproto";
5
+
import { isXRPCErrorPayload } from "@atcute/client";
6
+
7
+
import { client } from "~/lib/oauth";
8
+
9
+
let { params, data } = $props();
10
+
11
+
const rpc = client(data.session);
12
+
13
+
async function deleteWebsite(rkey: string) {
14
+
await rpc.post("com.atproto.repo.deleteRecord", {
15
+
input: { repo: data.did, collection: "com.jakelazaroff.test", rkey },
16
+
});
17
+
}
18
+
19
+
async function deployBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
20
+
e.preventDefault();
21
+
const form = e.currentTarget;
22
+
const formdata = new FormData(form);
23
+
24
+
const name = formdata.get("name");
25
+
if (typeof name !== "string") throw new Error("invalid name");
26
+
27
+
const files = formdata.getAll("files");
28
+
const assets: { path: string; file: any }[] = [];
29
+
for (const file of files) {
30
+
if (!(file instanceof File)) continue;
31
+
32
+
const { data } = await rpc.post("com.atproto.repo.uploadBlob", { input: file });
33
+
if (isXRPCErrorPayload(data)) throw new Error("couldn't upload file");
34
+
35
+
assets.push({
36
+
path: file.name,
37
+
file: { $type: "blob", ref: data.blob.ref, mimeType: file.type, size: file.size },
38
+
});
39
+
}
40
+
41
+
const record = { assets, createdAt: new Date().toISOString() };
42
+
await rpc.post("com.atproto.repo.putRecord", {
43
+
input: { repo: data.did, collection: "com.jakelazaroff.test", rkey: name, record },
44
+
});
45
+
if (isXRPCErrorPayload(data)) throw new Error("couldn't deploy");
46
+
}
47
+
</script>
48
+
49
+
<header class="header">
50
+
<h2>{params.name}</h2>
51
+
<button onclick={() => deleteWebsite(params.name)}>delete</button>
52
+
</header>
53
+
54
+
<ul>
55
+
{#each data.record.value.assets as asset}
56
+
<li>{asset.path}</li>
57
+
{/each}
58
+
</ul>
59
+
60
+
<form onsubmit={deployBundle}>
61
+
<input type="hidden" name="name" value={params.name} />
62
+
<input type="file" name="files" />
63
+
<button>upload</button>
64
+
</form>
+30
src/routes/~/sites/[name]/+page.ts
+30
src/routes/~/sites/[name]/+page.ts
···
1
+
import type {} from "@atcute/atproto";
2
+
import { isXRPCErrorPayload } from "@atcute/client";
3
+
4
+
import { client, configure } from "~/lib/oauth";
5
+
6
+
import type { PageLoad } from "./$types";
7
+
import { redirect } from "@sveltejs/kit";
8
+
9
+
configure();
10
+
11
+
export const load: PageLoad = async ({ parent, params, depends }) => {
12
+
depends("collection:com.jakelazaroff.test");
13
+
14
+
try {
15
+
const { did, session } = await parent();
16
+
17
+
const rpc = client(session);
18
+
19
+
const { data } = await rpc.get("com.atproto.repo.getRecord", {
20
+
params: { repo: did, collection: "com.jakelazaroff.test", rkey: params.name },
21
+
});
22
+
23
+
if (isXRPCErrorPayload(data)) throw new Error("couldn't load records");
24
+
25
+
return { record: data };
26
+
} catch (e) {
27
+
console.error(e);
28
+
redirect(303, "/");
29
+
}
30
+
};