tangled
alpha
login
or
join now
dbushell.com
/
attic.social
6
fork
atom
Attic is a cozy space with lofty ambitions.
attic.social
6
fork
atom
overview
issues
pulls
pipelines
cookie constants
dbushell.com
1 week ago
b62ee852
5d78a193
verified
This commit was signed with the committer's
known signature
.
dbushell.com
SSH Key Fingerprint:
SHA256:Sj5AfJ6VbC0PEnnQD2kGGEiGFwHdFBS/ypN5oifzzFI=
+72
-59
7 changed files
expand all
collapse all
unified
split
src
app.d.ts
lib
server
constants.ts
oauth.ts
session.ts
routes
+layout.server.ts
+page.server.ts
oauth
callback
+server.ts
-1
src/app.d.ts
···
15
15
did: Did;
16
16
handle: Handle;
17
17
displayName: string;
18
18
-
avatar: string;
19
18
};
20
19
}
21
20
// interface PageData {}
+3
src/lib/server/constants.ts
···
1
1
+
export const SESSION_COOKIE = "atproto_session";
2
2
+
export const HANDLE_COOKIE = "atproto_handle";
3
3
+
export const OAUTH_COOKIE_PREFIX = "atproto_oauth_";
+2
-1
src/lib/server/oauth.ts
···
10
10
import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node";
11
11
import { OAuthClient, scope, type Store } from "@atcute/oauth-node-client";
12
12
import { decryptText, encryptText } from "$lib/server/crypto.ts";
13
13
+
import { OAUTH_COOKIE_PREFIX } from "$lib/server/constants.ts";
13
14
import { env } from "$env/dynamic/private";
14
15
import { dev } from "$app/environment";
15
16
import { Buffer } from "node:buffer";
16
17
17
18
class CookieStore<K extends string, V> implements Store<K, V> {
18
19
#cookies: Cookies;
19
19
-
#prefix = "atproto_oauth_";
20
20
+
#prefix = OAUTH_COOKIE_PREFIX;
20
21
#maxAge = 60 * 60 * 24 * 7;
21
22
22
23
constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) {
+56
-21
src/lib/server/session.ts
···
3
3
import { isDid, isHandle } from "@atcute/lexicons/syntax";
4
4
import { createOAuthClient } from "$lib/server/oauth.ts";
5
5
import { decryptText } from "./crypto.ts";
6
6
+
import { dev } from "$app/environment";
6
7
import { env } from "$env/dynamic/private";
8
8
+
import { HANDLE_COOKIE, SESSION_COOKIE } from "./constants.ts";
7
9
8
8
-
export const destroySession = async (event: RequestEvent): Promise<void> => {
9
9
-
event.cookies.delete("atproto_session", { path: "/" });
10
10
-
if (event.locals.user === undefined) {
11
11
-
return;
10
10
+
/**
11
11
+
* Logout
12
12
+
*/
13
13
+
export const destroySession = async (
14
14
+
event: RequestEvent,
15
15
+
): Promise<void> => {
16
16
+
event.cookies.delete(SESSION_COOKIE, { path: "/" });
17
17
+
if (event.locals.user) {
18
18
+
try {
19
19
+
const oAuthClient = createOAuthClient(event);
20
20
+
await oAuthClient.revoke(event.locals.user.did);
21
21
+
} catch {
22
22
+
// Do nothing?
23
23
+
}
24
24
+
event.locals.user = undefined;
12
25
}
13
13
-
try {
14
14
-
const oAuthClient = createOAuthClient(event);
15
15
-
// await event.locals.user.session.signOut();
16
16
-
await oAuthClient.revoke(event.locals.user.did);
17
17
-
} catch {
18
18
-
// Do nothing?
26
26
+
event.locals.oAuthClient = undefined;
27
27
+
};
28
28
+
29
29
+
/**
30
30
+
* Login
31
31
+
* @returns {URL} OAuth redirect
32
32
+
*/
33
33
+
export const startSession = async (
34
34
+
event: RequestEvent,
35
35
+
handle: string,
36
36
+
): Promise<URL> => {
37
37
+
if (isHandle(handle) === false) {
38
38
+
throw new Error("invalid handle");
19
39
}
20
20
-
event.locals.oAuthClient = undefined;
40
40
+
const oAuthClient = createOAuthClient(event);
41
41
+
const { url } = await oAuthClient.authorize({
42
42
+
target: { "type": "account", identifier: handle },
43
43
+
});
44
44
+
// Temporary to remember handle across oauth flow
45
45
+
event.cookies.set(
46
46
+
HANDLE_COOKIE,
47
47
+
handle,
48
48
+
{
49
49
+
httpOnly: true,
50
50
+
maxAge: 60 * 10,
51
51
+
path: "/",
52
52
+
sameSite: "lax",
53
53
+
secure: !dev,
54
54
+
},
55
55
+
);
56
56
+
return url;
21
57
};
22
58
59
59
+
/**
60
60
+
* Setup OAuth client from cookies
61
61
+
*/
23
62
export const restoreSession = async (event: RequestEvent): Promise<void> => {
24
63
const { cookies } = event;
25
25
-
// Read the cookie
26
26
-
const encrypted = cookies.get("atproto_session");
64
64
+
const encrypted = cookies.get(SESSION_COOKIE);
27
65
if (encrypted === undefined) {
28
66
return;
29
67
}
30
30
-
// Parse and validate or delete
68
68
+
// Parse and validate or delete cookie
31
69
let data;
32
70
try {
33
71
const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY);
34
72
data = JSON.parse(decrypted);
35
73
} catch {
36
36
-
cookies.delete("atproto_session", { path: "/" });
74
74
+
cookies.delete(SESSION_COOKIE, { path: "/" });
37
75
return;
38
76
}
39
39
-
// [TODO] validate data type?
77
77
+
// [TODO] ArkType data validation?
40
78
try {
41
41
-
if (
42
42
-
isDid(data.did) === false ||
43
43
-
isHandle(data.handle) === false
44
44
-
) {
79
79
+
if (isDid(data.did) === false || isHandle(data.handle) === false) {
45
80
throw new Error();
46
81
}
47
82
const oAuthClient = createOAuthClient(event);
···
53
88
session,
54
89
};
55
90
} catch {
56
56
-
cookies.delete("atproto_session", { path: "/" });
91
91
+
cookies.delete(SESSION_COOKIE, { path: "/" });
57
92
return;
58
93
}
59
94
};
+1
-1
src/routes/+layout.server.ts
···
4
4
let user = undefined;
5
5
if (event.locals.user) {
6
6
user = {
7
7
+
did: event.locals.user.did,
7
8
handle: event.locals.user.handle,
8
9
displayName: event.locals.user.displayName,
9
9
-
avatar: event.locals.user.avatar,
10
10
};
11
11
}
12
12
return {
+5
-24
src/routes/+page.server.ts
···
1
1
import { type Actions, fail, redirect } from "@sveltejs/kit";
2
2
-
import { isActorIdentifier } from "@atcute/lexicons/syntax";
3
3
-
import { createOAuthClient } from "$lib/server/oauth.ts";
4
4
-
import { destroySession } from "../lib/server/session.ts";
5
5
-
import { dev } from "$app/environment";
2
2
+
import { destroySession, startSession } from "$lib/server/session.ts";
6
3
7
4
export const actions = {
8
5
logout: async (event) => {
···
12
9
login: async (event) => {
13
10
const formData = await event.request.formData();
14
11
const handle = formData.get("handle");
15
15
-
if (isActorIdentifier(handle) === false) {
12
12
+
let url: URL;
13
13
+
try {
14
14
+
url = await startSession(event, String(handle ?? ""));
15
15
+
} catch {
16
16
return fail(400, { handle, invalid: true });
17
17
}
18
18
-
const oAuthClient = createOAuthClient(event);
19
19
-
const { url } = await oAuthClient.authorize({
20
20
-
target: {
21
21
-
"type": "account",
22
22
-
identifier: handle,
23
23
-
},
24
24
-
});
25
25
-
// [TODO] encrypt handle necessary?
26
26
-
event.cookies.set(
27
27
-
"atproto_handle",
28
28
-
handle,
29
29
-
{
30
30
-
httpOnly: true,
31
31
-
maxAge: 60 * 5,
32
32
-
path: "/",
33
33
-
sameSite: "lax",
34
34
-
secure: !dev,
35
35
-
},
36
36
-
);
37
18
redirect(303, url);
38
19
},
39
20
} satisfies Actions;
+5
-11
src/routes/oauth/callback/+server.ts
···
5
5
import { redirect } from "@sveltejs/kit";
6
6
import { createOAuthClient } from "$lib/server/oauth.ts";
7
7
import { encryptText } from "$lib/server/crypto.ts";
8
8
+
import { HANDLE_COOKIE, SESSION_COOKIE } from "$lib/server/constants.ts";
8
9
import { env } from "$env/dynamic/private";
9
10
import { dev } from "$app/environment";
10
11
11
12
export const GET: RequestHandler = async (event) => {
12
13
const { url, cookies } = event;
13
14
14
14
-
const handle = cookies.get("atproto_handle");
15
15
+
const handle = cookies.get(HANDLE_COOKIE);
15
16
if (handle === undefined) {
16
17
return redirect(303, "/?error=expired");
17
18
}
18
18
-
cookies.delete("atproto_handle", { path: "/" });
19
19
+
cookies.delete(HANDLE_COOKIE, { path: "/" });
19
20
20
21
let session: OAuthSession;
21
22
try {
···
29
30
const data = {
30
31
handle,
31
32
did: session.did,
32
32
-
displayName: "",
33
33
-
avatar: "",
33
33
+
displayName: handle,
34
34
};
35
35
36
36
try {
···
40
40
params: { actor: session.did },
41
41
}),
42
42
);
43
43
-
// if (profile.handle) {
44
44
-
// data.handle = profile.handle;
45
45
-
// }
46
43
if (profile.displayName) {
47
44
data.displayName = profile.displayName;
48
48
-
}
49
49
-
if (profile.avatar) {
50
50
-
data.avatar = profile.avatar;
51
45
}
52
46
} catch {
53
47
// No Bluesky account?
···
59
53
);
60
54
61
55
cookies.set(
62
62
-
"atproto_session",
56
56
+
SESSION_COOKIE,
63
57
encrypted,
64
58
{
65
59
httpOnly: true,