Attic is a cozy space with lofty ambitions. attic.social
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

valibot user data

+76 -28
+2 -1
package.json
··· 28 28 "@atcute/identity-resolver-node": "^1.0.3", 29 29 "@atcute/lexicons": "^1.2.9", 30 30 "@atcute/oauth-node-client": "^1.1.0", 31 - "@types/node": "^25.3.3" 31 + "@types/node": "^25.3.3", 32 + "valibot": "^1.2.0" 32 33 } 33 34 }
+15
pnpm-lock.yaml
··· 32 32 '@types/node': 33 33 specifier: ^25.3.3 34 34 version: 25.3.3 35 + valibot: 36 + specifier: ^1.2.0 37 + version: 1.2.0(typescript@5.9.3) 35 38 devDependencies: 36 39 '@sveltejs/adapter-auto': 37 40 specifier: ^7.0.0 ··· 637 640 unicode-segmenter@0.14.5: 638 641 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 639 642 643 + valibot@1.2.0: 644 + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} 645 + peerDependencies: 646 + typescript: '>=5' 647 + peerDependenciesMeta: 648 + typescript: 649 + optional: true 650 + 640 651 vite@7.3.1: 641 652 resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 642 653 engines: {node: ^20.19.0 || >=22.12.0} ··· 1189 1200 undici-types@7.18.2: {} 1190 1201 1191 1202 unicode-segmenter@0.14.5: {} 1203 + 1204 + valibot@1.2.0(typescript@5.9.3): 1205 + optionalDependencies: 1206 + typescript: 5.9.3 1192 1207 1193 1208 vite@7.3.1(@types/node@25.3.3): 1194 1209 dependencies:
+3 -10
src/app.d.ts
··· 1 - import type { OAuthClient, OAuthSession } from "@atcute/oauth-node-client"; 2 - import type { Client } from "@atcute/client"; 3 - import type { Did, Handle } from "@atcute/lexicons"; 1 + import type { OAuthClient } from "@atcute/oauth-node-client"; 2 + import type { PrivateUserData } from "$lib/valibot.ts"; 4 3 5 4 // See https://svelte.dev/docs/kit/types#app.d.ts 6 5 // for information about these interfaces ··· 9 8 // interface Error {} 10 9 interface Locals { 11 10 oAuthClient?: OAuthClient; 12 - user?: { 13 - client: Client; 14 - session: OAuthSession; 15 - did: Did; 16 - handle: Handle; 17 - displayName: string; 18 - }; 11 + user?: PrivateUserData; 19 12 } 20 13 // interface PageData {} 21 14 // interface PageState {}
+3 -1
src/lib/server/constants.ts
··· 1 + export const OAUTH_MAX_AGE = 60 * 10; 2 + export const SESSION_MAX_AGE = 60 * 60 * 24 * 7; 3 + export const OAUTH_COOKIE_PREFIX = "atproto_oauth_"; 1 4 export const SESSION_COOKIE = "atproto_session"; 2 5 export const HANDLE_COOKIE = "atproto_handle"; 3 - export const OAUTH_COOKIE_PREFIX = "atproto_oauth_";
+7 -3
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 - import { OAUTH_COOKIE_PREFIX } from "$lib/server/constants.ts"; 13 + import { 14 + OAUTH_COOKIE_PREFIX, 15 + OAUTH_MAX_AGE, 16 + SESSION_MAX_AGE, 17 + } from "$lib/server/constants.ts"; 14 18 import { env } from "$env/dynamic/private"; 15 19 import { dev } from "$app/environment"; 16 20 import { Buffer } from "node:buffer"; ··· 18 22 class CookieStore<K extends string, V> implements Store<K, V> { 19 23 #cookies: Cookies; 20 24 #prefix = OAUTH_COOKIE_PREFIX; 21 - #maxAge = 60 * 60 * 24 * 7; 25 + #maxAge = SESSION_MAX_AGE; 22 26 23 27 constructor(event: { cookies: Cookies }, options?: { maxAge?: number }) { 24 28 this.#cookies = event.cookies; ··· 116 120 }), 117 121 stores: { 118 122 sessions: new CookieStore(event), 119 - states: new CookieStore(event, { maxAge: 60 * 10 }), 123 + states: new CookieStore(event, { maxAge: OAUTH_MAX_AGE }), 120 124 }, 121 125 }); 122 126
+11 -10
src/lib/server/session.ts
··· 1 1 import type { RequestEvent } from "@sveltejs/kit"; 2 2 import { Client } from "@atcute/client"; 3 - import { isDid, isHandle } from "@atcute/lexicons/syntax"; 3 + import { isHandle } from "@atcute/lexicons/syntax"; 4 4 import { createOAuthClient } from "$lib/server/oauth.ts"; 5 - import { decryptText } from "./crypto.ts"; 5 + import { decryptText } from "$lib/server/crypto.ts"; 6 + import { 7 + HANDLE_COOKIE, 8 + OAUTH_MAX_AGE, 9 + SESSION_COOKIE, 10 + } from "$lib/server/constants.ts"; 11 + import { parsePublicUser, type PublicUserData } from "$lib/valibot.ts"; 6 12 import { dev } from "$app/environment"; 7 13 import { env } from "$env/dynamic/private"; 8 - import { HANDLE_COOKIE, SESSION_COOKIE } from "./constants.ts"; 9 14 10 15 /** 11 16 * Logout ··· 47 52 handle, 48 53 { 49 54 httpOnly: true, 50 - maxAge: 60 * 10, 55 + maxAge: OAUTH_MAX_AGE, 51 56 path: "/", 52 57 sameSite: "lax", 53 58 secure: !dev, ··· 66 71 return; 67 72 } 68 73 // Parse and validate or delete cookie 69 - let data; 74 + let data: PublicUserData; 70 75 try { 71 76 const decrypted = await decryptText(encrypted, env.PRIVATE_COOKIE_KEY); 72 - data = JSON.parse(decrypted); 77 + data = parsePublicUser(JSON.parse(decrypted)); 73 78 } catch { 74 79 cookies.delete(SESSION_COOKIE, { path: "/" }); 75 80 return; 76 81 } 77 - // [TODO] ArkType data validation? 78 82 try { 79 - if (isDid(data.did) === false || isHandle(data.handle) === false) { 80 - throw new Error(); 81 - } 82 83 const oAuthClient = createOAuthClient(event); 83 84 const session = await oAuthClient.restore(data.did); 84 85 const client = new Client({ handler: session });
+29
src/lib/valibot.ts
··· 1 + import * as v from "valibot"; 2 + import { Client } from "@atcute/client"; 3 + import { OAuthSession } from "@atcute/oauth-node-client"; 4 + import { isDid, isHandle } from "@atcute/lexicons/syntax"; 5 + import type { Did, Handle } from "@atcute/lexicons"; 6 + 7 + const UserSchema = { 8 + did: v.custom<Did>(isDid, "invalid did"), 9 + handle: v.custom<Handle>(isHandle, "invalid handle"), 10 + displayName: v.string(), 11 + }; 12 + 13 + export const PublicUserSchema = v.object(UserSchema); 14 + export type PublicUserData = v.InferOutput<typeof PublicUserSchema>; 15 + 16 + export const PrivateUserSchema = v.object({ 17 + ...UserSchema, 18 + client: v.instance(Client), 19 + session: v.instance(OAuthSession), 20 + }); 21 + export type PrivateUserData = v.InferOutput<typeof PrivateUserSchema>; 22 + 23 + export function parsePublicUser(data: unknown): PublicUserData { 24 + return v.parse(PublicUserSchema, data); 25 + } 26 + 27 + export function parsePrivateUser(data: unknown): PrivateUserData { 28 + return v.parse(PrivateUserSchema, data); 29 + }
+2 -1
src/routes/+layout.server.ts
··· 1 1 import type { LayoutServerLoad } from "./$types"; 2 + import type { PublicUserData } from "$lib/valibot.ts"; 2 3 3 4 export const load: LayoutServerLoad = (event) => { 4 - let user = undefined; 5 + let user: PublicUserData | undefined = undefined; 5 6 if (event.locals.user) { 6 7 user = { 7 8 did: event.locals.user.did,
+4 -2
src/routes/oauth/callback/+server.ts
··· 8 8 import { HANDLE_COOKIE, SESSION_COOKIE } from "$lib/server/constants.ts"; 9 9 import { env } from "$env/dynamic/private"; 10 10 import { dev } from "$app/environment"; 11 + import { isHandle } from "@atcute/lexicons/syntax"; 12 + import type { PublicUserData } from "$lib/valibot.ts"; 11 13 12 14 export const GET: RequestHandler = async (event) => { 13 15 const { url, cookies } = event; 14 16 15 17 const handle = cookies.get(HANDLE_COOKIE); 16 - if (handle === undefined) { 18 + if (isHandle(handle) === false) { 17 19 return redirect(303, "/?error=expired"); 18 20 } 19 21 cookies.delete(HANDLE_COOKIE, { path: "/" }); ··· 27 29 redirect(303, "/?error=session"); 28 30 } 29 31 30 - const data = { 32 + const data: PublicUserData = { 31 33 handle, 32 34 did: session.did, 33 35 displayName: handle,