Resolve your Bluesky avatar with a human URL silhouette.town
atproto deno

init with basic implementation

+389
+1
.envrc
··· 1 + use flake
+21
deno.json
··· 1 + { 2 + "tasks": { 3 + "dev": "deno run --watch main.ts" 4 + }, 5 + "imports": { 6 + "@atcute/atproto": "npm:@atcute/atproto@^3.1.10", 7 + "@atcute/bluesky": "npm:@atcute/bluesky@^3.2.17", 8 + "@atcute/client": "npm:@atcute/client@^4.2.1", 9 + "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.7", 10 + "@atcute/microcosm": "npm:@atcute/microcosm@^1.0.1", 11 + "@std/assert": "jsr:@std/assert@1", 12 + "@std/http": "jsr:@std/http@^1.0.24" 13 + }, 14 + "compilerOptions": { 15 + "types": [ 16 + "@atcute/atproto", 17 + "@atcute/microcosm", 18 + "@atcute/bluesky" 19 + ] 20 + } 21 + }
+153
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "jsr:@std/assert@1": "1.0.18", 5 + "jsr:@std/cli@^1.0.27": "1.0.27", 6 + "jsr:@std/encoding@^1.0.10": "1.0.10", 7 + "jsr:@std/fmt@^1.0.9": "1.0.9", 8 + "jsr:@std/fs@^1.0.22": "1.0.22", 9 + "jsr:@std/html@^1.0.5": "1.0.5", 10 + "jsr:@std/http@^1.0.24": "1.0.24", 11 + "jsr:@std/internal@^1.0.12": "1.0.12", 12 + "jsr:@std/media-types@^1.1.0": "1.1.0", 13 + "jsr:@std/net@^1.0.6": "1.0.6", 14 + "jsr:@std/path@^1.1.4": "1.1.4", 15 + "jsr:@std/streams@^1.0.17": "1.0.17", 16 + "npm:@atcute/atproto@^3.1.10": "3.1.10", 17 + "npm:@atcute/bluesky@^3.2.17": "3.2.17", 18 + "npm:@atcute/client@^4.2.1": "4.2.1", 19 + "npm:@atcute/lexicons@^1.2.7": "1.2.7", 20 + "npm:@atcute/microcosm@^1.0.1": "1.0.1" 21 + }, 22 + "jsr": { 23 + "@std/assert@1.0.18": { 24 + "integrity": "270245e9c2c13b446286de475131dc688ca9abcd94fc5db41d43a219b34d1c78", 25 + "dependencies": [ 26 + "jsr:@std/internal" 27 + ] 28 + }, 29 + "@std/cli@1.0.27": { 30 + "integrity": "eba97edd0891871a7410e835dd94b3c260c709cca5983df2689c25a71fbe04de" 31 + }, 32 + "@std/encoding@1.0.10": { 33 + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 34 + }, 35 + "@std/fmt@1.0.9": { 36 + "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" 37 + }, 38 + "@std/fs@1.0.22": { 39 + "integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308" 40 + }, 41 + "@std/html@1.0.5": { 42 + "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" 43 + }, 44 + "@std/http@1.0.24": { 45 + "integrity": "4dd59afd7cfd6e2e96e175b67a5a829b449ae55f08575721ec691e5d85d886d4", 46 + "dependencies": [ 47 + "jsr:@std/cli", 48 + "jsr:@std/encoding", 49 + "jsr:@std/fmt", 50 + "jsr:@std/fs", 51 + "jsr:@std/html", 52 + "jsr:@std/media-types", 53 + "jsr:@std/net", 54 + "jsr:@std/path", 55 + "jsr:@std/streams" 56 + ] 57 + }, 58 + "@std/internal@1.0.12": { 59 + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 60 + }, 61 + "@std/media-types@1.1.0": { 62 + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" 63 + }, 64 + "@std/net@1.0.6": { 65 + "integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c" 66 + }, 67 + "@std/path@1.1.4": { 68 + "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", 69 + "dependencies": [ 70 + "jsr:@std/internal" 71 + ] 72 + }, 73 + "@std/streams@1.0.17": { 74 + "integrity": "7859f3d9deed83cf4b41f19223d4a67661b3d3819e9fc117698f493bf5992140" 75 + } 76 + }, 77 + "npm": { 78 + "@atcute/atproto@3.1.10": { 79 + "integrity": "sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==", 80 + "dependencies": [ 81 + "@atcute/lexicons" 82 + ] 83 + }, 84 + "@atcute/bluesky@3.2.17": { 85 + "integrity": "sha512-Li+RsPkcRNC6AnNlqOGnlmAcjSwBdXIKFubJL1nwACDngKNXG4ooGL5cvzeekdDEfHmtFhS/tyZNaUx9QXYEUw==", 86 + "dependencies": [ 87 + "@atcute/atproto", 88 + "@atcute/lexicons" 89 + ] 90 + }, 91 + "@atcute/client@4.2.1": { 92 + "integrity": "sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==", 93 + "dependencies": [ 94 + "@atcute/identity", 95 + "@atcute/lexicons" 96 + ] 97 + }, 98 + "@atcute/identity@1.1.3": { 99 + "integrity": "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==", 100 + "dependencies": [ 101 + "@atcute/lexicons", 102 + "@badrap/valita" 103 + ] 104 + }, 105 + "@atcute/lexicons@1.2.7": { 106 + "integrity": "sha512-gCvkSMI1F1zx7xXa59iPiSKMH3L5Hga6iurGqQjaQbE2V/np/2QuDqQzt96TNbWfaFAXE9f9oY+0z3ljf/bweA==", 107 + "dependencies": [ 108 + "@atcute/uint8array", 109 + "@atcute/util-text", 110 + "@standard-schema/spec", 111 + "esm-env" 112 + ] 113 + }, 114 + "@atcute/microcosm@1.0.1": { 115 + "integrity": "sha512-siyreLgOCZ6gT3x5tajTw1MrlR0s4SDNlUvaRYQZrAUZS1xuuLx1Ko/cwsf+/QQzEN6K1wgtTC0J6HqtRZwWVg==", 116 + "dependencies": [ 117 + "@atcute/lexicons" 118 + ] 119 + }, 120 + "@atcute/uint8array@1.1.0": { 121 + "integrity": "sha512-JtHXIVW6LPU9FMWp7SgE4HbUs3uV2WdfkK/2RWdEGjr4EgMV50P3FdU6fPeGlTfDNBJVYMIsuD2wwaKRPV/Aqg==" 122 + }, 123 + "@atcute/util-text@1.1.0": { 124 + "integrity": "sha512-34G9KD5Z9f7oEdFpZOmqrMnU86p8ne6LlxJowfZzKNszRcl1GH+FtEPh3N1woelJT2SkPXMK2anwT8DESTluwA==", 125 + "dependencies": [ 126 + "unicode-segmenter" 127 + ] 128 + }, 129 + "@badrap/valita@0.4.6": { 130 + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" 131 + }, 132 + "@standard-schema/spec@1.1.0": { 133 + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" 134 + }, 135 + "esm-env@1.2.2": { 136 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" 137 + }, 138 + "unicode-segmenter@0.14.5": { 139 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==" 140 + } 141 + }, 142 + "workspace": { 143 + "dependencies": [ 144 + "jsr:@std/assert@1", 145 + "jsr:@std/http@^1.0.24", 146 + "npm:@atcute/atproto@^3.1.10", 147 + "npm:@atcute/bluesky@^3.2.17", 148 + "npm:@atcute/client@^4.2.1", 149 + "npm:@atcute/lexicons@^1.2.7", 150 + "npm:@atcute/microcosm@^1.0.1" 151 + ] 152 + } 153 + }
+59
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1770141374, 6 + "narHash": "sha256-yD4K/vRHPwXbJf5CK3JkptBA6nFWUKNX/jlFp2eKEQc=", 7 + "owner": "NixOS", 8 + "repo": "nixpkgs", 9 + "rev": "41965737c1797c1d83cfb0b644ed0840a6220bd1", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "id": "nixpkgs", 14 + "type": "indirect" 15 + } 16 + }, 17 + "root": { 18 + "inputs": { 19 + "nixpkgs": "nixpkgs", 20 + "utils": "utils" 21 + } 22 + }, 23 + "systems": { 24 + "locked": { 25 + "lastModified": 1681028828, 26 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 27 + "owner": "nix-systems", 28 + "repo": "default", 29 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 30 + "type": "github" 31 + }, 32 + "original": { 33 + "owner": "nix-systems", 34 + "repo": "default", 35 + "type": "github" 36 + } 37 + }, 38 + "utils": { 39 + "inputs": { 40 + "systems": "systems" 41 + }, 42 + "locked": { 43 + "lastModified": 1731533236, 44 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 45 + "owner": "numtide", 46 + "repo": "flake-utils", 47 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 48 + "type": "github" 49 + }, 50 + "original": { 51 + "owner": "numtide", 52 + "repo": "flake-utils", 53 + "type": "github" 54 + } 55 + } 56 + }, 57 + "root": "root", 58 + "version": 7 59 + }
+17
flake.nix
··· 1 + { 2 + inputs = { 3 + utils.url = "github:numtide/flake-utils"; 4 + }; 5 + outputs = { self, nixpkgs, utils }: utils.lib.eachDefaultSystem (system: 6 + let 7 + pkgs = nixpkgs.legacyPackages.${system}; 8 + in 9 + { 10 + devShell = pkgs.mkShell { 11 + buildInputs = with pkgs; [ 12 + deno 13 + ]; 14 + }; 15 + } 16 + ); 17 + }
+132
main.ts
··· 1 + import { STATUS_CODE, STATUS_TEXT } from "@std/http"; 2 + import { Client, simpleFetchHandler } from "@atcute/client"; 3 + import { is } from "@atcute/lexicons"; 4 + import { isActorIdentifier } from "@atcute/lexicons/syntax"; 5 + import { AppBskyActorProfile } from "@atcute/bluesky"; 6 + 7 + const IDENTIFIER_ROUTE = new URLPattern({ pathname: "/avatar/:identifier" }); 8 + const slingshot = new Client({ 9 + handler: simpleFetchHandler({ service: "https://slingshot.microcosm.blue" }), 10 + }); 11 + 12 + // Algorithm: 13 + // 1. Resolve minidoc from identifier in URL 14 + // 2. Resolve Bluesky actor profile from PDS 15 + // 3. Return 303 with profile picture blob URL 16 + 17 + export async function handler(req: Request): Promise<Response> { 18 + const match = IDENTIFIER_ROUTE.exec(req.url); 19 + 20 + if (!match) { 21 + return new Response(null, { 22 + status: STATUS_CODE.NotFound, 23 + statusText: STATUS_TEXT[STATUS_CODE.NotFound], 24 + }); 25 + } else if (req.method !== "GET") { 26 + return new Response(null, { 27 + status: STATUS_CODE.MethodNotAllowed, 28 + statusText: STATUS_TEXT[STATUS_CODE.MethodNotAllowed], 29 + }); 30 + } 31 + 32 + const identifier = match.pathname.groups.identifier; 33 + 34 + if (!isActorIdentifier(identifier)) { 35 + return Response.json({ 36 + identifier, 37 + message: `invalid identifier ${identifier}`, 38 + }, { 39 + status: STATUS_CODE.BadRequest, 40 + statusText: STATUS_TEXT[STATUS_CODE.BadRequest], 41 + }); 42 + } 43 + 44 + const minidoc = await slingshot.get( 45 + "com.bad-example.identity.resolveMiniDoc", 46 + { 47 + params: { 48 + identifier, 49 + }, 50 + }, 51 + ); 52 + 53 + if (!minidoc.ok) { 54 + return Response.json({ 55 + identifier, 56 + message: `failed to resolve identifier ${identifier}`, 57 + }, { 58 + status: STATUS_CODE.InternalServerError, 59 + statusText: STATUS_TEXT[STATUS_CODE.InternalServerError], 60 + }); 61 + } else if (minidoc.status > 400) { 62 + return Response.json({ 63 + identifier, 64 + message: `error from Slingshot`, 65 + error: minidoc.data, 66 + }, { 67 + status: STATUS_CODE.InternalServerError, 68 + statusText: STATUS_TEXT[STATUS_CODE.InternalServerError], 69 + }); 70 + } 71 + 72 + const pds = new Client({ 73 + handler: simpleFetchHandler({ service: minidoc.data.pds }), 74 + }); 75 + 76 + const record = await pds.get("com.atproto.repo.getRecord", { 77 + params: { 78 + collection: "app.bsky.actor.profile", 79 + rkey: "self", 80 + repo: minidoc.data.did, 81 + }, 82 + }); 83 + 84 + if (!record.ok || record.status > 400) { 85 + return Response.json({ 86 + identifier, 87 + pds: minidoc.data.pds, 88 + message: `failed to resolve app.bsky.actor.profile for ${identifier}`, 89 + }, { 90 + status: record.status || STATUS_CODE.InternalServerError, 91 + statusText: STATUS_TEXT[ 92 + STATUS_CODE.InternalServerError 93 + ], 94 + }); 95 + } 96 + 97 + const profile = record.data.value; 98 + if (!is(AppBskyActorProfile.mainSchema, profile)) { 99 + return Response.json({ 100 + identifier, 101 + pds: minidoc.data.pds, 102 + message: `profile record does not match app.bsky.actor.profile`, 103 + profile, 104 + }, { 105 + status: STATUS_CODE.PreconditionFailed, 106 + statusText: STATUS_TEXT[STATUS_CODE.PreconditionFailed], 107 + }); 108 + } else if (!profile.avatar) { 109 + return Response.json({ 110 + identifier, 111 + pds: minidoc.data.pds, 112 + message: "profile has no avatar", 113 + }, { 114 + status: STATUS_CODE.NotFound, 115 + statusText: STATUS_TEXT[STATUS_CODE.NotFound], 116 + }); 117 + } 118 + 119 + const blobUrl = new URL("/xrpc/com.atproto.sync.getBlob", minidoc.data.pds); 120 + const cid = "ref" in profile.avatar 121 + ? profile.avatar.ref.$link 122 + : profile.avatar.cid; 123 + 124 + blobUrl.searchParams.set("did", minidoc.data.did); 125 + blobUrl.searchParams.set("cid", cid); 126 + 127 + return Response.redirect(blobUrl, STATUS_CODE.SeeOther); 128 + } 129 + 130 + if (import.meta.main) { 131 + Deno.serve(handler); 132 + }
+6
main_test.ts
··· 1 + import { assertEquals } from "@std/assert"; 2 + import { add } from "./main.ts"; 3 + 4 + Deno.test(function addTest() { 5 + assertEquals(add(2, 3), 5); 6 + });