[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
5
fork

Configure Feed

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

Initial CDN implementation

+908
+187
services/cdn/.dockerignore
··· 1 + 2 + # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 3 + 4 + # Logs 5 + 6 + logs 7 + _.log 8 + npm-debug.log_ 9 + yarn-debug.log* 10 + yarn-error.log* 11 + lerna-debug.log* 12 + .pnpm-debug.log* 13 + 14 + # Caches 15 + 16 + .cache 17 + 18 + # Diagnostic reports (https://nodejs.org/api/report.html) 19 + 20 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 21 + 22 + # Runtime data 23 + 24 + pids 25 + _.pid 26 + _.seed 27 + *.pid.lock 28 + 29 + # Directory for instrumented libs generated by jscoverage/JSCover 30 + 31 + lib-cov 32 + 33 + # Coverage directory used by tools like istanbul 34 + 35 + coverage 36 + *.lcov 37 + 38 + # nyc test coverage 39 + 40 + .nyc_output 41 + 42 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 43 + 44 + .grunt 45 + 46 + # Bower dependency directory (https://bower.io/) 47 + 48 + bower_components 49 + 50 + # node-waf configuration 51 + 52 + .lock-wscript 53 + 54 + # Compiled binary addons (https://nodejs.org/api/addons.html) 55 + 56 + build/Release 57 + 58 + # Dependency directories 59 + 60 + node_modules/ 61 + jspm_packages/ 62 + 63 + # Snowpack dependency directory (https://snowpack.dev/) 64 + 65 + web_modules/ 66 + 67 + # TypeScript cache 68 + 69 + *.tsbuildinfo 70 + 71 + # Optional npm cache directory 72 + 73 + .npm 74 + 75 + # Optional eslint cache 76 + 77 + .eslintcache 78 + 79 + # Optional stylelint cache 80 + 81 + .stylelintcache 82 + 83 + # Microbundle cache 84 + 85 + .rpt2_cache/ 86 + .rts2_cache_cjs/ 87 + .rts2_cache_es/ 88 + .rts2_cache_umd/ 89 + 90 + # Optional REPL history 91 + 92 + .node_repl_history 93 + 94 + # Output of 'npm pack' 95 + 96 + *.tgz 97 + 98 + # Yarn Integrity file 99 + 100 + .yarn-integrity 101 + 102 + # dotenv environment variable files 103 + 104 + .env 105 + .env.development.local 106 + .env.test.local 107 + .env.production.local 108 + .env.local 109 + 110 + # parcel-bundler cache (https://parceljs.org/) 111 + 112 + .parcel-cache 113 + 114 + # Next.js build output 115 + 116 + .next 117 + out 118 + 119 + # Nuxt.js build / generate output 120 + 121 + .nuxt 122 + dist 123 + 124 + # Gatsby files 125 + 126 + # Comment in the public line in if your project uses Gatsby and not Next.js 127 + 128 + # https://nextjs.org/blog/next-9-1#public-directory-support 129 + 130 + # public 131 + 132 + # vuepress build output 133 + 134 + .vuepress/dist 135 + 136 + # vuepress v2.x temp and cache directory 137 + 138 + .temp 139 + 140 + # Docusaurus cache and generated files 141 + 142 + .docusaurus 143 + 144 + # Serverless directories 145 + 146 + .serverless/ 147 + 148 + # FuseBox cache 149 + 150 + .fusebox/ 151 + 152 + # DynamoDB Local files 153 + 154 + .dynamodb/ 155 + 156 + # TernJS port file 157 + 158 + .tern-port 159 + 160 + # Stores VSCode versions used for testing VSCode extensions 161 + 162 + .vscode-test 163 + 164 + # yarn v2 165 + 166 + .yarn/cache 167 + .yarn/unplugged 168 + .yarn/build-state.yml 169 + .yarn/install-state.gz 170 + .pnp.* 171 + 172 + # IntelliJ based IDEs 173 + .idea 174 + 175 + # Finder (MacOS) folder config 176 + .DS_Store 177 + 178 + 179 + node_modules/ 180 + .idea/ 181 + .env 182 + 183 + .git 184 + dist 185 + *.mp4 186 + *.jpeg 187 + *.webp
+183
services/cdn/.gitignore
··· 1 + # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 + 3 + # Logs 4 + 5 + logs 6 + _.log 7 + npm-debug.log_ 8 + yarn-debug.log* 9 + yarn-error.log* 10 + lerna-debug.log* 11 + .pnpm-debug.log* 12 + 13 + # Caches 14 + 15 + .cache 16 + 17 + # Diagnostic reports (https://nodejs.org/api/report.html) 18 + 19 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 + 21 + # Runtime data 22 + 23 + pids 24 + _.pid 25 + _.seed 26 + *.pid.lock 27 + 28 + # Directory for instrumented libs generated by jscoverage/JSCover 29 + 30 + lib-cov 31 + 32 + # Coverage directory used by tools like istanbul 33 + 34 + coverage 35 + *.lcov 36 + 37 + # nyc test coverage 38 + 39 + .nyc_output 40 + 41 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 + 43 + .grunt 44 + 45 + # Bower dependency directory (https://bower.io/) 46 + 47 + bower_components 48 + 49 + # node-waf configuration 50 + 51 + .lock-wscript 52 + 53 + # Compiled binary addons (https://nodejs.org/api/addons.html) 54 + 55 + build/Release 56 + 57 + # Dependency directories 58 + 59 + node_modules/ 60 + jspm_packages/ 61 + 62 + # Snowpack dependency directory (https://snowpack.dev/) 63 + 64 + web_modules/ 65 + 66 + # TypeScript cache 67 + 68 + *.tsbuildinfo 69 + 70 + # Optional npm cache directory 71 + 72 + .npm 73 + 74 + # Optional eslint cache 75 + 76 + .eslintcache 77 + 78 + # Optional stylelint cache 79 + 80 + .stylelintcache 81 + 82 + # Microbundle cache 83 + 84 + .rpt2_cache/ 85 + .rts2_cache_cjs/ 86 + .rts2_cache_es/ 87 + .rts2_cache_umd/ 88 + 89 + # Optional REPL history 90 + 91 + .node_repl_history 92 + 93 + # Output of 'npm pack' 94 + 95 + *.tgz 96 + 97 + # Yarn Integrity file 98 + 99 + .yarn-integrity 100 + 101 + # dotenv environment variable files 102 + 103 + .env 104 + .env.development.local 105 + .env.test.local 106 + .env.production.local 107 + .env.local 108 + 109 + # parcel-bundler cache (https://parceljs.org/) 110 + 111 + .parcel-cache 112 + 113 + # Next.js build output 114 + 115 + .next 116 + out 117 + 118 + # Nuxt.js build / generate output 119 + 120 + .nuxt 121 + dist 122 + 123 + # Gatsby files 124 + 125 + # Comment in the public line in if your project uses Gatsby and not Next.js 126 + 127 + # https://nextjs.org/blog/next-9-1#public-directory-support 128 + 129 + # public 130 + 131 + # vuepress build output 132 + 133 + .vuepress/dist 134 + 135 + # vuepress v2.x temp and cache directory 136 + 137 + .temp 138 + 139 + # Docusaurus cache and generated files 140 + 141 + .docusaurus 142 + 143 + # Serverless directories 144 + 145 + .serverless/ 146 + 147 + # FuseBox cache 148 + 149 + .fusebox/ 150 + 151 + # DynamoDB Local files 152 + 153 + .dynamodb/ 154 + 155 + # TernJS port file 156 + 157 + .tern-port 158 + 159 + # Stores VSCode versions used for testing VSCode extensions 160 + 161 + .vscode-test 162 + 163 + # yarn v2 164 + 165 + .yarn/cache 166 + .yarn/unplugged 167 + .yarn/build-state.yml 168 + .yarn/install-state.gz 169 + .pnp.* 170 + 171 + # IntelliJ based IDEs 172 + .idea 173 + 174 + # Finder (MacOS) folder config 175 + .DS_Store 176 + 177 + node_modules/ 178 + .idea/ 179 + .env 180 + dist 181 + *.mp4 182 + *.jpeg 183 + *.webp
+19
services/cdn/Dockerfile
··· 1 + FROM oven/bun:1-alpine AS builder 2 + 3 + ENV NODE_ENV=production 4 + WORKDIR /app 5 + COPY package.json bun.lock ./ 6 + RUN bun install --frozen-lockfile 7 + COPY . . 8 + RUN bun run build 9 + 10 + FROM oven/bun:1-alpine 11 + WORKDIR /app 12 + COPY --from=builder /app/dist ./dist 13 + COPY --from=builder /app/package.json ./ 14 + COPY --from=builder /app/bun.lock ./ 15 + RUN bun install --production --frozen-lockfile 16 + 17 + ENV NODE_ENV=production 18 + EXPOSE 3000 19 + CMD ["bun", "run", "start"]
+15
services/cdn/README.md
··· 1 + # cdn 2 + 3 + To install dependencies: 4 + 5 + ```bash 6 + bun install 7 + ``` 8 + 9 + To run: 10 + 11 + ```bash 12 + bun dev 13 + ``` 14 + 15 + This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
+142
services/cdn/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "cdn", 6 + "dependencies": { 7 + "@atproto/identity": "^0.4.7", 8 + "hono": "^4.7.5", 9 + "pino": "^9.6.0", 10 + "pino-pretty": "^13.0.0", 11 + }, 12 + "devDependencies": { 13 + "@types/bun": "latest", 14 + "@types/pino": "^7.0.5", 15 + }, 16 + "peerDependencies": { 17 + "typescript": "^5.0.0", 18 + }, 19 + }, 20 + }, 21 + "packages": { 22 + "@atproto/common-web": ["@atproto/common-web@0.4.1", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-Ghh+djHYMAUCktLKwr2IuGgtjcwSWGudp+K7+N7KBA9pDDloOXUEY8Agjc5SHSo9B1QIEFkegClU5n+apn2e0w=="], 23 + 24 + "@atproto/crypto": ["@atproto/crypto@0.4.4", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA=="], 25 + 26 + "@atproto/identity": ["@atproto/identity@0.4.7", "", { "dependencies": { "@atproto/common-web": "^0.4.1", "@atproto/crypto": "^0.4.4" } }, "sha512-A61OT9yc74dEFi1elODt/tzQNSwV3ZGZCY5cRl6NYO9t/0AVdaD+fyt81yh3mRxyI8HeVOecvXl3cPX5knz9rQ=="], 27 + 28 + "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], 29 + 30 + "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], 31 + 32 + "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], 33 + 34 + "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="], 35 + 36 + "@types/pino": ["@types/pino@7.0.5", "", { "dependencies": { "pino": "*" } }, "sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg=="], 37 + 38 + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 39 + 40 + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], 41 + 42 + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], 43 + 44 + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 45 + 46 + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], 47 + 48 + "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], 49 + 50 + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], 51 + 52 + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], 53 + 54 + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], 55 + 56 + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 57 + 58 + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], 59 + 60 + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], 61 + 62 + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], 63 + 64 + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], 65 + 66 + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 67 + 68 + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], 69 + 70 + "hono": ["hono@4.7.5", "", {}, "sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ=="], 71 + 72 + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 73 + 74 + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], 75 + 76 + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 77 + 78 + "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 79 + 80 + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], 81 + 82 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 83 + 84 + "pino": ["pino@9.6.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^4.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg=="], 85 + 86 + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], 87 + 88 + "pino-pretty": ["pino-pretty@13.0.0", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^2.4.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA=="], 89 + 90 + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], 91 + 92 + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], 93 + 94 + "process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], 95 + 96 + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], 97 + 98 + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], 99 + 100 + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], 101 + 102 + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], 103 + 104 + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 105 + 106 + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], 107 + 108 + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], 109 + 110 + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], 111 + 112 + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 113 + 114 + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 115 + 116 + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 117 + 118 + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], 119 + 120 + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], 121 + 122 + "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 123 + 124 + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 125 + 126 + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 127 + 128 + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], 129 + 130 + "@types/pino/pino": ["pino@8.21.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^1.2.0", "pino-std-serializers": "^6.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^3.7.0", "thread-stream": "^2.6.0" }, "bin": { "pino": "bin.js" } }, "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q=="], 131 + 132 + "@types/pino/pino/pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="], 133 + 134 + "@types/pino/pino/pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="], 135 + 136 + "@types/pino/pino/process-warning": ["process-warning@3.0.0", "", {}, "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="], 137 + 138 + "@types/pino/pino/sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], 139 + 140 + "@types/pino/pino/thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], 141 + } 142 + }
+23
services/cdn/package.json
··· 1 + { 2 + "name": "cdn", 3 + "module": "index.ts", 4 + "type": "module", 5 + "scripts": { 6 + "dev": "NODE_ENV=development bun run --hot src/index.ts", 7 + "build": "bun build ./src/index.ts --target bun --outdir ./dist", 8 + "start": "NODE_ENV=production bun run ./dist/index.js" 9 + }, 10 + "devDependencies": { 11 + "@types/bun": "latest", 12 + "@types/pino": "^7.0.5" 13 + }, 14 + "peerDependencies": { 15 + "typescript": "^5.0.0" 16 + }, 17 + "dependencies": { 18 + "@atproto/identity": "^0.4.7", 19 + "hono": "^4.7.5", 20 + "pino": "^9.6.0", 21 + "pino-pretty": "^13.0.0" 22 + } 23 + }
+57
services/cdn/src/id-resolver.ts
··· 1 + import { type AtprotoData, IdResolver, MemoryCache } from '@atproto/identity' 2 + 3 + const HOUR = 60e3 * 60 4 + const DAY = HOUR * 24 5 + 6 + export function createIdResolver() { 7 + return new IdResolver({ 8 + didCache: new MemoryCache(HOUR, DAY), 9 + }) 10 + } 11 + 12 + export interface BidirectionalResolver { 13 + resolveDidToHandle(did: string): Promise<string> 14 + resolveDidToDidDoc(did: string): Promise<AtprotoData> 15 + resolveHandleToDidDoc(handle: string): Promise<AtprotoData> 16 + resolveDidsToHandles(dids: string[]): Promise<Record<string, string>> 17 + } 18 + 19 + export function createBidirectionalResolver(resolver: IdResolver) { 20 + return { 21 + async resolveDidToHandle(did: string): Promise<string> { 22 + const didDoc = await resolver.did.resolveAtprotoData(did) 23 + const resolvedHandle = await resolver.handle.resolve(didDoc.handle) 24 + if (resolvedHandle === did) { 25 + return didDoc.handle 26 + } 27 + return did 28 + }, 29 + 30 + async resolveDidToDidDoc(did: string): Promise<AtprotoData> { 31 + const didDoc = await resolver.did.resolveAtprotoData(did) 32 + return didDoc 33 + }, 34 + 35 + async resolveHandleToDidDoc(handle: string): Promise<AtprotoData> { 36 + const did = await resolver.handle.resolve(handle) 37 + if (!did) { 38 + throw new Error('Handle not found') 39 + } 40 + const didDoc = await resolver.did.resolveAtprotoData(did) 41 + return didDoc 42 + }, 43 + 44 + async resolveDidsToHandles( 45 + dids: string[], 46 + ): Promise<Record<string, string>> { 47 + const didHandleMap: Record<string, string> = {} 48 + const resolves = await Promise.all( 49 + dids.map((did) => this.resolveDidToHandle(did).catch((_) => did)), 50 + ) 51 + for (let i = 0; i < dids.length; i++) { 52 + didHandleMap[dids[i]] = resolves[i] 53 + } 54 + return didHandleMap 55 + }, 56 + } 57 + }
+58
services/cdn/src/index.ts
··· 1 + import { Hono } from 'hono' 2 + import { pino } from 'pino' 3 + import { createBidirectionalResolver, createIdResolver } from './id-resolver' 4 + import { videoHandler } from './videoHandler' 5 + 6 + const logger = pino({ 7 + name: 'cdn', 8 + transport: { 9 + target: 'pino-pretty', 10 + }, 11 + }) 12 + 13 + const port = process.env.PORT ? parseInt(process.env.PORT) : 3000 14 + let app: Hono 15 + 16 + export default { 17 + port, 18 + fetch: (request: Request) => app.fetch(request), 19 + } 20 + 21 + async function main() { 22 + logger.info('Starting Spark CDN service') 23 + 24 + try { 25 + // Create ID resolver 26 + const resolver = createIdResolver() 27 + const bidirectionalResolver = createBidirectionalResolver(resolver) 28 + app = new Hono() 29 + 30 + app.get('/', (c) => { 31 + return c.text( 32 + '✧・゚: ✧・゚:. ݁₊ ⊹ . ݁˖ . ݁ SPARK CDN . ݁₊ ⊹ . ݁˖ . ݁ :・゚✧:・゚✧', 33 + ) 34 + }) 35 + 36 + // Apply video route 37 + app.get('/video/:did/:cid', (c) => videoHandler(c, bidirectionalResolver)) 38 + 39 + logger.info({ port }, 'CDN service is running') 40 + } catch (err) { 41 + logger.error({ err }, 'Failed to start CDN service') 42 + process.exit(1) 43 + } 44 + } 45 + 46 + // Handle shutdown gracefully 47 + const shutdown = () => { 48 + logger.info('Shutting down CDN service...') 49 + process.exit(0) 50 + } 51 + 52 + process.on('SIGINT', shutdown) 53 + process.on('SIGTERM', shutdown) 54 + 55 + main().catch((err) => { 56 + logger.error({ err }, 'Fatal error in main process') 57 + process.exit(1) 58 + })
+197
services/cdn/src/videoHandler.ts
··· 1 + import type { Context } from 'hono' 2 + import { pino } from 'pino' 3 + import type { BidirectionalResolver } from './id-resolver' 4 + 5 + // Get logger instance from parent 6 + const logger = pino({ 7 + name: 'cdn:video', 8 + transport: { 9 + target: 'pino-pretty', 10 + }, 11 + }) 12 + 13 + // In-memory video cache: did:cid -> {buffer, timestamp} 14 + interface CachedVideo { 15 + buffer: ArrayBuffer 16 + timestamp: number 17 + } 18 + 19 + const videoCache = new Map<string, CachedVideo>() 20 + const CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours 21 + 22 + // Fetch video from PDS 23 + export const getVideo = async ( 24 + pdsUrl: URL | string, 25 + did: string, 26 + cid: string, 27 + ) => { 28 + const baseUrl = typeof pdsUrl === 'string' ? new URL(pdsUrl) : pdsUrl 29 + const videoUrl = new URL(`/xrpc/com.atproto.sync.getBlob`, baseUrl) 30 + videoUrl.searchParams.set('did', did) 31 + videoUrl.searchParams.set('cid', cid) 32 + const response = await fetch(videoUrl) 33 + 34 + if (!response.ok) { 35 + throw new Error( 36 + `Failed to fetch video: ${response.status} ${response.statusText}`, 37 + ) 38 + } 39 + 40 + const videoBytes = await response.arrayBuffer() 41 + return videoBytes 42 + } 43 + 44 + // Cleanup old cache entries 45 + export function cleanupCache() { 46 + const now = Date.now() 47 + const keysToDelete: string[] = [] 48 + 49 + videoCache.forEach((entry, key) => { 50 + if (now - entry.timestamp > CACHE_TTL) { 51 + keysToDelete.push(key) 52 + } 53 + }) 54 + 55 + keysToDelete.forEach((key) => { 56 + videoCache.delete(key) 57 + }) 58 + 59 + if (keysToDelete.length > 0) { 60 + logger.info( 61 + { count: keysToDelete.length }, 62 + 'Cleaned up expired cache entries', 63 + ) 64 + } 65 + } 66 + 67 + // Parse range header and return start and end bytes 68 + function parseRangeHeader( 69 + rangeHeader: string, 70 + fileSize: number, 71 + ): { start: number; end: number } | null { 72 + // Check if the range header has the correct format 73 + const matches = rangeHeader.match(/bytes=(\d+)-(\d*)/) 74 + if (!matches) return null 75 + 76 + // Get the start and end bytes 77 + const start = parseInt(matches[1]) 78 + 79 + // If end is not specified, use the file size minus 1 80 + let end = matches[2] ? parseInt(matches[2]) : fileSize - 1 81 + 82 + // Make sure end doesn't exceed file size 83 + end = Math.min(end, fileSize - 1) 84 + 85 + // Validate range 86 + if (start > end || start >= fileSize || end < 0) return null 87 + 88 + return { start, end } 89 + } 90 + 91 + // Video route handler with streaming support 92 + export const videoHandler = async ( 93 + c: Context, 94 + bidirectionalResolver: BidirectionalResolver, 95 + ) => { 96 + const did = c.req.param('did') 97 + const cid = c.req.param('cid') 98 + const cacheKey = `${did}:${cid}` 99 + 100 + try { 101 + // Get range header if present 102 + const rangeHeader = c.req.header('range') 103 + 104 + // Get video buffer (from cache or source) 105 + let videoBuffer: ArrayBuffer 106 + let fromCache = false 107 + 108 + // Check if video is in cache 109 + const cachedEntry = videoCache.get(cacheKey) 110 + if (cachedEntry) { 111 + logger.info({ did, cid, cached: true }, 'Found video in cache') 112 + videoBuffer = cachedEntry.buffer 113 + fromCache = true 114 + } else { 115 + // Resolve DID to find PDS URL 116 + logger.info({ did, cid }, 'Resolving DID to find PDS') 117 + const didDoc = await bidirectionalResolver.resolveDidToDidDoc(did) 118 + 119 + if (!didDoc?.pds) { 120 + logger.error({ did }, 'PDS not found for DID') 121 + return c.json({ error: 'PDS not found for DID' }, 404) 122 + } 123 + 124 + // Fetch video from PDS 125 + logger.info( 126 + { pds: didDoc.pds.toString(), did, cid }, 127 + 'Fetching video from PDS', 128 + ) 129 + videoBuffer = await getVideo(didDoc.pds, did, cid) 130 + 131 + // Cache the video 132 + videoCache.set(cacheKey, { 133 + buffer: videoBuffer, 134 + timestamp: Date.now(), 135 + }) 136 + 137 + // Cleanup old cache entries 138 + cleanupCache() 139 + } 140 + 141 + const fileSize = videoBuffer.byteLength 142 + 143 + // Set common headers 144 + c.header('Accept-Ranges', 'bytes') 145 + c.header('Content-Type', 'video/mp4') 146 + c.header('ETag', cid) 147 + 148 + // If no range requested, send entire file 149 + if (!rangeHeader) { 150 + logger.info( 151 + { did, cid, size: fileSize, cached: fromCache }, 152 + 'Serving full video', 153 + ) 154 + c.header('Content-Length', fileSize.toString()) 155 + c.header('Cache-Control', 'public, max-age=86400') 156 + return c.body(videoBuffer) 157 + } 158 + 159 + // Parse range header 160 + const range = parseRangeHeader(rangeHeader, fileSize) 161 + 162 + if (!range) { 163 + logger.warn({ did, cid, rangeHeader }, 'Invalid range header') 164 + c.status(416) // Range Not Satisfiable 165 + c.header('Content-Range', `bytes */${fileSize}`) 166 + return c.body(null) 167 + } 168 + 169 + const { start, end } = range 170 + const chunkSize = end - start + 1 171 + 172 + logger.info( 173 + { 174 + did, 175 + cid, 176 + range: `${start}-${end}`, 177 + size: chunkSize, 178 + cached: fromCache, 179 + }, 180 + 'Serving partial video content', 181 + ) 182 + 183 + // Create a new buffer with only the requested bytes 184 + const slicedBuffer = videoBuffer.slice(start, end + 1) 185 + 186 + // Set partial content headers 187 + c.status(206) // Partial Content 188 + c.header('Content-Range', `bytes ${start}-${end}/${fileSize}`) 189 + c.header('Content-Length', chunkSize.toString()) 190 + c.header('Cache-Control', 'public, max-age=86400') 191 + 192 + return c.body(slicedBuffer) 193 + } catch (err) { 194 + logger.error({ err, did, cid }, 'Error serving video') 195 + return c.json({ error: 'Error serving video' }, 500) 196 + } 197 + }
+27
services/cdn/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Enable latest features 4 + "lib": ["ESNext", "DOM"], 5 + "target": "ESNext", 6 + "module": "ESNext", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + 22 + // Some stricter flags (disabled by default) 23 + "noUnusedLocals": false, 24 + "noUnusedParameters": false, 25 + "noPropertyAccessFromIndexSignature": false 26 + } 27 + }