My website
0
fork

Configure Feed

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

Initial commit

Hono + JSX personal site with ATProtocol-backed blog content,
dark/light theme toggle, and Fly.io deployment config.

🐾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

Cameron ac4204c3

+6557
+5
.dockerignore
··· 1 + node_modules 2 + .env 3 + scripts 4 + *.md 5 + .git
+5
.gitignore
··· 1 + node_modules 2 + .env 3 + dist 4 + *.log 5 + .DS_Store
+18
Dockerfile
··· 1 + FROM node:22-slim 2 + 3 + RUN npm install -g tsx 4 + 5 + WORKDIR /app 6 + 7 + COPY package.json ./ 8 + RUN npm install --omit=dev 9 + 10 + # Copy app source 11 + COPY src/ src/ 12 + COPY public/ public/ 13 + COPY tsconfig.json ./ 14 + 15 + ENV PORT=8080 16 + EXPOSE 8080 17 + 18 + CMD ["tsx", "--conditions", "source", "src/index.tsx"]
+86
README.md
··· 1 + # cameron.stream 2 + 3 + Personal site for [cameron.stream](https://cameron.stream). Built with Hono + JSX, server-rendered, deployed to Fly.io. Blog content is stored as `site.standard.document` records on ATProtocol (PDS), not as local files. 4 + 5 + ## Setup 6 + 7 + ```bash 8 + pnpm install 9 + ``` 10 + 11 + Create a `.env` file in the project root: 12 + 13 + ``` 14 + ATP_IDENTIFIER=cameron.stream 15 + ATP_PASSWORD=<bluesky-app-password> 16 + ``` 17 + 18 + The following are set in `fly.toml` for production but can be overridden locally: 19 + 20 + | Variable | Default | Description | 21 + |----------|---------|-------------| 22 + | `CAMERON_DID` | `did:plc:gfrmhdmjvxn2sjedzboeudef` | Author DID | 23 + | `PUBLICATION_URI` | (see fly.toml) | AT-URI of the `site.standard.publication` record | 24 + | `PORT` | `3002` (dev) / `8080` (prod) | Server port | 25 + | `HOST` | `0.0.0.0` | Server bind address | 26 + | `SITE_URL` | `https://cameron.stream` | Canonical site URL (used for OG tags) | 27 + | `SEMBLE_HANDLE` | `cameron.stream` | Bluesky handle for Semble page | 28 + | `ENABLE_MARGIN` | unset | Set to `"true"` to enable margin notes | 29 + 30 + ## Development 31 + 32 + ```bash 33 + pnpm dev 34 + ``` 35 + 36 + Starts the dev server at `http://localhost:3002` with file watching. 37 + 38 + ## Type checking 39 + 40 + ```bash 41 + pnpm typecheck 42 + ``` 43 + 44 + ## Publishing blog posts 45 + 46 + Blog posts are published as `site.standard.document` records to your PDS: 47 + 48 + ```bash 49 + pnpm publish 50 + ``` 51 + 52 + This runs `scripts/publish.ts`, which reads markdown files and pushes them as AT Protocol records. 53 + 54 + ## Deployment 55 + 56 + Deployed to Fly.io as `cameron-stream`: 57 + 58 + ```bash 59 + fly deploy 60 + ``` 61 + 62 + Secrets (`ATP_IDENTIFIER`, `ATP_PASSWORD`) must be set via `fly secrets set`. 63 + 64 + ## Stack 65 + 66 + - **Hono** -- HTTP framework with JSX support 67 + - **tsx** -- TypeScript execution (no build step) 68 + - **marked** + **shiki** -- Markdown rendering with syntax highlighting 69 + - **@atproto/api** -- ATProtocol client for PDS reads/writes 70 + - **ioredis** -- Optional caching layer 71 + 72 + ## Project structure 73 + 74 + ``` 75 + src/ 76 + index.tsx -- Routes and page shell 77 + data.ts -- Data fetching (PDS, Bluesky API) 78 + markdown.ts -- Markdown rendering pipeline 79 + cache.ts -- Redis/memory cache 80 + components/ -- Page components (JSX) 81 + public/ 82 + site.css -- Main stylesheet 83 + host-primitives.css 84 + host-theme.css 85 + theme.js -- Dark/light mode toggle 86 + ```
+21
fly.toml
··· 1 + app = "cameron-stream" 2 + primary_region = "sjc" 3 + 4 + [build] 5 + 6 + [env] 7 + CAMERON_DID = "did:plc:gfrmhdmjvxn2sjedzboeudef" 8 + PUBLICATION_URI = "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3md7ylshxzk2y" 9 + PORT = "8080" 10 + 11 + [http_service] 12 + internal_port = 8080 13 + force_https = true 14 + auto_stop_machines = "stop" 15 + auto_start_machines = true 16 + min_machines_running = 0 17 + 18 + [[vm]] 19 + memory = "256mb" 20 + cpu_kind = "shared" 21 + cpus = 1
+53
lexicons/stream/cameron/blog/post.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "stream.cameron.blog.post", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "any", 8 + "description": "A blog post on cameron.stream.", 9 + "record": { 10 + "type": "object", 11 + "required": ["title", "body", "createdAt"], 12 + "properties": { 13 + "title": { 14 + "type": "string", 15 + "maxGraphemes": 300, 16 + "maxLength": 3000, 17 + "description": "Post title" 18 + }, 19 + "body": { 20 + "type": "string", 21 + "maxGraphemes": 100000, 22 + "maxLength": 1000000, 23 + "description": "Post body in Markdown" 24 + }, 25 + "summary": { 26 + "type": "string", 27 + "maxGraphemes": 1000, 28 + "maxLength": 10000, 29 + "description": "Short summary for previews and RSS" 30 + }, 31 + "tags": { 32 + "type": "array", 33 + "items": { 34 + "type": "string", 35 + "maxGraphemes": 64, 36 + "maxLength": 640 37 + }, 38 + "maxLength": 20, 39 + "description": "Categorization tags" 40 + }, 41 + "createdAt": { 42 + "type": "string", 43 + "format": "datetime" 44 + }, 45 + "updatedAt": { 46 + "type": "string", 47 + "format": "datetime" 48 + } 49 + } 50 + } 51 + } 52 + } 53 + }
+2155
package-lock.json
··· 1 + { 2 + "name": "cameron-site", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "name": "cameron-site", 8 + "dependencies": { 9 + "@atproto/api": "^0.18.17", 10 + "@atproto/lex-resolver": "^0.0.15", 11 + "@atproto/syntax": "^0.3.0", 12 + "@hono/node-server": "^1.19.9", 13 + "dotenv": "^16.5.0", 14 + "gray-matter": "^4.0.3", 15 + "hono": "^4.12.2", 16 + "ioredis": "^5.6.0", 17 + "marked": "^15.0.0", 18 + "marked-footnote": "^1.4.0", 19 + "shiki": "^3.0.0" 20 + }, 21 + "devDependencies": { 22 + "@types/node": "^22.0.0", 23 + "tsx": "^4.0.0", 24 + "typescript": "^5.9.0" 25 + }, 26 + "engines": { 27 + "node": ">=22.0.0" 28 + } 29 + }, 30 + "../inlay/packages/@inlay/core": { 31 + "version": "0.0.13", 32 + "extraneous": true, 33 + "license": "MIT", 34 + "dependencies": { 35 + "@atproto/lex": "^0.0.18", 36 + "@atproto/syntax": "^0.4.3" 37 + }, 38 + "devDependencies": { 39 + "typescript": "^5.9.0" 40 + } 41 + }, 42 + "../inlay/packages/@inlay/render": { 43 + "version": "0.3.1", 44 + "extraneous": true, 45 + "dependencies": { 46 + "@atproto/lexicon": "^0.6.1", 47 + "@atproto/syntax": "^0.4.3" 48 + }, 49 + "devDependencies": { 50 + "typescript": "^5.9.0" 51 + }, 52 + "peerDependencies": { 53 + "@inlay/core": "*" 54 + } 55 + }, 56 + "node_modules/@atproto-labs/did-resolver": { 57 + "version": "0.2.6", 58 + "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.6.tgz", 59 + "integrity": "sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg==", 60 + "license": "MIT", 61 + "dependencies": { 62 + "@atproto-labs/fetch": "0.2.3", 63 + "@atproto-labs/pipe": "0.1.1", 64 + "@atproto-labs/simple-store": "0.3.0", 65 + "@atproto-labs/simple-store-memory": "0.1.4", 66 + "@atproto/did": "0.3.0", 67 + "zod": "^3.23.8" 68 + } 69 + }, 70 + "node_modules/@atproto-labs/fetch": { 71 + "version": "0.2.3", 72 + "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz", 73 + "integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==", 74 + "license": "MIT", 75 + "dependencies": { 76 + "@atproto-labs/pipe": "0.1.1" 77 + } 78 + }, 79 + "node_modules/@atproto-labs/pipe": { 80 + "version": "0.1.1", 81 + "resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz", 82 + "integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==", 83 + "license": "MIT" 84 + }, 85 + "node_modules/@atproto-labs/simple-store": { 86 + "version": "0.3.0", 87 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz", 88 + "integrity": "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==", 89 + "license": "MIT" 90 + }, 91 + "node_modules/@atproto-labs/simple-store-memory": { 92 + "version": "0.1.4", 93 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.4.tgz", 94 + "integrity": "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==", 95 + "license": "MIT", 96 + "dependencies": { 97 + "@atproto-labs/simple-store": "0.3.0", 98 + "lru-cache": "^10.2.0" 99 + } 100 + }, 101 + "node_modules/@atproto/api": { 102 + "version": "0.18.21", 103 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.21.tgz", 104 + "integrity": "sha512-s35MIJerGT/pKe2xJtKKswqlIr/ola2r2iURBKBL0Mk1OKe6jP4YvTMh1N2d2PEANFzNNTbKoDaLfJPo2Uvc/w==", 105 + "license": "MIT", 106 + "dependencies": { 107 + "@atproto/common-web": "^0.4.16", 108 + "@atproto/lexicon": "^0.6.1", 109 + "@atproto/syntax": "^0.4.3", 110 + "@atproto/xrpc": "^0.7.7", 111 + "await-lock": "^2.2.2", 112 + "multiformats": "^9.9.0", 113 + "tlds": "^1.234.0", 114 + "zod": "^3.23.8" 115 + } 116 + }, 117 + "node_modules/@atproto/api/node_modules/@atproto/syntax": { 118 + "version": "0.4.3", 119 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 120 + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 121 + "license": "MIT", 122 + "dependencies": { 123 + "tslib": "^2.8.1" 124 + } 125 + }, 126 + "node_modules/@atproto/common": { 127 + "version": "0.5.15", 128 + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.15.tgz", 129 + "integrity": "sha512-+cdfdMPAIbH9zQGLfH1gNY2KEZsMxj0EelVQL5uJUFL+UkkAXiiqWj7J5mbax8sf02cC/afJnfkWzERNAheKoA==", 130 + "license": "MIT", 131 + "dependencies": { 132 + "@atproto/common-web": "^0.4.19", 133 + "@atproto/lex-cbor": "^0.0.15", 134 + "@atproto/lex-data": "^0.0.14", 135 + "multiformats": "^9.9.0", 136 + "pino": "^8.21.0" 137 + }, 138 + "engines": { 139 + "node": ">=18.7.0" 140 + } 141 + }, 142 + "node_modules/@atproto/common-web": { 143 + "version": "0.4.19", 144 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.19.tgz", 145 + "integrity": "sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==", 146 + "license": "MIT", 147 + "dependencies": { 148 + "@atproto/lex-data": "^0.0.14", 149 + "@atproto/lex-json": "^0.0.14", 150 + "@atproto/syntax": "^0.5.1", 151 + "zod": "^3.23.8" 152 + } 153 + }, 154 + "node_modules/@atproto/common-web/node_modules/@atproto/syntax": { 155 + "version": "0.5.3", 156 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.3.tgz", 157 + "integrity": "sha512-gzhlHOJHm5KXdCc17fXi1fXM81ccs5jJfNgCui84ay9JGvczxegpYHNqdMlv+iBuhtBzFIjgx6ChjRxN/kO8kQ==", 158 + "license": "MIT", 159 + "dependencies": { 160 + "tslib": "^2.8.1" 161 + } 162 + }, 163 + "node_modules/@atproto/crypto": { 164 + "version": "0.4.5", 165 + "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.5.tgz", 166 + "integrity": "sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw==", 167 + "license": "MIT", 168 + "dependencies": { 169 + "@noble/curves": "^1.7.0", 170 + "@noble/hashes": "^1.6.1", 171 + "uint8arrays": "3.0.0" 172 + }, 173 + "engines": { 174 + "node": ">=18.7.0" 175 + } 176 + }, 177 + "node_modules/@atproto/did": { 178 + "version": "0.3.0", 179 + "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.3.0.tgz", 180 + "integrity": "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA==", 181 + "license": "MIT", 182 + "dependencies": { 183 + "zod": "^3.23.8" 184 + } 185 + }, 186 + "node_modules/@atproto/lex-cbor": { 187 + "version": "0.0.15", 188 + "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.15.tgz", 189 + "integrity": "sha512-3osDicK9bAMXJlKjLKqwYrhLQ60bOguWBNjE+fuNjMuizNzC0aqaClE3d+qMsFuFq9bjEHFw+4Vr9Qmd/m6VYg==", 190 + "license": "MIT", 191 + "dependencies": { 192 + "@atproto/lex-data": "^0.0.14", 193 + "tslib": "^2.8.1" 194 + } 195 + }, 196 + "node_modules/@atproto/lex-client": { 197 + "version": "0.0.13", 198 + "resolved": "https://registry.npmjs.org/@atproto/lex-client/-/lex-client-0.0.13.tgz", 199 + "integrity": "sha512-NftQ9SSIilMFFj99fBlv1hvZ6Oe4Bl+HYn4VkXrWsGrHeOIM3GLgVZSMWAlg33rCvd6bYfb+YnIdPxcV6lCU0g==", 200 + "license": "MIT", 201 + "dependencies": { 202 + "@atproto/lex-data": "^0.0.12", 203 + "@atproto/lex-json": "^0.0.12", 204 + "@atproto/lex-schema": "^0.0.13", 205 + "tslib": "^2.8.1" 206 + } 207 + }, 208 + "node_modules/@atproto/lex-client/node_modules/@atproto/lex-data": { 209 + "version": "0.0.12", 210 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.12.tgz", 211 + "integrity": "sha512-aekJudcK1p6sbTqUv2bJMJBAGZaOJS0mgDclpK3U6VuBREK/au4B6ffunBFWgrDfg0Vwj2JGyEA7E51WZkJcRw==", 212 + "license": "MIT", 213 + "dependencies": { 214 + "multiformats": "^9.9.0", 215 + "tslib": "^2.8.1", 216 + "uint8arrays": "3.0.0", 217 + "unicode-segmenter": "^0.14.0" 218 + } 219 + }, 220 + "node_modules/@atproto/lex-client/node_modules/@atproto/lex-json": { 221 + "version": "0.0.12", 222 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.12.tgz", 223 + "integrity": "sha512-XlEpnWWZdDJ5BIgG25GyH+6iBfyrFL18BI5JSE6rUfMObbFMrQRaCuRLQfryRXNysVz3L3U+Qb9y8KcXbE8AcA==", 224 + "license": "MIT", 225 + "dependencies": { 226 + "@atproto/lex-data": "^0.0.12", 227 + "tslib": "^2.8.1" 228 + } 229 + }, 230 + "node_modules/@atproto/lex-data": { 231 + "version": "0.0.14", 232 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.14.tgz", 233 + "integrity": "sha512-53DUa9664SS76nGAMYopWsO10OH0AAdf7P/HSKB6Wzx3iqe6lk/K61QZnKxOG1LreYl5CfvIJU6eNf4txI6GlQ==", 234 + "license": "MIT", 235 + "dependencies": { 236 + "multiformats": "^9.9.0", 237 + "tslib": "^2.8.1", 238 + "uint8arrays": "3.0.0", 239 + "unicode-segmenter": "^0.14.0" 240 + } 241 + }, 242 + "node_modules/@atproto/lex-document": { 243 + "version": "0.0.14", 244 + "resolved": "https://registry.npmjs.org/@atproto/lex-document/-/lex-document-0.0.14.tgz", 245 + "integrity": "sha512-BaCSZOZUIv3kQ23b3Lhe4sprJYHc0spSeWS3TLaMoVi6bFZ3RzeM8n7ROTzVP3BTNYxpRHPHtOarx9A8ZAAC4w==", 246 + "license": "MIT", 247 + "dependencies": { 248 + "@atproto/lex-schema": "^0.0.13", 249 + "core-js": "^3", 250 + "tslib": "^2.8.1" 251 + } 252 + }, 253 + "node_modules/@atproto/lex-json": { 254 + "version": "0.0.14", 255 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.14.tgz", 256 + "integrity": "sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww==", 257 + "license": "MIT", 258 + "dependencies": { 259 + "@atproto/lex-data": "^0.0.14", 260 + "tslib": "^2.8.1" 261 + } 262 + }, 263 + "node_modules/@atproto/lex-resolver": { 264 + "version": "0.0.15", 265 + "resolved": "https://registry.npmjs.org/@atproto/lex-resolver/-/lex-resolver-0.0.15.tgz", 266 + "integrity": "sha512-oNxcNCts3ReJ+A4hTPthQbPA68yMQ0ZrBUIiUTZwRazrmg/xkV15jtyYSnH4CGBjRdt420MV9aLR2s4qLSmyTQ==", 267 + "license": "MIT", 268 + "dependencies": { 269 + "@atproto-labs/did-resolver": "^0.2.6", 270 + "@atproto/crypto": "^0.4.5", 271 + "@atproto/lex-client": "^0.0.13", 272 + "@atproto/lex-data": "^0.0.12", 273 + "@atproto/lex-document": "^0.0.14", 274 + "@atproto/lex-schema": "^0.0.13", 275 + "@atproto/repo": "^0.8.12", 276 + "@atproto/syntax": "^0.4.3", 277 + "tslib": "^2.8.1" 278 + } 279 + }, 280 + "node_modules/@atproto/lex-resolver/node_modules/@atproto/lex-data": { 281 + "version": "0.0.12", 282 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.12.tgz", 283 + "integrity": "sha512-aekJudcK1p6sbTqUv2bJMJBAGZaOJS0mgDclpK3U6VuBREK/au4B6ffunBFWgrDfg0Vwj2JGyEA7E51WZkJcRw==", 284 + "license": "MIT", 285 + "dependencies": { 286 + "multiformats": "^9.9.0", 287 + "tslib": "^2.8.1", 288 + "uint8arrays": "3.0.0", 289 + "unicode-segmenter": "^0.14.0" 290 + } 291 + }, 292 + "node_modules/@atproto/lex-resolver/node_modules/@atproto/syntax": { 293 + "version": "0.4.3", 294 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 295 + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 296 + "license": "MIT", 297 + "dependencies": { 298 + "tslib": "^2.8.1" 299 + } 300 + }, 301 + "node_modules/@atproto/lex-schema": { 302 + "version": "0.0.13", 303 + "resolved": "https://registry.npmjs.org/@atproto/lex-schema/-/lex-schema-0.0.13.tgz", 304 + "integrity": "sha512-FeY4YBesEUO4Ey3BJhDRma0cZt6XxunSZPXny5Q/6ltc7pvyJGXXtJ8D7mHl7p5EXPwylEYOQkM6ck4IyfMP0A==", 305 + "license": "MIT", 306 + "dependencies": { 307 + "@atproto/lex-data": "^0.0.12", 308 + "@atproto/syntax": "^0.4.3", 309 + "tslib": "^2.8.1" 310 + } 311 + }, 312 + "node_modules/@atproto/lex-schema/node_modules/@atproto/lex-data": { 313 + "version": "0.0.12", 314 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.12.tgz", 315 + "integrity": "sha512-aekJudcK1p6sbTqUv2bJMJBAGZaOJS0mgDclpK3U6VuBREK/au4B6ffunBFWgrDfg0Vwj2JGyEA7E51WZkJcRw==", 316 + "license": "MIT", 317 + "dependencies": { 318 + "multiformats": "^9.9.0", 319 + "tslib": "^2.8.1", 320 + "uint8arrays": "3.0.0", 321 + "unicode-segmenter": "^0.14.0" 322 + } 323 + }, 324 + "node_modules/@atproto/lex-schema/node_modules/@atproto/syntax": { 325 + "version": "0.4.3", 326 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 327 + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 328 + "license": "MIT", 329 + "dependencies": { 330 + "tslib": "^2.8.1" 331 + } 332 + }, 333 + "node_modules/@atproto/lexicon": { 334 + "version": "0.6.2", 335 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.2.tgz", 336 + "integrity": "sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==", 337 + "license": "MIT", 338 + "dependencies": { 339 + "@atproto/common-web": "^0.4.18", 340 + "@atproto/syntax": "^0.5.0", 341 + "iso-datestring-validator": "^2.2.2", 342 + "multiformats": "^9.9.0", 343 + "zod": "^3.23.8" 344 + } 345 + }, 346 + "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { 347 + "version": "0.5.3", 348 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.3.tgz", 349 + "integrity": "sha512-gzhlHOJHm5KXdCc17fXi1fXM81ccs5jJfNgCui84ay9JGvczxegpYHNqdMlv+iBuhtBzFIjgx6ChjRxN/kO8kQ==", 350 + "license": "MIT", 351 + "dependencies": { 352 + "tslib": "^2.8.1" 353 + } 354 + }, 355 + "node_modules/@atproto/repo": { 356 + "version": "0.8.13", 357 + "resolved": "https://registry.npmjs.org/@atproto/repo/-/repo-0.8.13.tgz", 358 + "integrity": "sha512-VS8XHaBMGdq60xwRI5zQmXzsMF1hU7NKPjmkdr65tJdrv2z0VW77mG01Ui19Xh9O0mUc/LG6GEhwVrabB9Txow==", 359 + "license": "MIT", 360 + "dependencies": { 361 + "@atproto/common": "^0.5.14", 362 + "@atproto/common-web": "^0.4.18", 363 + "@atproto/crypto": "^0.4.5", 364 + "@atproto/lexicon": "^0.6.2", 365 + "@ipld/dag-cbor": "^7.0.0", 366 + "multiformats": "^9.9.0", 367 + "uint8arrays": "3.0.0", 368 + "varint": "^6.0.0", 369 + "zod": "^3.23.8" 370 + }, 371 + "engines": { 372 + "node": ">=18.7.0" 373 + } 374 + }, 375 + "node_modules/@atproto/syntax": { 376 + "version": "0.3.4", 377 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.4.tgz", 378 + "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 379 + "license": "MIT" 380 + }, 381 + "node_modules/@atproto/xrpc": { 382 + "version": "0.7.7", 383 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", 384 + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", 385 + "license": "MIT", 386 + "dependencies": { 387 + "@atproto/lexicon": "^0.6.0", 388 + "zod": "^3.23.8" 389 + } 390 + }, 391 + "node_modules/@esbuild/aix-ppc64": { 392 + "version": "0.27.7", 393 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", 394 + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", 395 + "cpu": [ 396 + "ppc64" 397 + ], 398 + "dev": true, 399 + "license": "MIT", 400 + "optional": true, 401 + "os": [ 402 + "aix" 403 + ], 404 + "engines": { 405 + "node": ">=18" 406 + } 407 + }, 408 + "node_modules/@esbuild/android-arm": { 409 + "version": "0.27.7", 410 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", 411 + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", 412 + "cpu": [ 413 + "arm" 414 + ], 415 + "dev": true, 416 + "license": "MIT", 417 + "optional": true, 418 + "os": [ 419 + "android" 420 + ], 421 + "engines": { 422 + "node": ">=18" 423 + } 424 + }, 425 + "node_modules/@esbuild/android-arm64": { 426 + "version": "0.27.7", 427 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", 428 + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", 429 + "cpu": [ 430 + "arm64" 431 + ], 432 + "dev": true, 433 + "license": "MIT", 434 + "optional": true, 435 + "os": [ 436 + "android" 437 + ], 438 + "engines": { 439 + "node": ">=18" 440 + } 441 + }, 442 + "node_modules/@esbuild/android-x64": { 443 + "version": "0.27.7", 444 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", 445 + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", 446 + "cpu": [ 447 + "x64" 448 + ], 449 + "dev": true, 450 + "license": "MIT", 451 + "optional": true, 452 + "os": [ 453 + "android" 454 + ], 455 + "engines": { 456 + "node": ">=18" 457 + } 458 + }, 459 + "node_modules/@esbuild/darwin-arm64": { 460 + "version": "0.27.7", 461 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", 462 + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", 463 + "cpu": [ 464 + "arm64" 465 + ], 466 + "dev": true, 467 + "license": "MIT", 468 + "optional": true, 469 + "os": [ 470 + "darwin" 471 + ], 472 + "engines": { 473 + "node": ">=18" 474 + } 475 + }, 476 + "node_modules/@esbuild/darwin-x64": { 477 + "version": "0.27.7", 478 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", 479 + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", 480 + "cpu": [ 481 + "x64" 482 + ], 483 + "dev": true, 484 + "license": "MIT", 485 + "optional": true, 486 + "os": [ 487 + "darwin" 488 + ], 489 + "engines": { 490 + "node": ">=18" 491 + } 492 + }, 493 + "node_modules/@esbuild/freebsd-arm64": { 494 + "version": "0.27.7", 495 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", 496 + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", 497 + "cpu": [ 498 + "arm64" 499 + ], 500 + "dev": true, 501 + "license": "MIT", 502 + "optional": true, 503 + "os": [ 504 + "freebsd" 505 + ], 506 + "engines": { 507 + "node": ">=18" 508 + } 509 + }, 510 + "node_modules/@esbuild/freebsd-x64": { 511 + "version": "0.27.7", 512 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", 513 + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", 514 + "cpu": [ 515 + "x64" 516 + ], 517 + "dev": true, 518 + "license": "MIT", 519 + "optional": true, 520 + "os": [ 521 + "freebsd" 522 + ], 523 + "engines": { 524 + "node": ">=18" 525 + } 526 + }, 527 + "node_modules/@esbuild/linux-arm": { 528 + "version": "0.27.7", 529 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", 530 + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", 531 + "cpu": [ 532 + "arm" 533 + ], 534 + "dev": true, 535 + "license": "MIT", 536 + "optional": true, 537 + "os": [ 538 + "linux" 539 + ], 540 + "engines": { 541 + "node": ">=18" 542 + } 543 + }, 544 + "node_modules/@esbuild/linux-arm64": { 545 + "version": "0.27.7", 546 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", 547 + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", 548 + "cpu": [ 549 + "arm64" 550 + ], 551 + "dev": true, 552 + "license": "MIT", 553 + "optional": true, 554 + "os": [ 555 + "linux" 556 + ], 557 + "engines": { 558 + "node": ">=18" 559 + } 560 + }, 561 + "node_modules/@esbuild/linux-ia32": { 562 + "version": "0.27.7", 563 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", 564 + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", 565 + "cpu": [ 566 + "ia32" 567 + ], 568 + "dev": true, 569 + "license": "MIT", 570 + "optional": true, 571 + "os": [ 572 + "linux" 573 + ], 574 + "engines": { 575 + "node": ">=18" 576 + } 577 + }, 578 + "node_modules/@esbuild/linux-loong64": { 579 + "version": "0.27.7", 580 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", 581 + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", 582 + "cpu": [ 583 + "loong64" 584 + ], 585 + "dev": true, 586 + "license": "MIT", 587 + "optional": true, 588 + "os": [ 589 + "linux" 590 + ], 591 + "engines": { 592 + "node": ">=18" 593 + } 594 + }, 595 + "node_modules/@esbuild/linux-mips64el": { 596 + "version": "0.27.7", 597 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", 598 + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", 599 + "cpu": [ 600 + "mips64el" 601 + ], 602 + "dev": true, 603 + "license": "MIT", 604 + "optional": true, 605 + "os": [ 606 + "linux" 607 + ], 608 + "engines": { 609 + "node": ">=18" 610 + } 611 + }, 612 + "node_modules/@esbuild/linux-ppc64": { 613 + "version": "0.27.7", 614 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", 615 + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", 616 + "cpu": [ 617 + "ppc64" 618 + ], 619 + "dev": true, 620 + "license": "MIT", 621 + "optional": true, 622 + "os": [ 623 + "linux" 624 + ], 625 + "engines": { 626 + "node": ">=18" 627 + } 628 + }, 629 + "node_modules/@esbuild/linux-riscv64": { 630 + "version": "0.27.7", 631 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", 632 + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", 633 + "cpu": [ 634 + "riscv64" 635 + ], 636 + "dev": true, 637 + "license": "MIT", 638 + "optional": true, 639 + "os": [ 640 + "linux" 641 + ], 642 + "engines": { 643 + "node": ">=18" 644 + } 645 + }, 646 + "node_modules/@esbuild/linux-s390x": { 647 + "version": "0.27.7", 648 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", 649 + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", 650 + "cpu": [ 651 + "s390x" 652 + ], 653 + "dev": true, 654 + "license": "MIT", 655 + "optional": true, 656 + "os": [ 657 + "linux" 658 + ], 659 + "engines": { 660 + "node": ">=18" 661 + } 662 + }, 663 + "node_modules/@esbuild/linux-x64": { 664 + "version": "0.27.7", 665 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", 666 + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", 667 + "cpu": [ 668 + "x64" 669 + ], 670 + "dev": true, 671 + "license": "MIT", 672 + "optional": true, 673 + "os": [ 674 + "linux" 675 + ], 676 + "engines": { 677 + "node": ">=18" 678 + } 679 + }, 680 + "node_modules/@esbuild/netbsd-arm64": { 681 + "version": "0.27.7", 682 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", 683 + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", 684 + "cpu": [ 685 + "arm64" 686 + ], 687 + "dev": true, 688 + "license": "MIT", 689 + "optional": true, 690 + "os": [ 691 + "netbsd" 692 + ], 693 + "engines": { 694 + "node": ">=18" 695 + } 696 + }, 697 + "node_modules/@esbuild/netbsd-x64": { 698 + "version": "0.27.7", 699 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", 700 + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", 701 + "cpu": [ 702 + "x64" 703 + ], 704 + "dev": true, 705 + "license": "MIT", 706 + "optional": true, 707 + "os": [ 708 + "netbsd" 709 + ], 710 + "engines": { 711 + "node": ">=18" 712 + } 713 + }, 714 + "node_modules/@esbuild/openbsd-arm64": { 715 + "version": "0.27.7", 716 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", 717 + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", 718 + "cpu": [ 719 + "arm64" 720 + ], 721 + "dev": true, 722 + "license": "MIT", 723 + "optional": true, 724 + "os": [ 725 + "openbsd" 726 + ], 727 + "engines": { 728 + "node": ">=18" 729 + } 730 + }, 731 + "node_modules/@esbuild/openbsd-x64": { 732 + "version": "0.27.7", 733 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", 734 + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", 735 + "cpu": [ 736 + "x64" 737 + ], 738 + "dev": true, 739 + "license": "MIT", 740 + "optional": true, 741 + "os": [ 742 + "openbsd" 743 + ], 744 + "engines": { 745 + "node": ">=18" 746 + } 747 + }, 748 + "node_modules/@esbuild/openharmony-arm64": { 749 + "version": "0.27.7", 750 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", 751 + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", 752 + "cpu": [ 753 + "arm64" 754 + ], 755 + "dev": true, 756 + "license": "MIT", 757 + "optional": true, 758 + "os": [ 759 + "openharmony" 760 + ], 761 + "engines": { 762 + "node": ">=18" 763 + } 764 + }, 765 + "node_modules/@esbuild/sunos-x64": { 766 + "version": "0.27.7", 767 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", 768 + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", 769 + "cpu": [ 770 + "x64" 771 + ], 772 + "dev": true, 773 + "license": "MIT", 774 + "optional": true, 775 + "os": [ 776 + "sunos" 777 + ], 778 + "engines": { 779 + "node": ">=18" 780 + } 781 + }, 782 + "node_modules/@esbuild/win32-arm64": { 783 + "version": "0.27.7", 784 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", 785 + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", 786 + "cpu": [ 787 + "arm64" 788 + ], 789 + "dev": true, 790 + "license": "MIT", 791 + "optional": true, 792 + "os": [ 793 + "win32" 794 + ], 795 + "engines": { 796 + "node": ">=18" 797 + } 798 + }, 799 + "node_modules/@esbuild/win32-ia32": { 800 + "version": "0.27.7", 801 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", 802 + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", 803 + "cpu": [ 804 + "ia32" 805 + ], 806 + "dev": true, 807 + "license": "MIT", 808 + "optional": true, 809 + "os": [ 810 + "win32" 811 + ], 812 + "engines": { 813 + "node": ">=18" 814 + } 815 + }, 816 + "node_modules/@esbuild/win32-x64": { 817 + "version": "0.27.7", 818 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", 819 + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", 820 + "cpu": [ 821 + "x64" 822 + ], 823 + "dev": true, 824 + "license": "MIT", 825 + "optional": true, 826 + "os": [ 827 + "win32" 828 + ], 829 + "engines": { 830 + "node": ">=18" 831 + } 832 + }, 833 + "node_modules/@hono/node-server": { 834 + "version": "1.19.12", 835 + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz", 836 + "integrity": "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==", 837 + "license": "MIT", 838 + "engines": { 839 + "node": ">=18.14.1" 840 + }, 841 + "peerDependencies": { 842 + "hono": "^4" 843 + } 844 + }, 845 + "node_modules/@ioredis/commands": { 846 + "version": "1.5.1", 847 + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", 848 + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", 849 + "license": "MIT" 850 + }, 851 + "node_modules/@ipld/dag-cbor": { 852 + "version": "7.0.3", 853 + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz", 854 + "integrity": "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==", 855 + "license": "(Apache-2.0 AND MIT)", 856 + "dependencies": { 857 + "cborg": "^1.6.0", 858 + "multiformats": "^9.5.4" 859 + } 860 + }, 861 + "node_modules/@noble/curves": { 862 + "version": "1.9.7", 863 + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", 864 + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", 865 + "license": "MIT", 866 + "dependencies": { 867 + "@noble/hashes": "1.8.0" 868 + }, 869 + "engines": { 870 + "node": "^14.21.3 || >=16" 871 + }, 872 + "funding": { 873 + "url": "https://paulmillr.com/funding/" 874 + } 875 + }, 876 + "node_modules/@noble/hashes": { 877 + "version": "1.8.0", 878 + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", 879 + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", 880 + "license": "MIT", 881 + "engines": { 882 + "node": "^14.21.3 || >=16" 883 + }, 884 + "funding": { 885 + "url": "https://paulmillr.com/funding/" 886 + } 887 + }, 888 + "node_modules/@shikijs/core": { 889 + "version": "3.23.0", 890 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", 891 + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", 892 + "license": "MIT", 893 + "dependencies": { 894 + "@shikijs/types": "3.23.0", 895 + "@shikijs/vscode-textmate": "^10.0.2", 896 + "@types/hast": "^3.0.4", 897 + "hast-util-to-html": "^9.0.5" 898 + } 899 + }, 900 + "node_modules/@shikijs/engine-javascript": { 901 + "version": "3.23.0", 902 + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", 903 + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", 904 + "license": "MIT", 905 + "dependencies": { 906 + "@shikijs/types": "3.23.0", 907 + "@shikijs/vscode-textmate": "^10.0.2", 908 + "oniguruma-to-es": "^4.3.4" 909 + } 910 + }, 911 + "node_modules/@shikijs/engine-oniguruma": { 912 + "version": "3.23.0", 913 + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", 914 + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", 915 + "license": "MIT", 916 + "dependencies": { 917 + "@shikijs/types": "3.23.0", 918 + "@shikijs/vscode-textmate": "^10.0.2" 919 + } 920 + }, 921 + "node_modules/@shikijs/langs": { 922 + "version": "3.23.0", 923 + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", 924 + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", 925 + "license": "MIT", 926 + "dependencies": { 927 + "@shikijs/types": "3.23.0" 928 + } 929 + }, 930 + "node_modules/@shikijs/themes": { 931 + "version": "3.23.0", 932 + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", 933 + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", 934 + "license": "MIT", 935 + "dependencies": { 936 + "@shikijs/types": "3.23.0" 937 + } 938 + }, 939 + "node_modules/@shikijs/types": { 940 + "version": "3.23.0", 941 + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", 942 + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", 943 + "license": "MIT", 944 + "dependencies": { 945 + "@shikijs/vscode-textmate": "^10.0.2", 946 + "@types/hast": "^3.0.4" 947 + } 948 + }, 949 + "node_modules/@shikijs/vscode-textmate": { 950 + "version": "10.0.2", 951 + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", 952 + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", 953 + "license": "MIT" 954 + }, 955 + "node_modules/@types/hast": { 956 + "version": "3.0.4", 957 + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", 958 + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 959 + "license": "MIT", 960 + "dependencies": { 961 + "@types/unist": "*" 962 + } 963 + }, 964 + "node_modules/@types/mdast": { 965 + "version": "4.0.4", 966 + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", 967 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", 968 + "license": "MIT", 969 + "dependencies": { 970 + "@types/unist": "*" 971 + } 972 + }, 973 + "node_modules/@types/node": { 974 + "version": "22.19.15", 975 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", 976 + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", 977 + "dev": true, 978 + "license": "MIT", 979 + "dependencies": { 980 + "undici-types": "~6.21.0" 981 + } 982 + }, 983 + "node_modules/@types/unist": { 984 + "version": "3.0.3", 985 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", 986 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", 987 + "license": "MIT" 988 + }, 989 + "node_modules/@ungap/structured-clone": { 990 + "version": "1.3.0", 991 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", 992 + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", 993 + "license": "ISC" 994 + }, 995 + "node_modules/abort-controller": { 996 + "version": "3.0.0", 997 + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 998 + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 999 + "license": "MIT", 1000 + "dependencies": { 1001 + "event-target-shim": "^5.0.0" 1002 + }, 1003 + "engines": { 1004 + "node": ">=6.5" 1005 + } 1006 + }, 1007 + "node_modules/argparse": { 1008 + "version": "1.0.10", 1009 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 1010 + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 1011 + "license": "MIT", 1012 + "dependencies": { 1013 + "sprintf-js": "~1.0.2" 1014 + } 1015 + }, 1016 + "node_modules/atomic-sleep": { 1017 + "version": "1.0.0", 1018 + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 1019 + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 1020 + "license": "MIT", 1021 + "engines": { 1022 + "node": ">=8.0.0" 1023 + } 1024 + }, 1025 + "node_modules/await-lock": { 1026 + "version": "2.2.2", 1027 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 1028 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 1029 + "license": "MIT" 1030 + }, 1031 + "node_modules/base64-js": { 1032 + "version": "1.5.1", 1033 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 1034 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 1035 + "funding": [ 1036 + { 1037 + "type": "github", 1038 + "url": "https://github.com/sponsors/feross" 1039 + }, 1040 + { 1041 + "type": "patreon", 1042 + "url": "https://www.patreon.com/feross" 1043 + }, 1044 + { 1045 + "type": "consulting", 1046 + "url": "https://feross.org/support" 1047 + } 1048 + ], 1049 + "license": "MIT" 1050 + }, 1051 + "node_modules/buffer": { 1052 + "version": "6.0.3", 1053 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 1054 + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 1055 + "funding": [ 1056 + { 1057 + "type": "github", 1058 + "url": "https://github.com/sponsors/feross" 1059 + }, 1060 + { 1061 + "type": "patreon", 1062 + "url": "https://www.patreon.com/feross" 1063 + }, 1064 + { 1065 + "type": "consulting", 1066 + "url": "https://feross.org/support" 1067 + } 1068 + ], 1069 + "license": "MIT", 1070 + "dependencies": { 1071 + "base64-js": "^1.3.1", 1072 + "ieee754": "^1.2.1" 1073 + } 1074 + }, 1075 + "node_modules/cborg": { 1076 + "version": "1.10.2", 1077 + "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.2.tgz", 1078 + "integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==", 1079 + "license": "Apache-2.0", 1080 + "bin": { 1081 + "cborg": "cli.js" 1082 + } 1083 + }, 1084 + "node_modules/ccount": { 1085 + "version": "2.0.1", 1086 + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", 1087 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", 1088 + "license": "MIT", 1089 + "funding": { 1090 + "type": "github", 1091 + "url": "https://github.com/sponsors/wooorm" 1092 + } 1093 + }, 1094 + "node_modules/character-entities-html4": { 1095 + "version": "2.1.0", 1096 + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", 1097 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", 1098 + "license": "MIT", 1099 + "funding": { 1100 + "type": "github", 1101 + "url": "https://github.com/sponsors/wooorm" 1102 + } 1103 + }, 1104 + "node_modules/character-entities-legacy": { 1105 + "version": "3.0.0", 1106 + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", 1107 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", 1108 + "license": "MIT", 1109 + "funding": { 1110 + "type": "github", 1111 + "url": "https://github.com/sponsors/wooorm" 1112 + } 1113 + }, 1114 + "node_modules/cluster-key-slot": { 1115 + "version": "1.1.2", 1116 + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", 1117 + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", 1118 + "license": "Apache-2.0", 1119 + "engines": { 1120 + "node": ">=0.10.0" 1121 + } 1122 + }, 1123 + "node_modules/comma-separated-tokens": { 1124 + "version": "2.0.3", 1125 + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", 1126 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", 1127 + "license": "MIT", 1128 + "funding": { 1129 + "type": "github", 1130 + "url": "https://github.com/sponsors/wooorm" 1131 + } 1132 + }, 1133 + "node_modules/core-js": { 1134 + "version": "3.49.0", 1135 + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", 1136 + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", 1137 + "hasInstallScript": true, 1138 + "license": "MIT", 1139 + "funding": { 1140 + "type": "opencollective", 1141 + "url": "https://opencollective.com/core-js" 1142 + } 1143 + }, 1144 + "node_modules/debug": { 1145 + "version": "4.4.3", 1146 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1147 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1148 + "license": "MIT", 1149 + "dependencies": { 1150 + "ms": "^2.1.3" 1151 + }, 1152 + "engines": { 1153 + "node": ">=6.0" 1154 + }, 1155 + "peerDependenciesMeta": { 1156 + "supports-color": { 1157 + "optional": true 1158 + } 1159 + } 1160 + }, 1161 + "node_modules/denque": { 1162 + "version": "2.1.0", 1163 + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", 1164 + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", 1165 + "license": "Apache-2.0", 1166 + "engines": { 1167 + "node": ">=0.10" 1168 + } 1169 + }, 1170 + "node_modules/dequal": { 1171 + "version": "2.0.3", 1172 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 1173 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 1174 + "license": "MIT", 1175 + "engines": { 1176 + "node": ">=6" 1177 + } 1178 + }, 1179 + "node_modules/devlop": { 1180 + "version": "1.1.0", 1181 + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", 1182 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", 1183 + "license": "MIT", 1184 + "dependencies": { 1185 + "dequal": "^2.0.0" 1186 + }, 1187 + "funding": { 1188 + "type": "github", 1189 + "url": "https://github.com/sponsors/wooorm" 1190 + } 1191 + }, 1192 + "node_modules/dotenv": { 1193 + "version": "16.6.1", 1194 + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 1195 + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 1196 + "license": "BSD-2-Clause", 1197 + "engines": { 1198 + "node": ">=12" 1199 + }, 1200 + "funding": { 1201 + "url": "https://dotenvx.com" 1202 + } 1203 + }, 1204 + "node_modules/esbuild": { 1205 + "version": "0.27.7", 1206 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", 1207 + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", 1208 + "dev": true, 1209 + "hasInstallScript": true, 1210 + "license": "MIT", 1211 + "bin": { 1212 + "esbuild": "bin/esbuild" 1213 + }, 1214 + "engines": { 1215 + "node": ">=18" 1216 + }, 1217 + "optionalDependencies": { 1218 + "@esbuild/aix-ppc64": "0.27.7", 1219 + "@esbuild/android-arm": "0.27.7", 1220 + "@esbuild/android-arm64": "0.27.7", 1221 + "@esbuild/android-x64": "0.27.7", 1222 + "@esbuild/darwin-arm64": "0.27.7", 1223 + "@esbuild/darwin-x64": "0.27.7", 1224 + "@esbuild/freebsd-arm64": "0.27.7", 1225 + "@esbuild/freebsd-x64": "0.27.7", 1226 + "@esbuild/linux-arm": "0.27.7", 1227 + "@esbuild/linux-arm64": "0.27.7", 1228 + "@esbuild/linux-ia32": "0.27.7", 1229 + "@esbuild/linux-loong64": "0.27.7", 1230 + "@esbuild/linux-mips64el": "0.27.7", 1231 + "@esbuild/linux-ppc64": "0.27.7", 1232 + "@esbuild/linux-riscv64": "0.27.7", 1233 + "@esbuild/linux-s390x": "0.27.7", 1234 + "@esbuild/linux-x64": "0.27.7", 1235 + "@esbuild/netbsd-arm64": "0.27.7", 1236 + "@esbuild/netbsd-x64": "0.27.7", 1237 + "@esbuild/openbsd-arm64": "0.27.7", 1238 + "@esbuild/openbsd-x64": "0.27.7", 1239 + "@esbuild/openharmony-arm64": "0.27.7", 1240 + "@esbuild/sunos-x64": "0.27.7", 1241 + "@esbuild/win32-arm64": "0.27.7", 1242 + "@esbuild/win32-ia32": "0.27.7", 1243 + "@esbuild/win32-x64": "0.27.7" 1244 + } 1245 + }, 1246 + "node_modules/esprima": { 1247 + "version": "4.0.1", 1248 + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 1249 + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 1250 + "license": "BSD-2-Clause", 1251 + "bin": { 1252 + "esparse": "bin/esparse.js", 1253 + "esvalidate": "bin/esvalidate.js" 1254 + }, 1255 + "engines": { 1256 + "node": ">=4" 1257 + } 1258 + }, 1259 + "node_modules/event-target-shim": { 1260 + "version": "5.0.1", 1261 + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 1262 + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 1263 + "license": "MIT", 1264 + "engines": { 1265 + "node": ">=6" 1266 + } 1267 + }, 1268 + "node_modules/events": { 1269 + "version": "3.3.0", 1270 + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 1271 + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 1272 + "license": "MIT", 1273 + "engines": { 1274 + "node": ">=0.8.x" 1275 + } 1276 + }, 1277 + "node_modules/extend-shallow": { 1278 + "version": "2.0.1", 1279 + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 1280 + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", 1281 + "license": "MIT", 1282 + "dependencies": { 1283 + "is-extendable": "^0.1.0" 1284 + }, 1285 + "engines": { 1286 + "node": ">=0.10.0" 1287 + } 1288 + }, 1289 + "node_modules/fast-redact": { 1290 + "version": "3.5.0", 1291 + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", 1292 + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", 1293 + "license": "MIT", 1294 + "engines": { 1295 + "node": ">=6" 1296 + } 1297 + }, 1298 + "node_modules/fsevents": { 1299 + "version": "2.3.3", 1300 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1301 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1302 + "dev": true, 1303 + "hasInstallScript": true, 1304 + "license": "MIT", 1305 + "optional": true, 1306 + "os": [ 1307 + "darwin" 1308 + ], 1309 + "engines": { 1310 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1311 + } 1312 + }, 1313 + "node_modules/get-tsconfig": { 1314 + "version": "4.13.7", 1315 + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", 1316 + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", 1317 + "dev": true, 1318 + "license": "MIT", 1319 + "dependencies": { 1320 + "resolve-pkg-maps": "^1.0.0" 1321 + }, 1322 + "funding": { 1323 + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" 1324 + } 1325 + }, 1326 + "node_modules/gray-matter": { 1327 + "version": "4.0.3", 1328 + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", 1329 + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", 1330 + "license": "MIT", 1331 + "dependencies": { 1332 + "js-yaml": "^3.13.1", 1333 + "kind-of": "^6.0.2", 1334 + "section-matter": "^1.0.0", 1335 + "strip-bom-string": "^1.0.0" 1336 + }, 1337 + "engines": { 1338 + "node": ">=6.0" 1339 + } 1340 + }, 1341 + "node_modules/hast-util-to-html": { 1342 + "version": "9.0.5", 1343 + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", 1344 + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", 1345 + "license": "MIT", 1346 + "dependencies": { 1347 + "@types/hast": "^3.0.0", 1348 + "@types/unist": "^3.0.0", 1349 + "ccount": "^2.0.0", 1350 + "comma-separated-tokens": "^2.0.0", 1351 + "hast-util-whitespace": "^3.0.0", 1352 + "html-void-elements": "^3.0.0", 1353 + "mdast-util-to-hast": "^13.0.0", 1354 + "property-information": "^7.0.0", 1355 + "space-separated-tokens": "^2.0.0", 1356 + "stringify-entities": "^4.0.0", 1357 + "zwitch": "^2.0.4" 1358 + }, 1359 + "funding": { 1360 + "type": "opencollective", 1361 + "url": "https://opencollective.com/unified" 1362 + } 1363 + }, 1364 + "node_modules/hast-util-whitespace": { 1365 + "version": "3.0.0", 1366 + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", 1367 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", 1368 + "license": "MIT", 1369 + "dependencies": { 1370 + "@types/hast": "^3.0.0" 1371 + }, 1372 + "funding": { 1373 + "type": "opencollective", 1374 + "url": "https://opencollective.com/unified" 1375 + } 1376 + }, 1377 + "node_modules/hono": { 1378 + "version": "4.12.10", 1379 + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz", 1380 + "integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==", 1381 + "license": "MIT", 1382 + "engines": { 1383 + "node": ">=16.9.0" 1384 + } 1385 + }, 1386 + "node_modules/html-void-elements": { 1387 + "version": "3.0.0", 1388 + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", 1389 + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", 1390 + "license": "MIT", 1391 + "funding": { 1392 + "type": "github", 1393 + "url": "https://github.com/sponsors/wooorm" 1394 + } 1395 + }, 1396 + "node_modules/ieee754": { 1397 + "version": "1.2.1", 1398 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1399 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1400 + "funding": [ 1401 + { 1402 + "type": "github", 1403 + "url": "https://github.com/sponsors/feross" 1404 + }, 1405 + { 1406 + "type": "patreon", 1407 + "url": "https://www.patreon.com/feross" 1408 + }, 1409 + { 1410 + "type": "consulting", 1411 + "url": "https://feross.org/support" 1412 + } 1413 + ], 1414 + "license": "BSD-3-Clause" 1415 + }, 1416 + "node_modules/ioredis": { 1417 + "version": "5.10.1", 1418 + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", 1419 + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", 1420 + "license": "MIT", 1421 + "dependencies": { 1422 + "@ioredis/commands": "1.5.1", 1423 + "cluster-key-slot": "^1.1.0", 1424 + "debug": "^4.3.4", 1425 + "denque": "^2.1.0", 1426 + "lodash.defaults": "^4.2.0", 1427 + "lodash.isarguments": "^3.1.0", 1428 + "redis-errors": "^1.2.0", 1429 + "redis-parser": "^3.0.0", 1430 + "standard-as-callback": "^2.1.0" 1431 + }, 1432 + "engines": { 1433 + "node": ">=12.22.0" 1434 + }, 1435 + "funding": { 1436 + "type": "opencollective", 1437 + "url": "https://opencollective.com/ioredis" 1438 + } 1439 + }, 1440 + "node_modules/is-extendable": { 1441 + "version": "0.1.1", 1442 + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 1443 + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", 1444 + "license": "MIT", 1445 + "engines": { 1446 + "node": ">=0.10.0" 1447 + } 1448 + }, 1449 + "node_modules/iso-datestring-validator": { 1450 + "version": "2.2.2", 1451 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 1452 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 1453 + "license": "MIT" 1454 + }, 1455 + "node_modules/js-yaml": { 1456 + "version": "3.14.2", 1457 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", 1458 + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", 1459 + "license": "MIT", 1460 + "dependencies": { 1461 + "argparse": "^1.0.7", 1462 + "esprima": "^4.0.0" 1463 + }, 1464 + "bin": { 1465 + "js-yaml": "bin/js-yaml.js" 1466 + } 1467 + }, 1468 + "node_modules/kind-of": { 1469 + "version": "6.0.3", 1470 + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 1471 + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 1472 + "license": "MIT", 1473 + "engines": { 1474 + "node": ">=0.10.0" 1475 + } 1476 + }, 1477 + "node_modules/lodash.defaults": { 1478 + "version": "4.2.0", 1479 + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 1480 + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", 1481 + "license": "MIT" 1482 + }, 1483 + "node_modules/lodash.isarguments": { 1484 + "version": "3.1.0", 1485 + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", 1486 + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", 1487 + "license": "MIT" 1488 + }, 1489 + "node_modules/lru-cache": { 1490 + "version": "10.4.3", 1491 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 1492 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 1493 + "license": "ISC" 1494 + }, 1495 + "node_modules/marked": { 1496 + "version": "15.0.12", 1497 + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", 1498 + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", 1499 + "license": "MIT", 1500 + "bin": { 1501 + "marked": "bin/marked.js" 1502 + }, 1503 + "engines": { 1504 + "node": ">= 18" 1505 + } 1506 + }, 1507 + "node_modules/marked-footnote": { 1508 + "version": "1.4.0", 1509 + "resolved": "https://registry.npmjs.org/marked-footnote/-/marked-footnote-1.4.0.tgz", 1510 + "integrity": "sha512-fZTxAhI1TcLEs5UOjCfYfTHpyKGaWQevbxaGTEA68B51l7i87SctPFtHETYqPkEN0ka5opvy4Dy1l/yXVC+hmg==", 1511 + "license": "MIT", 1512 + "peerDependencies": { 1513 + "marked": ">=7.0.0" 1514 + } 1515 + }, 1516 + "node_modules/mdast-util-to-hast": { 1517 + "version": "13.2.1", 1518 + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", 1519 + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", 1520 + "license": "MIT", 1521 + "dependencies": { 1522 + "@types/hast": "^3.0.0", 1523 + "@types/mdast": "^4.0.0", 1524 + "@ungap/structured-clone": "^1.0.0", 1525 + "devlop": "^1.0.0", 1526 + "micromark-util-sanitize-uri": "^2.0.0", 1527 + "trim-lines": "^3.0.0", 1528 + "unist-util-position": "^5.0.0", 1529 + "unist-util-visit": "^5.0.0", 1530 + "vfile": "^6.0.0" 1531 + }, 1532 + "funding": { 1533 + "type": "opencollective", 1534 + "url": "https://opencollective.com/unified" 1535 + } 1536 + }, 1537 + "node_modules/micromark-util-character": { 1538 + "version": "2.1.1", 1539 + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", 1540 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", 1541 + "funding": [ 1542 + { 1543 + "type": "GitHub Sponsors", 1544 + "url": "https://github.com/sponsors/unifiedjs" 1545 + }, 1546 + { 1547 + "type": "OpenCollective", 1548 + "url": "https://opencollective.com/unified" 1549 + } 1550 + ], 1551 + "license": "MIT", 1552 + "dependencies": { 1553 + "micromark-util-symbol": "^2.0.0", 1554 + "micromark-util-types": "^2.0.0" 1555 + } 1556 + }, 1557 + "node_modules/micromark-util-encode": { 1558 + "version": "2.0.1", 1559 + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", 1560 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", 1561 + "funding": [ 1562 + { 1563 + "type": "GitHub Sponsors", 1564 + "url": "https://github.com/sponsors/unifiedjs" 1565 + }, 1566 + { 1567 + "type": "OpenCollective", 1568 + "url": "https://opencollective.com/unified" 1569 + } 1570 + ], 1571 + "license": "MIT" 1572 + }, 1573 + "node_modules/micromark-util-sanitize-uri": { 1574 + "version": "2.0.1", 1575 + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", 1576 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", 1577 + "funding": [ 1578 + { 1579 + "type": "GitHub Sponsors", 1580 + "url": "https://github.com/sponsors/unifiedjs" 1581 + }, 1582 + { 1583 + "type": "OpenCollective", 1584 + "url": "https://opencollective.com/unified" 1585 + } 1586 + ], 1587 + "license": "MIT", 1588 + "dependencies": { 1589 + "micromark-util-character": "^2.0.0", 1590 + "micromark-util-encode": "^2.0.0", 1591 + "micromark-util-symbol": "^2.0.0" 1592 + } 1593 + }, 1594 + "node_modules/micromark-util-symbol": { 1595 + "version": "2.0.1", 1596 + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", 1597 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", 1598 + "funding": [ 1599 + { 1600 + "type": "GitHub Sponsors", 1601 + "url": "https://github.com/sponsors/unifiedjs" 1602 + }, 1603 + { 1604 + "type": "OpenCollective", 1605 + "url": "https://opencollective.com/unified" 1606 + } 1607 + ], 1608 + "license": "MIT" 1609 + }, 1610 + "node_modules/micromark-util-types": { 1611 + "version": "2.0.2", 1612 + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", 1613 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", 1614 + "funding": [ 1615 + { 1616 + "type": "GitHub Sponsors", 1617 + "url": "https://github.com/sponsors/unifiedjs" 1618 + }, 1619 + { 1620 + "type": "OpenCollective", 1621 + "url": "https://opencollective.com/unified" 1622 + } 1623 + ], 1624 + "license": "MIT" 1625 + }, 1626 + "node_modules/ms": { 1627 + "version": "2.1.3", 1628 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1629 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1630 + "license": "MIT" 1631 + }, 1632 + "node_modules/multiformats": { 1633 + "version": "9.9.0", 1634 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 1635 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 1636 + "license": "(Apache-2.0 AND MIT)" 1637 + }, 1638 + "node_modules/on-exit-leak-free": { 1639 + "version": "2.1.2", 1640 + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", 1641 + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", 1642 + "license": "MIT", 1643 + "engines": { 1644 + "node": ">=14.0.0" 1645 + } 1646 + }, 1647 + "node_modules/oniguruma-parser": { 1648 + "version": "0.12.1", 1649 + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", 1650 + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", 1651 + "license": "MIT" 1652 + }, 1653 + "node_modules/oniguruma-to-es": { 1654 + "version": "4.3.5", 1655 + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", 1656 + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", 1657 + "license": "MIT", 1658 + "dependencies": { 1659 + "oniguruma-parser": "^0.12.1", 1660 + "regex": "^6.1.0", 1661 + "regex-recursion": "^6.0.2" 1662 + } 1663 + }, 1664 + "node_modules/pino": { 1665 + "version": "8.21.0", 1666 + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", 1667 + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", 1668 + "license": "MIT", 1669 + "dependencies": { 1670 + "atomic-sleep": "^1.0.0", 1671 + "fast-redact": "^3.1.1", 1672 + "on-exit-leak-free": "^2.1.0", 1673 + "pino-abstract-transport": "^1.2.0", 1674 + "pino-std-serializers": "^6.0.0", 1675 + "process-warning": "^3.0.0", 1676 + "quick-format-unescaped": "^4.0.3", 1677 + "real-require": "^0.2.0", 1678 + "safe-stable-stringify": "^2.3.1", 1679 + "sonic-boom": "^3.7.0", 1680 + "thread-stream": "^2.6.0" 1681 + }, 1682 + "bin": { 1683 + "pino": "bin.js" 1684 + } 1685 + }, 1686 + "node_modules/pino-abstract-transport": { 1687 + "version": "1.2.0", 1688 + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", 1689 + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", 1690 + "license": "MIT", 1691 + "dependencies": { 1692 + "readable-stream": "^4.0.0", 1693 + "split2": "^4.0.0" 1694 + } 1695 + }, 1696 + "node_modules/pino-std-serializers": { 1697 + "version": "6.2.2", 1698 + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", 1699 + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", 1700 + "license": "MIT" 1701 + }, 1702 + "node_modules/process": { 1703 + "version": "0.11.10", 1704 + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", 1705 + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", 1706 + "license": "MIT", 1707 + "engines": { 1708 + "node": ">= 0.6.0" 1709 + } 1710 + }, 1711 + "node_modules/process-warning": { 1712 + "version": "3.0.0", 1713 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", 1714 + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", 1715 + "license": "MIT" 1716 + }, 1717 + "node_modules/property-information": { 1718 + "version": "7.1.0", 1719 + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", 1720 + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", 1721 + "license": "MIT", 1722 + "funding": { 1723 + "type": "github", 1724 + "url": "https://github.com/sponsors/wooorm" 1725 + } 1726 + }, 1727 + "node_modules/quick-format-unescaped": { 1728 + "version": "4.0.4", 1729 + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 1730 + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", 1731 + "license": "MIT" 1732 + }, 1733 + "node_modules/readable-stream": { 1734 + "version": "4.7.0", 1735 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", 1736 + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", 1737 + "license": "MIT", 1738 + "dependencies": { 1739 + "abort-controller": "^3.0.0", 1740 + "buffer": "^6.0.3", 1741 + "events": "^3.3.0", 1742 + "process": "^0.11.10", 1743 + "string_decoder": "^1.3.0" 1744 + }, 1745 + "engines": { 1746 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1747 + } 1748 + }, 1749 + "node_modules/real-require": { 1750 + "version": "0.2.0", 1751 + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 1752 + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 1753 + "license": "MIT", 1754 + "engines": { 1755 + "node": ">= 12.13.0" 1756 + } 1757 + }, 1758 + "node_modules/redis-errors": { 1759 + "version": "1.2.0", 1760 + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", 1761 + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", 1762 + "license": "MIT", 1763 + "engines": { 1764 + "node": ">=4" 1765 + } 1766 + }, 1767 + "node_modules/redis-parser": { 1768 + "version": "3.0.0", 1769 + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", 1770 + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", 1771 + "license": "MIT", 1772 + "dependencies": { 1773 + "redis-errors": "^1.0.0" 1774 + }, 1775 + "engines": { 1776 + "node": ">=4" 1777 + } 1778 + }, 1779 + "node_modules/regex": { 1780 + "version": "6.1.0", 1781 + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", 1782 + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", 1783 + "license": "MIT", 1784 + "dependencies": { 1785 + "regex-utilities": "^2.3.0" 1786 + } 1787 + }, 1788 + "node_modules/regex-recursion": { 1789 + "version": "6.0.2", 1790 + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", 1791 + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", 1792 + "license": "MIT", 1793 + "dependencies": { 1794 + "regex-utilities": "^2.3.0" 1795 + } 1796 + }, 1797 + "node_modules/regex-utilities": { 1798 + "version": "2.3.0", 1799 + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", 1800 + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", 1801 + "license": "MIT" 1802 + }, 1803 + "node_modules/resolve-pkg-maps": { 1804 + "version": "1.0.0", 1805 + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", 1806 + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", 1807 + "dev": true, 1808 + "license": "MIT", 1809 + "funding": { 1810 + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" 1811 + } 1812 + }, 1813 + "node_modules/safe-buffer": { 1814 + "version": "5.2.1", 1815 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1816 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1817 + "funding": [ 1818 + { 1819 + "type": "github", 1820 + "url": "https://github.com/sponsors/feross" 1821 + }, 1822 + { 1823 + "type": "patreon", 1824 + "url": "https://www.patreon.com/feross" 1825 + }, 1826 + { 1827 + "type": "consulting", 1828 + "url": "https://feross.org/support" 1829 + } 1830 + ], 1831 + "license": "MIT" 1832 + }, 1833 + "node_modules/safe-stable-stringify": { 1834 + "version": "2.5.0", 1835 + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", 1836 + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", 1837 + "license": "MIT", 1838 + "engines": { 1839 + "node": ">=10" 1840 + } 1841 + }, 1842 + "node_modules/section-matter": { 1843 + "version": "1.0.0", 1844 + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", 1845 + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", 1846 + "license": "MIT", 1847 + "dependencies": { 1848 + "extend-shallow": "^2.0.1", 1849 + "kind-of": "^6.0.0" 1850 + }, 1851 + "engines": { 1852 + "node": ">=4" 1853 + } 1854 + }, 1855 + "node_modules/shiki": { 1856 + "version": "3.23.0", 1857 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", 1858 + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", 1859 + "license": "MIT", 1860 + "dependencies": { 1861 + "@shikijs/core": "3.23.0", 1862 + "@shikijs/engine-javascript": "3.23.0", 1863 + "@shikijs/engine-oniguruma": "3.23.0", 1864 + "@shikijs/langs": "3.23.0", 1865 + "@shikijs/themes": "3.23.0", 1866 + "@shikijs/types": "3.23.0", 1867 + "@shikijs/vscode-textmate": "^10.0.2", 1868 + "@types/hast": "^3.0.4" 1869 + } 1870 + }, 1871 + "node_modules/sonic-boom": { 1872 + "version": "3.8.1", 1873 + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", 1874 + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", 1875 + "license": "MIT", 1876 + "dependencies": { 1877 + "atomic-sleep": "^1.0.0" 1878 + } 1879 + }, 1880 + "node_modules/space-separated-tokens": { 1881 + "version": "2.0.2", 1882 + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", 1883 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", 1884 + "license": "MIT", 1885 + "funding": { 1886 + "type": "github", 1887 + "url": "https://github.com/sponsors/wooorm" 1888 + } 1889 + }, 1890 + "node_modules/split2": { 1891 + "version": "4.2.0", 1892 + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 1893 + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 1894 + "license": "ISC", 1895 + "engines": { 1896 + "node": ">= 10.x" 1897 + } 1898 + }, 1899 + "node_modules/sprintf-js": { 1900 + "version": "1.0.3", 1901 + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1902 + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", 1903 + "license": "BSD-3-Clause" 1904 + }, 1905 + "node_modules/standard-as-callback": { 1906 + "version": "2.1.0", 1907 + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", 1908 + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", 1909 + "license": "MIT" 1910 + }, 1911 + "node_modules/string_decoder": { 1912 + "version": "1.3.0", 1913 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1914 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1915 + "license": "MIT", 1916 + "dependencies": { 1917 + "safe-buffer": "~5.2.0" 1918 + } 1919 + }, 1920 + "node_modules/stringify-entities": { 1921 + "version": "4.0.4", 1922 + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", 1923 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", 1924 + "license": "MIT", 1925 + "dependencies": { 1926 + "character-entities-html4": "^2.0.0", 1927 + "character-entities-legacy": "^3.0.0" 1928 + }, 1929 + "funding": { 1930 + "type": "github", 1931 + "url": "https://github.com/sponsors/wooorm" 1932 + } 1933 + }, 1934 + "node_modules/strip-bom-string": { 1935 + "version": "1.0.0", 1936 + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", 1937 + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", 1938 + "license": "MIT", 1939 + "engines": { 1940 + "node": ">=0.10.0" 1941 + } 1942 + }, 1943 + "node_modules/thread-stream": { 1944 + "version": "2.7.0", 1945 + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", 1946 + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", 1947 + "license": "MIT", 1948 + "dependencies": { 1949 + "real-require": "^0.2.0" 1950 + } 1951 + }, 1952 + "node_modules/tlds": { 1953 + "version": "1.261.0", 1954 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", 1955 + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", 1956 + "license": "MIT", 1957 + "bin": { 1958 + "tlds": "bin.js" 1959 + } 1960 + }, 1961 + "node_modules/trim-lines": { 1962 + "version": "3.0.1", 1963 + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", 1964 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", 1965 + "license": "MIT", 1966 + "funding": { 1967 + "type": "github", 1968 + "url": "https://github.com/sponsors/wooorm" 1969 + } 1970 + }, 1971 + "node_modules/tslib": { 1972 + "version": "2.8.1", 1973 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 1974 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 1975 + "license": "0BSD" 1976 + }, 1977 + "node_modules/tsx": { 1978 + "version": "4.21.0", 1979 + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", 1980 + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", 1981 + "dev": true, 1982 + "license": "MIT", 1983 + "dependencies": { 1984 + "esbuild": "~0.27.0", 1985 + "get-tsconfig": "^4.7.5" 1986 + }, 1987 + "bin": { 1988 + "tsx": "dist/cli.mjs" 1989 + }, 1990 + "engines": { 1991 + "node": ">=18.0.0" 1992 + }, 1993 + "optionalDependencies": { 1994 + "fsevents": "~2.3.3" 1995 + } 1996 + }, 1997 + "node_modules/typescript": { 1998 + "version": "5.9.3", 1999 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 2000 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 2001 + "dev": true, 2002 + "license": "Apache-2.0", 2003 + "bin": { 2004 + "tsc": "bin/tsc", 2005 + "tsserver": "bin/tsserver" 2006 + }, 2007 + "engines": { 2008 + "node": ">=14.17" 2009 + } 2010 + }, 2011 + "node_modules/uint8arrays": { 2012 + "version": "3.0.0", 2013 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 2014 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 2015 + "license": "MIT", 2016 + "dependencies": { 2017 + "multiformats": "^9.4.2" 2018 + } 2019 + }, 2020 + "node_modules/undici-types": { 2021 + "version": "6.21.0", 2022 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 2023 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 2024 + "dev": true, 2025 + "license": "MIT" 2026 + }, 2027 + "node_modules/unicode-segmenter": { 2028 + "version": "0.14.5", 2029 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", 2030 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", 2031 + "license": "MIT" 2032 + }, 2033 + "node_modules/unist-util-is": { 2034 + "version": "6.0.1", 2035 + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", 2036 + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", 2037 + "license": "MIT", 2038 + "dependencies": { 2039 + "@types/unist": "^3.0.0" 2040 + }, 2041 + "funding": { 2042 + "type": "opencollective", 2043 + "url": "https://opencollective.com/unified" 2044 + } 2045 + }, 2046 + "node_modules/unist-util-position": { 2047 + "version": "5.0.0", 2048 + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", 2049 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", 2050 + "license": "MIT", 2051 + "dependencies": { 2052 + "@types/unist": "^3.0.0" 2053 + }, 2054 + "funding": { 2055 + "type": "opencollective", 2056 + "url": "https://opencollective.com/unified" 2057 + } 2058 + }, 2059 + "node_modules/unist-util-stringify-position": { 2060 + "version": "4.0.0", 2061 + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", 2062 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", 2063 + "license": "MIT", 2064 + "dependencies": { 2065 + "@types/unist": "^3.0.0" 2066 + }, 2067 + "funding": { 2068 + "type": "opencollective", 2069 + "url": "https://opencollective.com/unified" 2070 + } 2071 + }, 2072 + "node_modules/unist-util-visit": { 2073 + "version": "5.1.0", 2074 + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", 2075 + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", 2076 + "license": "MIT", 2077 + "dependencies": { 2078 + "@types/unist": "^3.0.0", 2079 + "unist-util-is": "^6.0.0", 2080 + "unist-util-visit-parents": "^6.0.0" 2081 + }, 2082 + "funding": { 2083 + "type": "opencollective", 2084 + "url": "https://opencollective.com/unified" 2085 + } 2086 + }, 2087 + "node_modules/unist-util-visit-parents": { 2088 + "version": "6.0.2", 2089 + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", 2090 + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", 2091 + "license": "MIT", 2092 + "dependencies": { 2093 + "@types/unist": "^3.0.0", 2094 + "unist-util-is": "^6.0.0" 2095 + }, 2096 + "funding": { 2097 + "type": "opencollective", 2098 + "url": "https://opencollective.com/unified" 2099 + } 2100 + }, 2101 + "node_modules/varint": { 2102 + "version": "6.0.0", 2103 + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", 2104 + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", 2105 + "license": "MIT" 2106 + }, 2107 + "node_modules/vfile": { 2108 + "version": "6.0.3", 2109 + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", 2110 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", 2111 + "license": "MIT", 2112 + "dependencies": { 2113 + "@types/unist": "^3.0.0", 2114 + "vfile-message": "^4.0.0" 2115 + }, 2116 + "funding": { 2117 + "type": "opencollective", 2118 + "url": "https://opencollective.com/unified" 2119 + } 2120 + }, 2121 + "node_modules/vfile-message": { 2122 + "version": "4.0.3", 2123 + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", 2124 + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", 2125 + "license": "MIT", 2126 + "dependencies": { 2127 + "@types/unist": "^3.0.0", 2128 + "unist-util-stringify-position": "^4.0.0" 2129 + }, 2130 + "funding": { 2131 + "type": "opencollective", 2132 + "url": "https://opencollective.com/unified" 2133 + } 2134 + }, 2135 + "node_modules/zod": { 2136 + "version": "3.25.76", 2137 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 2138 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 2139 + "license": "MIT", 2140 + "funding": { 2141 + "url": "https://github.com/sponsors/colinhacks" 2142 + } 2143 + }, 2144 + "node_modules/zwitch": { 2145 + "version": "2.0.4", 2146 + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", 2147 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", 2148 + "license": "MIT", 2149 + "funding": { 2150 + "type": "github", 2151 + "url": "https://github.com/sponsors/wooorm" 2152 + } 2153 + } 2154 + } 2155 + }
+32
package.json
··· 1 + { 2 + "name": "cameron-site", 3 + "private": true, 4 + "type": "module", 5 + "scripts": { 6 + "dev": "tsx --conditions source --watch src/index.tsx", 7 + "start": "tsx --conditions source src/index.tsx", 8 + "publish": "tsx scripts/publish.ts", 9 + "typecheck": "tsc --noEmit" 10 + }, 11 + "dependencies": { 12 + "@atproto/api": "^0.18.17", 13 + "@atproto/lex-resolver": "^0.0.15", 14 + "@atproto/syntax": "^0.3.0", 15 + "@hono/node-server": "^1.19.9", 16 + "dotenv": "^16.5.0", 17 + "gray-matter": "^4.0.3", 18 + "hono": "^4.12.2", 19 + "ioredis": "^5.6.0", 20 + "marked": "^15.0.0", 21 + "marked-footnote": "^1.4.0", 22 + "shiki": "^3.0.0" 23 + }, 24 + "devDependencies": { 25 + "@types/node": "^22.0.0", 26 + "tsx": "^4.0.0", 27 + "typescript": "^5.9.0" 28 + }, 29 + "engines": { 30 + "node": ">=22.0.0" 31 + } 32 + }
+230
public/host-primitives.css
··· 1 + /* Structural reset for inlay custom elements. 2 + Display modes, flex, sizing. No visual styling. 3 + Everything visual comes from host-theme.css. */ 4 + 5 + at-inlay-root { 6 + display: block; 7 + width: fit-content; 8 + max-width: 100%; 9 + max-height: 100%; 10 + box-sizing: border-box; 11 + overflow-y: auto; 12 + overflow-x: hidden; 13 + overflow-wrap: break-word; 14 + position: relative; 15 + } 16 + at-inlay-root[data-page] { 17 + width: 100%; 18 + max-width: 600px; 19 + } 20 + at-inlay-root[data-full] { 21 + width: 100%; 22 + min-height: 100%; 23 + max-width: none; 24 + max-height: none; 25 + border-radius: 0; 26 + } 27 + at-inlay-root:has(org-atsui-stack) { 28 + width: 100%; 29 + } 30 + 31 + /* Stack — vertical flex, fills parent width */ 32 + 33 + org-atsui-stack { 34 + display: flex; 35 + flex-direction: column; 36 + width: 100%; 37 + box-sizing: border-box; 38 + text-align: inherit; 39 + } 40 + org-atsui-stack[align="start"] { 41 + align-items: flex-start; 42 + } 43 + org-atsui-stack[align="center"] { 44 + align-items: center; 45 + } 46 + org-atsui-stack[align="end"] { 47 + align-items: flex-end; 48 + } 49 + org-atsui-stack[align="stretch"] { 50 + align-items: stretch; 51 + } 52 + org-atsui-stack[sticky] { 53 + position: sticky; 54 + top: 0; 55 + z-index: 1; 56 + } 57 + 58 + /* Row — horizontal flex, hugs content */ 59 + 60 + org-atsui-row { 61 + display: flex; 62 + flex-direction: row; 63 + flex-wrap: nowrap; 64 + align-items: center; 65 + text-align: inherit; 66 + } 67 + org-atsui-row[align="start"] { 68 + align-items: flex-start; 69 + } 70 + org-atsui-row[align="center"] { 71 + align-items: center; 72 + } 73 + org-atsui-row[align="end"] { 74 + align-items: flex-end; 75 + } 76 + org-atsui-row[sticky] { 77 + position: sticky; 78 + top: 0; 79 + z-index: 1; 80 + } 81 + 82 + /* Justify — shared between Stack and Row */ 83 + 84 + :is(org-atsui-stack, org-atsui-row)[justify="between"] { 85 + justify-content: space-between; 86 + } 87 + :is(org-atsui-stack, org-atsui-row)[justify="center"] { 88 + justify-content: center; 89 + } 90 + :is(org-atsui-stack, org-atsui-row)[justify="end"] { 91 + justify-content: flex-end; 92 + } 93 + 94 + /* Fill — greedy child, takes remaining space on parent main axis */ 95 + 96 + org-atsui-fill { 97 + display: flex; 98 + flex-direction: column; 99 + flex: 1; 100 + min-width: 0; 101 + min-height: 0; 102 + } 103 + 104 + /* Text elements */ 105 + 106 + org-atsui-title, 107 + org-atsui-heading, 108 + org-atsui-text, 109 + org-atsui-caption { 110 + display: block; 111 + overflow-wrap: break-word; 112 + } 113 + org-atsui-timestamp { 114 + display: inline; 115 + } 116 + 117 + /* Avatar — fixed dimensions, never stretches */ 118 + 119 + org-atsui-avatar { 120 + display: block; 121 + overflow: hidden; 122 + flex-shrink: 0; 123 + } 124 + org-atsui-avatar[size="xsmall"] { 125 + width: 20px; 126 + height: 20px; 127 + } 128 + org-atsui-avatar[size="small"] { 129 + width: 32px; 130 + height: 32px; 131 + } 132 + org-atsui-avatar[size="medium"] { 133 + width: 48px; 134 + height: 48px; 135 + } 136 + org-atsui-avatar[size="large"] { 137 + width: 80px; 138 + height: 80px; 139 + } 140 + org-atsui-avatar img { 141 + width: 100%; 142 + height: 100%; 143 + object-fit: cover; 144 + display: block; 145 + } 146 + 147 + /* Cover — full-bleed background image */ 148 + 149 + org-atsui-cover { 150 + display: block; 151 + width: 100%; 152 + } 153 + org-atsui-cover::before { 154 + content: ""; 155 + display: block; 156 + } 157 + 158 + /* Clip — constrains child height relative to own width. 159 + cqi custom props are set on the outer element; the inner 160 + div resolves them relative to Clip's inline size. */ 161 + 162 + org-atsui-clip { 163 + display: block; 164 + container-type: inline-size; 165 + } 166 + org-atsui-clip > div { 167 + display: grid; 168 + grid-template-rows: auto; 169 + min-height: var(--clip-min, 0px); 170 + max-height: var(--clip-max, none); 171 + overflow: hidden; 172 + } 173 + 174 + /* Blob — image container */ 175 + 176 + org-atsui-blob { 177 + display: block; 178 + overflow: hidden; 179 + width: 100%; 180 + } 181 + org-atsui-blob[fit] { 182 + height: 100%; 183 + } 184 + org-atsui-blob img { 185 + display: block; 186 + width: 100%; 187 + object-fit: cover; 188 + } 189 + org-atsui-blob[fit] img { 190 + height: 100%; 191 + } 192 + org-atsui-blob[fit="contain"] img { 193 + object-fit: contain; 194 + } 195 + 196 + /* Link — transparent wrapper */ 197 + 198 + org-atsui-link { 199 + display: contents; 200 + } 201 + org-atsui-link a { 202 + overflow-wrap: break-word; 203 + min-width: 0; 204 + } 205 + 206 + /* Tabs */ 207 + 208 + org-atsui-tabs { 209 + display: flex; 210 + flex-direction: column; 211 + } 212 + org-atsui-tabs .tabs-bar { 213 + display: flex; 214 + flex-direction: row; 215 + position: sticky; 216 + top: 0; 217 + z-index: 1; 218 + } 219 + org-atsui-tabs .tabs-bar button { 220 + all: unset; 221 + cursor: pointer; 222 + } 223 + 224 + /* Grid — equal columns */ 225 + 226 + org-atsui-grid { 227 + display: grid; 228 + grid-template-columns: repeat(var(--grid-cols, 3), 1fr); 229 + width: 100%; 230 + }
+169
public/host-theme.css
··· 1 + /* host-theme.css */ 2 + 3 + at-inlay-root { 4 + --text-primary: #272727; 5 + --text-body: #555; 6 + --text-secondary: #999; 7 + 8 + font-family: "Inter", system-ui, sans-serif; 9 + font-size: 15px; 10 + line-height: 1.5; 11 + color: var(--text-primary); 12 + background: #fff; 13 + border-radius: 12px; 14 + -webkit-font-smoothing: antialiased; 15 + } 16 + 17 + at-inlay-root[data-page], 18 + at-inlay-root[data-full] { 19 + border-radius: 0; 20 + } 21 + 22 + /* Gap */ 23 + 24 + :is(org-atsui-stack, org-atsui-row, org-atsui-grid)[gap="none"] { 25 + gap: 0; 26 + } 27 + :is(org-atsui-stack, org-atsui-row, org-atsui-grid)[gap="small"] { 28 + gap: 6px; 29 + } 30 + :is(org-atsui-stack, org-atsui-row, org-atsui-grid)[gap="medium"] { 31 + gap: 12px; 32 + } 33 + :is(org-atsui-stack, org-atsui-row, org-atsui-grid)[gap="large"] { 34 + gap: 24px; 35 + } 36 + 37 + /* Type */ 38 + 39 + org-atsui-title { 40 + font-size: 1.5rem; 41 + font-weight: 700; 42 + line-height: 1.2; 43 + letter-spacing: -0.02em; 44 + color: var(--text-primary); 45 + } 46 + 47 + org-atsui-heading { 48 + font-weight: 600; 49 + color: var(--text-primary); 50 + } 51 + 52 + org-atsui-text { 53 + color: var(--text-body); 54 + } 55 + 56 + org-atsui-text b, 57 + org-atsui-text strong { 58 + font-weight: 600; 59 + color: var(--text-primary); 60 + } 61 + 62 + org-atsui-caption { 63 + font-size: 0.8125rem; 64 + color: var(--text-secondary); 65 + } 66 + 67 + /* Avatar */ 68 + 69 + org-atsui-avatar { 70 + border-radius: 50%; 71 + } 72 + 73 + /* Cover */ 74 + 75 + org-atsui-cover::before { 76 + height: 200px; 77 + background: 78 + linear-gradient(to top, rgba(0, 0, 0, 0.4) 0%, transparent 50%), 79 + var(--cover-src) center / cover no-repeat; 80 + } 81 + 82 + /* Avatar lift */ 83 + 84 + org-atsui-avatar[lift] { 85 + position: relative; 86 + z-index: 1; 87 + box-shadow: 0 0 0 3px #fff; 88 + } 89 + org-atsui-avatar[lift][size="xsmall"] { 90 + margin-top: calc(-10px - var(--inset, 0px)); 91 + } 92 + org-atsui-avatar[lift][size="small"] { 93 + margin-top: calc(-16px - var(--inset, 0px)); 94 + } 95 + org-atsui-avatar[lift][size="medium"] { 96 + margin-top: calc(-24px - var(--inset, 0px)); 97 + } 98 + org-atsui-avatar[lift][size="large"] { 99 + margin-top: calc(-40px - var(--inset, 0px)); 100 + } 101 + 102 + /* Blob */ 103 + 104 + org-atsui-blob img { 105 + border-radius: 8px; 106 + } 107 + 108 + /* Inset */ 109 + 110 + :is(org-atsui-stack, org-atsui-row)[inset] { 111 + --inset: 12px; 112 + padding: 12px; 113 + } 114 + 115 + /* Link */ 116 + 117 + org-atsui-link a { 118 + text-decoration: none; 119 + color: inherit; 120 + display: block; 121 + } 122 + org-atsui-link[decoration="underline"] a { 123 + text-decoration: underline; 124 + color: #b497bf; 125 + } 126 + 127 + org-atsui-link a:hover { 128 + opacity: 0.85; 129 + } 130 + 131 + /* List separators */ 132 + 133 + hr { 134 + border: none; 135 + height: 1px; 136 + background: #eee; 137 + margin: 0; 138 + width: 100%; 139 + } 140 + 141 + /* Tabs */ 142 + 143 + org-atsui-tabs .tabs-bar { 144 + gap: 0; 145 + border-bottom: 1px solid #eee; 146 + padding: 0 16px; 147 + background: #fff; 148 + } 149 + 150 + org-atsui-tabs .tabs-bar button { 151 + flex: 1; 152 + text-align: center; 153 + font-size: 0.8125rem; 154 + font-weight: 500; 155 + padding: 10px 0; 156 + color: var(--text-secondary); 157 + border-bottom: 2px solid transparent; 158 + margin-bottom: -1px; 159 + } 160 + 161 + org-atsui-tabs .tabs-bar button[aria-selected="true"] { 162 + color: var(--text-primary); 163 + border-bottom-color: var(--text-primary); 164 + font-weight: 600; 165 + } 166 + 167 + org-atsui-tabs .tabs-bar button:hover:not([aria-selected="true"]) { 168 + color: #666; 169 + }
public/images/banner.jpg

This is a binary file and will not be displayed.

public/images/banner.png

This is a binary file and will not be displayed.

public/images/banner_old.png

This is a binary file and will not be displayed.

public/images/bannerwhite.png

This is a binary file and will not be displayed.

public/images/congo.jpg

This is a binary file and will not be displayed.

public/images/grad-champ.jpg

This is a binary file and will not be displayed.

public/images/laugh.jpg

This is a binary file and will not be displayed.

public/images/lugano.png

This is a binary file and will not be displayed.

public/images/mom.jpg

This is a binary file and will not be displayed.

public/images/social-ai/bluesky-atproto-architecture.svg.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/activity-patterns.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/anisota-harvest-post-type-counts-did_plc_mxzuau6m53jtdsbqe6f4laov-2025.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/calendar.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/engagements.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/top-posts.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/top-words.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/writing-stats.png

This is a binary file and will not be displayed.

public/images/void-harvest-2025/year-highlight.png

This is a binary file and will not be displayed.

+620
public/site.css
··· 1 + /* cameron.stream site theme */ 2 + 3 + :root { 4 + --site-bg: #fff; 5 + --site-surface: #fff; 6 + --site-text: #1a1a1a; 7 + --site-text-secondary: #2a2a2a; 8 + --site-text-muted: #555; 9 + --site-accent: #23a64d; 10 + --site-border: #e8e8e8; 11 + --site-code-bg: #f5f5f5; 12 + --site-max-width: 640px; 13 + --site-font-body: "Inter", system-ui, -apple-system, sans-serif; 14 + } 15 + 16 + /* Dark mode: explicit toggle or system preference fallback */ 17 + :root[data-theme="dark"] { 18 + --site-bg: #0a0a0a; 19 + --site-surface: #141414; 20 + --site-text: #e5e5e5; 21 + --site-text-secondary: #b0b0b0; 22 + --site-text-muted: #8a8a8a; 23 + --site-accent: #3fbf6a; 24 + --site-border: #2a2a2a; 25 + --site-code-bg: #1a1a1a; 26 + } 27 + @media (prefers-color-scheme: dark) { 28 + :root:not([data-theme="light"]) { 29 + --site-bg: #0a0a0a; 30 + --site-surface: #141414; 31 + --site-text: #e5e5e5; 32 + --site-text-secondary: #b0b0b0; 33 + --site-text-muted: #8a8a8a; 34 + --site-accent: #3fbf6a; 35 + --site-border: #2a2a2a; 36 + --site-code-bg: #1a1a1a; 37 + } 38 + } 39 + 40 + /* Inlay dark overrides */ 41 + :root[data-theme="dark"] at-inlay-root { 42 + --text-primary: #ddd; 43 + --text-body: #bbb; 44 + --text-secondary: #888; 45 + background: var(--site-surface); 46 + } 47 + :root[data-theme="dark"] org-atsui-avatar[lift] { 48 + box-shadow: 0 0 0 3px var(--site-surface); 49 + } 50 + :root[data-theme="dark"] org-atsui-tabs .tabs-bar { 51 + background: var(--site-surface); 52 + border-bottom-color: var(--site-border); 53 + } 54 + :root[data-theme="dark"] hr { 55 + background: var(--site-border); 56 + } 57 + 58 + @media (prefers-color-scheme: dark) { 59 + :root:not([data-theme="light"]) at-inlay-root { 60 + --text-primary: #ddd; 61 + --text-body: #bbb; 62 + --text-secondary: #888; 63 + background: var(--site-surface); 64 + } 65 + :root:not([data-theme="light"]) org-atsui-avatar[lift] { 66 + box-shadow: 0 0 0 3px var(--site-surface); 67 + } 68 + :root:not([data-theme="light"]) org-atsui-tabs .tabs-bar { 69 + background: var(--site-surface); 70 + border-bottom-color: var(--site-border); 71 + } 72 + :root:not([data-theme="light"]) hr { 73 + background: var(--site-border); 74 + } 75 + } 76 + 77 + /* Theme toggle button */ 78 + .theme-toggle { 79 + position: fixed; 80 + top: 1em; 81 + right: 1em; 82 + background: none; 83 + border: none; 84 + cursor: pointer; 85 + color: var(--site-text-muted); 86 + font-size: 1.125rem; 87 + padding: 0.25em; 88 + line-height: 1; 89 + transition: color 0.15s; 90 + z-index: 10; 91 + } 92 + .theme-toggle:hover { 93 + color: var(--site-text); 94 + } 95 + 96 + body { 97 + margin: 0; 98 + font-family: var(--site-font-body); 99 + background: var(--site-bg); 100 + color: var(--site-text); 101 + font-size: 16px; 102 + line-height: 1.5; 103 + -webkit-font-smoothing: antialiased; 104 + } 105 + 106 + /* Page headings */ 107 + 108 + h1 { 109 + font-family: var(--site-font-body); 110 + } 111 + 112 + /* Blog post rendered HTML */ 113 + 114 + .blog-content { 115 + line-height: 1.7; 116 + color: var(--site-text); 117 + } 118 + 119 + .blog-content h1, 120 + .blog-content h2, 121 + .blog-content h3 { 122 + font-family: var(--site-font-body); 123 + color: var(--site-text); 124 + margin-top: 2em; 125 + margin-bottom: 0.5em; 126 + line-height: 1.25; 127 + letter-spacing: -0.01em; 128 + } 129 + 130 + .blog-content h1 { font-size: 1.625rem; font-weight: 700; } 131 + .blog-content h2 { font-size: 1.25rem; font-weight: 700; } 132 + .blog-content h3 { font-size: 1.125rem; font-weight: 600; } 133 + 134 + .blog-content p { 135 + margin: 1em 0; 136 + } 137 + 138 + .blog-content a { 139 + color: var(--site-accent); 140 + text-decoration: none; 141 + } 142 + .blog-content a:hover { 143 + text-decoration: underline; 144 + } 145 + 146 + .blog-content code { 147 + font-family: "SF Mono", "Menlo", monospace; 148 + font-size: 0.875em; 149 + background: var(--site-code-bg); 150 + padding: 0.15em 0.4em; 151 + border-radius: 4px; 152 + } 153 + 154 + .blog-content pre { 155 + background: var(--site-code-bg); 156 + padding: 1em; 157 + border-radius: 8px; 158 + overflow-x: auto; 159 + font-size: 0.875em; 160 + line-height: 1.5; 161 + margin: 1.5em 0; 162 + } 163 + 164 + .blog-content pre code { 165 + background: none; 166 + padding: 0; 167 + border-radius: 0; 168 + } 169 + 170 + .blog-content blockquote { 171 + border-left: 3px solid var(--site-border); 172 + margin: 1.5em 0; 173 + padding: 0.5em 1em; 174 + color: var(--site-text-secondary); 175 + } 176 + 177 + .blog-content img { 178 + max-width: 100%; 179 + border-radius: 8px; 180 + } 181 + 182 + .blog-content .bluesky-embed { 183 + margin: 1.5em auto; 184 + max-width: 100%; 185 + } 186 + 187 + .blog-content ul, .blog-content ol { 188 + padding-left: 1.5em; 189 + } 190 + 191 + .blog-content li { 192 + margin: 0.3em 0; 193 + } 194 + 195 + /* Nav */ 196 + 197 + .site-nav { 198 + display: flex; 199 + align-items: center; 200 + gap: 1.5em; 201 + padding: 1em 0; 202 + font-size: 0.875rem; 203 + } 204 + 205 + .site-nav a { 206 + color: var(--site-text-secondary); 207 + text-decoration: none; 208 + transition: color 0.15s; 209 + } 210 + .site-nav a:hover { 211 + color: var(--site-text); 212 + } 213 + 214 + /* Post list items */ 215 + 216 + .post-item { 217 + padding: 1em 0; 218 + } 219 + .post-item + .post-item { 220 + border-top: 1px solid var(--site-border); 221 + } 222 + 223 + .post-date { 224 + font-size: 0.8125rem; 225 + color: var(--site-text-muted); 226 + } 227 + 228 + .post-title { 229 + font-family: var(--site-font-body); 230 + font-weight: 600; 231 + color: var(--site-text); 232 + text-decoration: none; 233 + line-height: 1.3; 234 + } 235 + .post-title:hover { 236 + color: var(--site-accent); 237 + } 238 + 239 + .post-summary { 240 + font-size: 0.9rem; 241 + color: var(--site-text-secondary); 242 + margin-top: 0.3em; 243 + line-height: 1.5; 244 + } 245 + 246 + /* Annotations */ 247 + 248 + .annotations-list { 249 + display: flex; 250 + flex-direction: column; 251 + gap: 0; 252 + } 253 + 254 + .annotation-item { 255 + padding: 0.75em 0; 256 + } 257 + .annotation-item + .annotation-item { 258 + border-top: 1px solid var(--site-border); 259 + } 260 + 261 + .annotation-body { 262 + font-size: 0.9rem; 263 + margin: 0.3em 0 0; 264 + line-height: 1.5; 265 + } 266 + 267 + .annotation-quote { 268 + font-size: 0.8125rem; 269 + color: var(--site-text-secondary); 270 + border-left: 2px solid var(--site-accent); 271 + padding-left: 0.75em; 272 + margin: 0.4em 0; 273 + font-style: italic; 274 + } 275 + 276 + .annotation-meta { 277 + display: flex; 278 + gap: 0.75em; 279 + align-items: center; 280 + margin-top: 0.3em; 281 + font-size: 0.75rem; 282 + color: var(--site-text-muted); 283 + } 284 + 285 + /* Endorsements */ 286 + 287 + .endorsements-grid { 288 + display: grid; 289 + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 290 + gap: 0.75em; 291 + } 292 + 293 + .endorsement-card { 294 + padding: 0.75em 1em; 295 + border: 1px solid var(--site-border); 296 + border-radius: 6px; 297 + transition: border-color 0.15s; 298 + } 299 + .endorsement-card:hover { 300 + border-color: var(--site-accent); 301 + } 302 + 303 + .endorsement-name { 304 + color: var(--site-text); 305 + text-decoration: none; 306 + font-weight: 600; 307 + font-size: 0.9rem; 308 + } 309 + .endorsement-name:hover { 310 + color: var(--site-accent); 311 + } 312 + 313 + .endorsement-meta { 314 + display: flex; 315 + justify-content: space-between; 316 + align-items: center; 317 + margin-top: 0.3em; 318 + font-size: 0.75rem; 319 + color: var(--site-text-muted); 320 + } 321 + 322 + /* Margin annotations */ 323 + 324 + .margin-annotations { 325 + margin-top: 2em; 326 + padding-top: 1.5em; 327 + border-top: 1px solid var(--site-border); 328 + } 329 + 330 + .margin-annotation { 331 + padding: 0.75em 0; 332 + } 333 + .margin-annotation + .margin-annotation { 334 + border-top: 1px solid var(--site-border); 335 + } 336 + 337 + .margin-highlight { 338 + font-size: 0.8125rem; 339 + color: var(--site-text-secondary); 340 + border-left: 2px solid var(--site-accent); 341 + padding-left: 0.75em; 342 + margin: 0 0 0.4em 0; 343 + font-style: italic; 344 + } 345 + 346 + .margin-body { 347 + font-size: 0.875rem; 348 + margin: 0; 349 + line-height: 1.5; 350 + } 351 + 352 + .margin-meta { 353 + font-size: 0.75rem; 354 + color: var(--site-text-muted); 355 + margin-top: 0.3em; 356 + display: flex; 357 + gap: 0.75em; 358 + } 359 + 360 + .margin-meta a { 361 + color: var(--site-accent); 362 + text-decoration: none; 363 + } 364 + 365 + /* Feed */ 366 + 367 + .feed { 368 + max-height: 80vh; 369 + overflow-y: auto; 370 + scrollbar-width: thin; 371 + } 372 + 373 + .feed-post { 374 + padding: 0.75em 0; 375 + } 376 + .feed-post + .feed-post { 377 + border-top: 1px solid var(--site-border); 378 + } 379 + 380 + .feed-post-header { 381 + display: flex; 382 + align-items: center; 383 + gap: 0.5em; 384 + margin-bottom: 0.4em; 385 + } 386 + 387 + .feed-avatar { 388 + width: 32px; 389 + height: 32px; 390 + border-radius: 50%; 391 + object-fit: cover; 392 + } 393 + .feed-avatar-sm { 394 + width: 18px; 395 + height: 18px; 396 + border-radius: 50%; 397 + object-fit: cover; 398 + } 399 + 400 + .feed-display-name { 401 + font-weight: 600; 402 + font-size: 0.85rem; 403 + } 404 + 405 + .feed-handle { 406 + color: var(--site-text-muted); 407 + font-size: 0.8rem; 408 + margin-left: 0.25em; 409 + } 410 + 411 + .feed-time { 412 + color: var(--site-text-muted); 413 + font-size: 0.75rem; 414 + text-decoration: none; 415 + margin-left: 0.5em; 416 + } 417 + .feed-time:hover { 418 + text-decoration: underline; 419 + } 420 + 421 + .feed-text { 422 + font-size: 0.9rem; 423 + line-height: 1.5; 424 + white-space: pre-wrap; 425 + word-break: break-word; 426 + } 427 + 428 + .feed-link { 429 + color: var(--site-accent); 430 + text-decoration: none; 431 + word-break: break-all; 432 + } 433 + .feed-link:hover { 434 + text-decoration: underline; 435 + } 436 + 437 + .feed-mention { 438 + color: var(--site-accent); 439 + text-decoration: none; 440 + } 441 + 442 + /* Feed images */ 443 + 444 + .feed-images { 445 + margin-top: 0.5em; 446 + border-radius: 8px; 447 + overflow: hidden; 448 + max-height: 240px; 449 + } 450 + .feed-images img { 451 + width: 100%; 452 + display: block; 453 + max-height: 240px; 454 + object-fit: cover; 455 + } 456 + .feed-images-grid { 457 + display: grid; 458 + grid-template-columns: 1fr 1fr; 459 + gap: 2px; 460 + } 461 + .feed-images-grid img { 462 + aspect-ratio: 1; 463 + object-fit: cover; 464 + } 465 + 466 + /* Feed link card */ 467 + 468 + .feed-card { 469 + display: flex; 470 + margin-top: 0.5em; 471 + border: 1px solid var(--site-border); 472 + border-radius: 8px; 473 + overflow: hidden; 474 + text-decoration: none; 475 + color: inherit; 476 + } 477 + .feed-card:hover { 478 + border-color: var(--site-text-muted); 479 + } 480 + .feed-card-thumb { 481 + width: 100px; 482 + object-fit: cover; 483 + flex-shrink: 0; 484 + } 485 + .feed-card-body { 486 + padding: 0.5em 0.75em; 487 + min-width: 0; 488 + } 489 + .feed-card-title { 490 + font-weight: 600; 491 + font-size: 0.8rem; 492 + overflow: hidden; 493 + text-overflow: ellipsis; 494 + white-space: nowrap; 495 + } 496 + .feed-card-desc { 497 + font-size: 0.75rem; 498 + color: var(--site-text-secondary); 499 + margin-top: 0.15em; 500 + overflow: hidden; 501 + text-overflow: ellipsis; 502 + white-space: nowrap; 503 + } 504 + .feed-card-url { 505 + font-size: 0.7rem; 506 + color: var(--site-text-muted); 507 + margin-top: 0.25em; 508 + } 509 + 510 + /* Feed quote post */ 511 + 512 + .feed-quote { 513 + margin-top: 0.5em; 514 + border: 1px solid var(--site-border); 515 + border-radius: 8px; 516 + padding: 0.5em 0.75em; 517 + } 518 + .feed-quote-author { 519 + display: flex; 520 + align-items: center; 521 + gap: 0.35em; 522 + margin-bottom: 0.25em; 523 + } 524 + .feed-quote-text { 525 + font-size: 0.8rem; 526 + line-height: 1.4; 527 + color: var(--site-text-secondary); 528 + } 529 + 530 + /* Feed stats */ 531 + 532 + .feed-stats { 533 + display: flex; 534 + gap: 1em; 535 + margin-top: 0.4em; 536 + font-size: 0.75rem; 537 + color: var(--site-text-muted); 538 + } 539 + 540 + /* Semble */ 541 + 542 + .semble-collections { 543 + display: grid; 544 + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 545 + gap: 0.75em; 546 + } 547 + 548 + .semble-collection-card { 549 + display: block; 550 + padding: 0.75em 1em; 551 + border: 1px solid var(--site-border); 552 + border-radius: 6px; 553 + text-decoration: none; 554 + color: inherit; 555 + transition: border-color 0.15s; 556 + } 557 + .semble-collection-card:hover { 558 + border-color: var(--site-accent); 559 + } 560 + 561 + .semble-collection-name { 562 + font-weight: 600; 563 + font-size: 0.9rem; 564 + color: var(--site-text); 565 + } 566 + 567 + .semble-collection-desc { 568 + font-size: 0.8125rem; 569 + color: var(--site-text-secondary); 570 + margin-top: 0.2em; 571 + line-height: 1.4; 572 + } 573 + 574 + .semble-collection-meta { 575 + font-size: 0.75rem; 576 + color: var(--site-text-muted); 577 + margin-top: 0.4em; 578 + } 579 + 580 + .semble-cards { 581 + display: flex; 582 + flex-direction: column; 583 + gap: 0; 584 + } 585 + 586 + .semble-card-item { 587 + padding: 0.75em 0; 588 + } 589 + .semble-card-item + .semble-card-item { 590 + border-top: 1px solid var(--site-border); 591 + } 592 + 593 + .semble-card-item a:hover { 594 + color: var(--site-accent); 595 + } 596 + 597 + .semble-card-note { 598 + font-size: 0.8125rem; 599 + color: var(--site-text-secondary); 600 + border-left: 2px solid var(--site-accent); 601 + padding-left: 0.75em; 602 + margin: 0.3em 0; 603 + font-style: italic; 604 + } 605 + 606 + .semble-card-meta { 607 + display: flex; 608 + gap: 0.75em; 609 + font-size: 0.75rem; 610 + color: var(--site-text-muted); 611 + margin-top: 0.3em; 612 + } 613 + 614 + /* Responsive: stack columns on mobile */ 615 + 616 + @media (max-width: 680px) { 617 + .page-container { 618 + max-width: 100% !important; 619 + } 620 + }
+23
public/theme.js
··· 1 + (function () { 2 + var btn = document.getElementById("theme-toggle"); 3 + if (!btn) return; 4 + 5 + function isDark() { 6 + var t = document.documentElement.getAttribute("data-theme"); 7 + if (t) return t === "dark"; 8 + return window.matchMedia("(prefers-color-scheme: dark)").matches; 9 + } 10 + 11 + function update() { 12 + btn.textContent = isDark() ? "\u2600" : "\u263E"; 13 + } 14 + 15 + btn.addEventListener("click", function () { 16 + var next = isDark() ? "light" : "dark"; 17 + document.documentElement.setAttribute("data-theme", next); 18 + localStorage.setItem("theme", next); 19 + update(); 20 + }); 21 + 22 + update(); 23 + })();
+51
scripts/cleanup-old-records.ts
··· 1 + // Delete all stream.cameron.blog.post records (replaced by site.standard.document). 2 + // Usage: npx tsx scripts/cleanup-old-records.ts 3 + 4 + import { resolve } from "node:path"; 5 + import { config } from "dotenv"; 6 + import { AtpAgent } from "@atproto/api"; 7 + 8 + config({ path: resolve(process.cwd(), ".env") }); 9 + 10 + async function main() { 11 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 12 + const identifier = process.env.ATP_IDENTIFIER; 13 + const password = process.env.ATP_PASSWORD; 14 + 15 + if (!identifier || !password) { 16 + console.error("Set ATP_IDENTIFIER and ATP_PASSWORD in .env"); 17 + process.exit(1); 18 + } 19 + 20 + const agent = new AtpAgent({ service }); 21 + await agent.login({ identifier, password }); 22 + 23 + const collection = "stream.cameron.blog.post"; 24 + const records = await agent.com.atproto.repo.listRecords({ 25 + repo: agent.session!.did, 26 + collection, 27 + limit: 100, 28 + }); 29 + 30 + if (records.data.records.length === 0) { 31 + console.log("No stream.cameron.blog.post records to delete."); 32 + return; 33 + } 34 + 35 + for (const record of records.data.records) { 36 + const rkey = record.uri.split("/").pop()!; 37 + console.log(`Deleting ${collection}/${rkey}...`); 38 + await agent.com.atproto.repo.deleteRecord({ 39 + repo: agent.session!.did, 40 + collection, 41 + rkey, 42 + }); 43 + } 44 + 45 + console.log(`Deleted ${records.data.records.length} records.`); 46 + } 47 + 48 + main().catch((err) => { 49 + console.error(err); 50 + process.exit(1); 51 + });
+33
scripts/delete-record.ts
··· 1 + // Delete a record by rkey. 2 + // Usage: npx tsx scripts/delete-record.ts <collection> <rkey> 3 + 4 + import { resolve } from "node:path"; 5 + import { config } from "dotenv"; 6 + import { AtpAgent } from "@atproto/api"; 7 + 8 + config({ path: resolve(process.cwd(), ".env") }); 9 + 10 + async function main() { 11 + const collection = process.argv[2]; 12 + const rkey = process.argv[3]; 13 + if (!collection || !rkey) { 14 + console.error("Usage: npx tsx scripts/delete-record.ts <collection> <rkey>"); 15 + process.exit(1); 16 + } 17 + 18 + const agent = new AtpAgent({ service: process.env.ATP_SERVICE || "https://bsky.social" }); 19 + await agent.login({ identifier: process.env.ATP_IDENTIFIER!, password: process.env.ATP_PASSWORD! }); 20 + 21 + await agent.com.atproto.repo.deleteRecord({ 22 + repo: agent.session!.did, 23 + collection, 24 + rkey, 25 + }); 26 + 27 + console.log(`Deleted ${collection}/${rkey}`); 28 + } 29 + 30 + main().catch((err) => { 31 + console.error(err); 32 + process.exit(1); 33 + });
+157
scripts/embed-images.ts
··· 1 + // Upload blog images as blobs and embed refs in the document records 2 + // so the PDS keeps them alive. Outputs an updated image-map.json. 3 + // 4 + // Usage: npx tsx scripts/embed-images.ts <images-dir> 5 + 6 + import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs"; 7 + import { join, relative, resolve } from "node:path"; 8 + import { config } from "dotenv"; 9 + import { AtpAgent, BlobRef } from "@atproto/api"; 10 + 11 + config({ path: resolve(process.cwd(), ".env") }); 12 + 13 + const COLLECTION = "site.standard.document"; 14 + 15 + const MIME_TYPES: Record<string, string> = { 16 + ".jpg": "image/jpeg", 17 + ".jpeg": "image/jpeg", 18 + ".png": "image/png", 19 + ".webp": "image/webp", 20 + ".gif": "image/gif", 21 + }; 22 + 23 + function walkDir(dir: string): string[] { 24 + const files: string[] = []; 25 + for (const entry of readdirSync(dir)) { 26 + const full = join(dir, entry); 27 + if (statSync(full).isDirectory()) { 28 + files.push(...walkDir(full)); 29 + } else { 30 + const ext = full.slice(full.lastIndexOf(".")).toLowerCase(); 31 + if (MIME_TYPES[ext]) files.push(full); 32 + } 33 + } 34 + return files; 35 + } 36 + 37 + async function main() { 38 + const imagesDir = process.argv[2]; 39 + if (!imagesDir) { 40 + console.error("Usage: npx tsx scripts/embed-images.ts <images-dir>"); 41 + process.exit(1); 42 + } 43 + 44 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 45 + const identifier = process.env.ATP_IDENTIFIER!; 46 + const password = process.env.ATP_PASSWORD!; 47 + const publicationUri = process.env.PUBLICATION_URI!; 48 + 49 + const agent = new AtpAgent({ service }); 50 + await agent.login({ identifier, password }); 51 + const did = agent.session!.did; 52 + 53 + // Build path -> file mapping 54 + const absDir = resolve(imagesDir); 55 + const imageFiles = walkDir(absDir); 56 + const pathToFile: Record<string, string> = {}; 57 + for (const f of imageFiles) { 58 + const rel = relative(absDir, f); 59 + pathToFile[`/assets/images/${rel}`] = f; 60 + } 61 + 62 + console.log(`Found ${imageFiles.length} images.\n`); 63 + 64 + // Get all our documents 65 + const existing = await agent.com.atproto.repo.listRecords({ 66 + repo: did, 67 + collection: COLLECTION, 68 + limit: 100, 69 + }); 70 + 71 + const docs = existing.data.records.filter( 72 + (r) => (r.value as any).site === publicationUri 73 + ); 74 + 75 + console.log(`Found ${docs.length} documents to check.\n`); 76 + 77 + const mapping: Record<string, string> = {}; 78 + // Track which images have been uploaded (path -> BlobRef) 79 + const uploadedBlobs: Record<string, BlobRef> = {}; 80 + 81 + for (const doc of docs) { 82 + const val = doc.value as any; 83 + const content = val.content?.value ?? ""; 84 + const rkey = doc.uri.split("/").pop()!; 85 + 86 + // Find /assets/images/ references in markdown 87 + const imageRefs: string[] = []; 88 + const regex = /\/assets\/images\/[^\s)]+/g; 89 + let match; 90 + while ((match = regex.exec(content)) !== null) { 91 + imageRefs.push(match[0]); 92 + } 93 + 94 + if (imageRefs.length === 0) continue; 95 + 96 + console.log(`${val.title}: ${imageRefs.length} image(s)`); 97 + 98 + // Upload and collect blob refs for this document 99 + const blobEntries: Array<{ path: string; image: BlobRef }> = []; 100 + 101 + for (const imgPath of imageRefs) { 102 + const file = pathToFile[imgPath]; 103 + if (!file) { 104 + console.log(` SKIP ${imgPath}: file not found`); 105 + continue; 106 + } 107 + 108 + let blobRef = uploadedBlobs[imgPath]; 109 + if (!blobRef) { 110 + const ext = file.slice(file.lastIndexOf(".")).toLowerCase(); 111 + const mimeType = MIME_TYPES[ext] || "application/octet-stream"; 112 + const data = readFileSync(file); 113 + 114 + const result = await agent.uploadBlob(new Uint8Array(data), { 115 + encoding: mimeType, 116 + }); 117 + blobRef = result.data.blob; 118 + uploadedBlobs[imgPath] = blobRef; 119 + console.log(` UPLOAD ${imgPath} -> ${blobRef.ref.toString()}`); 120 + await new Promise((r) => setTimeout(r, 100)); 121 + } else { 122 + console.log(` REUSE ${imgPath}`); 123 + } 124 + 125 + blobEntries.push({ path: imgPath, image: blobRef }); 126 + mapping[imgPath] = `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${blobRef.ref.toString()}@jpeg`; 127 + } 128 + 129 + // Update the record with embedded image refs 130 + const updatedRecord = { 131 + ...val, 132 + images: blobEntries.map((e) => ({ 133 + path: e.path, 134 + image: e.image, 135 + alt: "", 136 + })), 137 + }; 138 + 139 + await agent.com.atproto.repo.putRecord({ 140 + repo: did, 141 + collection: COLLECTION, 142 + rkey, 143 + record: updatedRecord, 144 + }); 145 + console.log(` UPDATED record ${rkey}\n`); 146 + } 147 + 148 + // Write mapping 149 + const outPath = resolve(process.cwd(), "src/image-map.json"); 150 + writeFileSync(outPath, JSON.stringify(mapping, null, 2)); 151 + console.log(`\nMapping written to ${outPath}`); 152 + } 153 + 154 + main().catch((err) => { 155 + console.error(err); 156 + process.exit(1); 157 + });
+69
scripts/fix-paths-no-blog.ts
··· 1 + // Strip /blog/ prefix from all document path fields. 2 + // /blog/slug -> /slug 3 + 4 + import "dotenv/config"; 5 + import { AtpAgent } from "@atproto/api"; 6 + 7 + const COLLECTION = "site.standard.document"; 8 + const PUBLICATION_URI = 9 + process.env.PUBLICATION_URI || 10 + "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3md7ylshxzk2y"; 11 + 12 + const agent = new AtpAgent({ service: process.env.ATP_SERVICE || "https://bsky.social" }); 13 + 14 + async function main() { 15 + await agent.login({ 16 + identifier: process.env.ATP_IDENTIFIER!, 17 + password: process.env.ATP_PASSWORD!, 18 + }); 19 + 20 + const did = agent.session!.did; 21 + console.log(`Logged in as ${did}\n`); 22 + 23 + let cursor: string | undefined; 24 + const records: Array<{ uri: string; cid: string; value: Record<string, any> }> = []; 25 + do { 26 + const res = await agent.api.com.atproto.repo.listRecords({ 27 + repo: did, 28 + collection: COLLECTION, 29 + limit: 100, 30 + cursor, 31 + }); 32 + records.push(...(res.data.records as any[])); 33 + cursor = res.data.cursor; 34 + } while (cursor); 35 + 36 + const ours = records.filter((r) => r.value.site === PUBLICATION_URI); 37 + console.log(`Found ${ours.length} records\n`); 38 + 39 + let changed = 0; 40 + for (const record of ours) { 41 + const rkey = record.uri.split("/").pop()!; 42 + const path: string = record.value.path || ""; 43 + 44 + if (!path.startsWith("/blog/")) { 45 + console.log(`OK: ${path}`); 46 + continue; 47 + } 48 + 49 + const newPath = path.replace("/blog/", "/"); 50 + console.log(`FIX: ${path} → ${newPath}`); 51 + 52 + await agent.api.com.atproto.repo.putRecord({ 53 + repo: did, 54 + collection: COLLECTION, 55 + rkey, 56 + record: { ...record.value, path: newPath }, 57 + }); 58 + 59 + changed++; 60 + await new Promise((r) => setTimeout(r, 200)); 61 + } 62 + 63 + console.log(`\nDone. Changed: ${changed}`); 64 + } 65 + 66 + main().catch((e) => { 67 + console.error(e); 68 + process.exit(1); 69 + });
+88
scripts/fix-paths.ts
··· 1 + // Fix Leaflet documents that have TID-style paths to use proper /blog/slug paths. 2 + // Usage: npx tsx scripts/fix-paths.ts 3 + 4 + import { resolve } from "node:path"; 5 + import { config } from "dotenv"; 6 + import { AtpAgent } from "@atproto/api"; 7 + 8 + config({ path: resolve(process.cwd(), ".env") }); 9 + 10 + function slugify(title: string): string { 11 + return title 12 + .toLowerCase() 13 + .replace(/['']/g, "") 14 + .replace(/[^a-z0-9]+/g, "-") 15 + .replace(/^-|-$/g, ""); 16 + } 17 + 18 + // Manual slug overrides for better URLs 19 + const SLUG_OVERRIDES: Record<string, string> = { 20 + "The Mint Condition Is Here: Big Stack, Fresh Finish": "mint-condition", 21 + "What does good AI memory feel like?": "good-ai-memory", 22 + "Ezra's Architecture": "ezra-architecture", 23 + "Central": "central-leaflet", // avoid conflict with existing /blog/central 24 + }; 25 + 26 + async function main() { 27 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 28 + const identifier = process.env.ATP_IDENTIFIER!; 29 + const password = process.env.ATP_PASSWORD!; 30 + 31 + const agent = new AtpAgent({ service }); 32 + await agent.login({ identifier, password }); 33 + const did = agent.session!.did; 34 + 35 + const pubUri = 36 + "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3md7ylshxzk2y"; 37 + 38 + const res = await agent.com.atproto.repo.listRecords({ 39 + repo: did, 40 + collection: "site.standard.document", 41 + limit: 100, 42 + }); 43 + 44 + const existingSlugs = new Set<string>(); 45 + for (const r of res.data.records) { 46 + const v = r.value as any; 47 + if (v.site === pubUri && v.path?.startsWith("/blog/")) { 48 + existingSlugs.add(v.path); 49 + } 50 + } 51 + 52 + for (const r of res.data.records) { 53 + const v = r.value as any; 54 + const rkey = r.uri.split("/").pop()!; 55 + const path = v.path ?? ""; 56 + 57 + if (v.site !== pubUri) continue; 58 + if (path.startsWith("/blog/")) continue; // already has proper path 59 + 60 + const title = v.title ?? ""; 61 + const slug = SLUG_OVERRIDES[title] ?? slugify(title); 62 + const newPath = `/blog/${slug}`; 63 + 64 + if (existingSlugs.has(newPath)) { 65 + console.log(`SKIP ${rkey} "${title}" -> ${newPath} (slug already taken)`); 66 + continue; 67 + } 68 + 69 + const updated = { ...v, path: newPath }; 70 + 71 + await agent.com.atproto.repo.putRecord({ 72 + repo: did, 73 + collection: "site.standard.document", 74 + rkey, 75 + record: updated, 76 + }); 77 + 78 + existingSlugs.add(newPath); 79 + console.log(`FIXED ${rkey} "${title}" -> ${newPath}`); 80 + } 81 + 82 + console.log("\nDone."); 83 + } 84 + 85 + main().catch((err) => { 86 + console.error(err); 87 + process.exit(1); 88 + });
+73
scripts/migrate-to-leaflet.ts
··· 1 + // Delete documents from Offprint publication and re-publish under Leaflet. 2 + // One-time migration script. 3 + 4 + import { resolve } from "node:path"; 5 + import { config } from "dotenv"; 6 + import { AtpAgent } from "@atproto/api"; 7 + 8 + config({ path: resolve(process.cwd(), ".env") }); 9 + 10 + const OFFPRINT_PUB = "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3mi2ykrhap42n"; 11 + const LEAFLET_PUB = "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3md7ylshxzk2y"; 12 + const COLLECTION = "site.standard.document"; 13 + 14 + async function main() { 15 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 16 + const identifier = process.env.ATP_IDENTIFIER; 17 + const password = process.env.ATP_PASSWORD; 18 + 19 + if (!identifier || !password) { 20 + console.error("Set ATP_IDENTIFIER and ATP_PASSWORD in .env"); 21 + process.exit(1); 22 + } 23 + 24 + const agent = new AtpAgent({ service }); 25 + await agent.login({ identifier, password }); 26 + const did = agent.session!.did; 27 + 28 + // List all documents 29 + const records = await agent.com.atproto.repo.listRecords({ 30 + repo: did, 31 + collection: COLLECTION, 32 + limit: 100, 33 + }); 34 + 35 + const offprintDocs = records.data.records.filter( 36 + (r) => (r.value as any).site === OFFPRINT_PUB && (r.value as any).path?.startsWith("/blog/") 37 + ); 38 + 39 + console.log(`Found ${offprintDocs.length} blog documents to migrate.\n`); 40 + 41 + for (const doc of offprintDocs) { 42 + const rkey = doc.uri.split("/").pop()!; 43 + const val = doc.value as any; 44 + const title = val.title; 45 + 46 + // Delete the old record 47 + console.log(`Deleting ${title} (${rkey})...`); 48 + await agent.com.atproto.repo.deleteRecord({ 49 + repo: did, 50 + collection: COLLECTION, 51 + rkey, 52 + }); 53 + 54 + // Re-create under Leaflet publication (same rkey to keep it simple) 55 + const newRecord = { ...val, site: LEAFLET_PUB }; 56 + console.log(`Re-creating under Leaflet publication...`); 57 + await agent.com.atproto.repo.createRecord({ 58 + repo: did, 59 + collection: COLLECTION, 60 + rkey, 61 + record: newRecord, 62 + }); 63 + 64 + console.log(` Done: ${title}\n`); 65 + } 66 + 67 + console.log("Migration complete."); 68 + } 69 + 70 + main().catch((err) => { 71 + console.error(err); 72 + process.exit(1); 73 + });
+49
scripts/publish-about.ts
··· 1 + // Publish the about page as a stream.cameron.about record. 2 + // Usage: npx tsx scripts/publish-about.ts 3 + 4 + import { readFileSync } from "node:fs"; 5 + import { resolve } from "node:path"; 6 + import { config } from "dotenv"; 7 + import { AtpAgent } from "@atproto/api"; 8 + 9 + config({ path: resolve(process.cwd(), ".env") }); 10 + 11 + async function main() { 12 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 13 + const identifier = process.env.ATP_IDENTIFIER!; 14 + const password = process.env.ATP_PASSWORD!; 15 + 16 + const agent = new AtpAgent({ service }); 17 + await agent.login({ identifier, password }); 18 + const did = agent.session!.did; 19 + 20 + const rawFile = readFileSync( 21 + resolve(process.env.HOME!, "letta/cameron-blog/about.md"), 22 + "utf-8" 23 + ); 24 + 25 + // Strip Franklin frontmatter 26 + const content = rawFile.replace(/^\+\+\+\n[\s\S]*?\n\+\+\+\n/, "").trim(); 27 + 28 + const record = { 29 + $type: "stream.cameron.about", 30 + content, 31 + updatedAt: new Date().toISOString(), 32 + }; 33 + 34 + // Use "self" as rkey — singleton record 35 + const result = await agent.com.atproto.repo.putRecord({ 36 + repo: did, 37 + collection: "stream.cameron.about", 38 + rkey: "self", 39 + record, 40 + }); 41 + 42 + console.log(`Published: ${result.data.uri}`); 43 + console.log(`View: https://pdsls.dev/${result.data.uri}`); 44 + } 45 + 46 + main().catch((err) => { 47 + console.error(err); 48 + process.exit(1); 49 + });
+204
scripts/publish-all.ts
··· 1 + // Batch publish all markdown blog posts that aren't already on the PDS. 2 + // Usage: npx tsx scripts/publish-all.ts <blog-dir> 3 + 4 + import { readdirSync, readFileSync } from "node:fs"; 5 + import { basename, join, resolve } from "node:path"; 6 + import { config } from "dotenv"; 7 + import matter from "gray-matter"; 8 + import { AtpAgent } from "@atproto/api"; 9 + import { TID } from "@atproto/common-web"; 10 + 11 + config({ path: resolve(process.cwd(), ".env") }); 12 + 13 + const COLLECTION = "site.standard.document"; 14 + 15 + // Parse Franklin-style Date(year, month, day) into a JS Date. 16 + function parseFranklinDate(val: string): Date | null { 17 + const m = val.match(/Date\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/); 18 + if (!m) return null; 19 + return new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3])); 20 + } 21 + 22 + function parseFranklinFrontmatter(raw: string): { 23 + data: Record<string, unknown>; 24 + content: string; 25 + } { 26 + const match = raw.match(/^\+\+\+\n([\s\S]*?)\n\+\+\+\n([\s\S]*)$/); 27 + if (!match) return { data: {}, content: raw }; 28 + 29 + const tomlBlock = match[1]; 30 + const content = match[2]; 31 + const data: Record<string, unknown> = {}; 32 + 33 + for (const line of tomlBlock.split("\n")) { 34 + const eqIdx = line.indexOf("="); 35 + if (eqIdx < 0) continue; 36 + const key = line.slice(0, eqIdx).trim(); 37 + let val = line.slice(eqIdx + 1).trim(); 38 + 39 + if ( 40 + (val.startsWith('"') && val.endsWith('"')) || 41 + (val.startsWith("'") && val.endsWith("'")) 42 + ) { 43 + val = val.slice(1, -1); 44 + } 45 + 46 + const dateVal = parseFranklinDate(val); 47 + if (dateVal) { 48 + data[key] = dateVal; 49 + continue; 50 + } 51 + 52 + if (val === "true") { data[key] = true; continue; } 53 + if (val === "false") { data[key] = false; continue; } 54 + 55 + // Parse arrays like ["tag1", "tag2"] 56 + if (val.startsWith("[") && val.endsWith("]")) { 57 + try { 58 + data[key] = JSON.parse(val); 59 + continue; 60 + } catch {} 61 + } 62 + 63 + data[key] = val; 64 + } 65 + 66 + return { data, content }; 67 + } 68 + 69 + function stripMarkdown(md: string): string { 70 + return md 71 + .replace(/^#{1,6}\s+/gm, "") 72 + .replace(/\*\*([^*]+)\*\*/g, "$1") 73 + .replace(/\*([^*]+)\*/g, "$1") 74 + .replace(/`{1,3}[^`]*`{1,3}/g, "") 75 + .replace(/```[\s\S]*?```/g, "") 76 + .replace(/~~~[\s\S]*?~~~/g, "") 77 + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") 78 + .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") 79 + .replace(/^\s*[-*+]\s+/gm, "") 80 + .replace(/^\s*>\s+/gm, "") 81 + .replace(/\n{3,}/g, "\n\n") 82 + .trim(); 83 + } 84 + 85 + async function main() { 86 + const blogDir = process.argv[2]; 87 + if (!blogDir) { 88 + console.error("Usage: npx tsx scripts/publish-all.ts <blog-dir>"); 89 + process.exit(1); 90 + } 91 + 92 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 93 + const identifier = process.env.ATP_IDENTIFIER!; 94 + const password = process.env.ATP_PASSWORD!; 95 + const publicationUri = process.env.PUBLICATION_URI!; 96 + 97 + if (!identifier || !password || !publicationUri) { 98 + console.error("Set ATP_IDENTIFIER, ATP_PASSWORD, and PUBLICATION_URI in .env"); 99 + process.exit(1); 100 + } 101 + 102 + const agent = new AtpAgent({ service }); 103 + await agent.login({ identifier, password }); 104 + const did = agent.session!.did; 105 + 106 + // Fetch existing documents to skip duplicates 107 + const existing = await agent.com.atproto.repo.listRecords({ 108 + repo: did, 109 + collection: COLLECTION, 110 + limit: 100, 111 + }); 112 + 113 + const publishedPaths = new Set<string>(); 114 + const publishedTitles = new Set<string>(); 115 + for (const r of existing.data.records) { 116 + const v = r.value as Record<string, unknown>; 117 + if (v.site === publicationUri) { 118 + if (v.path) publishedPaths.add(v.path as string); 119 + if (v.title) publishedTitles.add((v.title as string).toLowerCase()); 120 + } 121 + } 122 + 123 + // Find all markdown files 124 + const files = readdirSync(resolve(blogDir)) 125 + .filter((f) => f.endsWith(".md")) 126 + .sort(); 127 + 128 + let published = 0; 129 + let skipped = 0; 130 + 131 + for (const file of files) { 132 + const slug = basename(file, ".md").replace(/[^a-z0-9-]/gi, "-").toLowerCase(); 133 + const path = `/blog/${slug}`; 134 + 135 + const rawFile = readFileSync(join(resolve(blogDir), file), "utf-8"); 136 + const isFranklin = rawFile.startsWith("+++\n"); 137 + const { data: front, content: body } = isFranklin 138 + ? parseFranklinFrontmatter(rawFile) 139 + : matter(rawFile); 140 + 141 + const title = front.title as string; 142 + if (!title) { 143 + console.log(`SKIP ${file}: no title`); 144 + skipped++; 145 + continue; 146 + } 147 + 148 + // Skip if already published (by path or title) 149 + if (publishedPaths.has(path) || publishedTitles.has(title.toLowerCase())) { 150 + console.log(`SKIP ${file}: already published`); 151 + skipped++; 152 + continue; 153 + } 154 + 155 + const publishedAt = 156 + front.date instanceof Date 157 + ? front.date.toISOString() 158 + : front.date 159 + ? new Date(front.date as string).toISOString() 160 + : new Date().toISOString(); 161 + 162 + const description = (front.summary ?? front.rss ?? "") as string; 163 + 164 + const record: Record<string, unknown> = { 165 + $type: COLLECTION, 166 + site: publicationUri, 167 + title, 168 + path, 169 + publishedAt, 170 + content: { 171 + $type: "site.standard.document#markdown", 172 + value: body.trim(), 173 + }, 174 + textContent: stripMarkdown(body).slice(0, 100000), 175 + }; 176 + if (description) record.description = description; 177 + if (front.tags) record.tags = front.tags; 178 + 179 + const rkey = TID.nextStr(); 180 + 181 + try { 182 + await agent.com.atproto.repo.createRecord({ 183 + repo: did, 184 + collection: COLLECTION, 185 + rkey, 186 + record, 187 + }); 188 + console.log(`OK ${file} -> ${COLLECTION}/${rkey} (${path})`); 189 + published++; 190 + } catch (err: any) { 191 + console.error(`FAIL ${file}: ${err.message}`); 192 + } 193 + 194 + // Small delay to avoid rate limiting 195 + await new Promise((r) => setTimeout(r, 200)); 196 + } 197 + 198 + console.log(`\nDone. Published: ${published}, Skipped: ${skipped}`); 199 + } 200 + 201 + main().catch((err) => { 202 + console.error(err); 203 + process.exit(1); 204 + });
+180
scripts/publish.ts
··· 1 + // Publish a markdown blog post as a site.standard.document record. 2 + // 3 + // Usage: npx tsx scripts/publish.ts <markdown-file> [--slug my-slug] 4 + // 5 + // Supports both YAML (---) and Franklin TOML (+++) frontmatter. 6 + // Franklin dates like Date(2025,07,08) are parsed automatically. 7 + // 8 + // Requires PUBLICATION_URI in .env (the at:// URI of your site.standard.publication). 9 + 10 + import { readFileSync } from "node:fs"; 11 + import { basename, resolve } from "node:path"; 12 + import { config } from "dotenv"; 13 + import matter from "gray-matter"; 14 + import { AtpAgent } from "@atproto/api"; 15 + import { TID } from "@atproto/common-web"; 16 + 17 + config({ path: resolve(process.cwd(), ".env") }); 18 + 19 + const COLLECTION = "site.standard.document"; 20 + const SITE_URL = "https://cameron.stream"; 21 + 22 + // Parse Franklin-style Date(year, month, day) into a JS Date. 23 + function parseFranklinDate(val: string): Date | null { 24 + const m = val.match(/Date\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/); 25 + if (!m) return null; 26 + // Franklin months are 1-indexed, JS Date months are 0-indexed 27 + return new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3])); 28 + } 29 + 30 + // Parse Franklin TOML frontmatter (lines between +++ delimiters). 31 + function parseFranklinFrontmatter(raw: string): { 32 + data: Record<string, unknown>; 33 + content: string; 34 + } { 35 + const match = raw.match(/^\+\+\+\n([\s\S]*?)\n\+\+\+\n([\s\S]*)$/); 36 + if (!match) return { data: {}, content: raw }; 37 + 38 + const tomlBlock = match[1]; 39 + const content = match[2]; 40 + const data: Record<string, unknown> = {}; 41 + 42 + for (const line of tomlBlock.split("\n")) { 43 + const eqIdx = line.indexOf("="); 44 + if (eqIdx < 0) continue; 45 + const key = line.slice(0, eqIdx).trim(); 46 + let val = line.slice(eqIdx + 1).trim(); 47 + 48 + // Strip surrounding quotes 49 + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { 50 + val = val.slice(1, -1); 51 + } 52 + 53 + // Parse Date(...) 54 + const dateVal = parseFranklinDate(val); 55 + if (dateVal) { 56 + data[key] = dateVal; 57 + continue; 58 + } 59 + 60 + // Parse booleans 61 + if (val === "true") { data[key] = true; continue; } 62 + if (val === "false") { data[key] = false; continue; } 63 + 64 + data[key] = val; 65 + } 66 + 67 + return { data, content }; 68 + } 69 + 70 + // Strip markdown to plaintext (rough approximation). 71 + function stripMarkdown(md: string): string { 72 + return md 73 + .replace(/^#{1,6}\s+/gm, "") // headings 74 + .replace(/\*\*([^*]+)\*\*/g, "$1") // bold 75 + .replace(/\*([^*]+)\*/g, "$1") // italic 76 + .replace(/`{1,3}[^`]*`{1,3}/g, "") // inline code 77 + .replace(/```[\s\S]*?```/g, "") // code blocks 78 + .replace(/~~~[\s\S]*?~~~/g, "") // code blocks alt 79 + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links 80 + .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") // images 81 + .replace(/^\s*[-*+]\s+/gm, "") // list items 82 + .replace(/^\s*>\s+/gm, "") // blockquotes 83 + .replace(/\n{3,}/g, "\n\n") // collapse whitespace 84 + .trim(); 85 + } 86 + 87 + async function main() { 88 + const args = process.argv.slice(2); 89 + const filePath = args.find((a) => !a.startsWith("--")); 90 + const slugFlag = args.indexOf("--slug"); 91 + const slugOverride = slugFlag >= 0 ? args[slugFlag + 1] : undefined; 92 + 93 + if (!filePath) { 94 + console.error("Usage: npx tsx scripts/publish.ts <file.md> [--slug slug]"); 95 + process.exit(1); 96 + } 97 + 98 + const rawFile = readFileSync(resolve(filePath), "utf-8"); 99 + 100 + // Detect frontmatter format 101 + const isFranklin = rawFile.startsWith("+++\n"); 102 + const { data: front, content: body } = isFranklin 103 + ? parseFranklinFrontmatter(rawFile) 104 + : matter(rawFile); 105 + 106 + const title = front.title as string; 107 + if (!title) { 108 + console.error("Missing 'title' in frontmatter"); 109 + process.exit(1); 110 + } 111 + 112 + // Derive slug from filename or override 113 + const slug = 114 + slugOverride ?? basename(filePath, ".md").replace(/[^a-z0-9-]/gi, "-").toLowerCase(); 115 + 116 + const publishedAt = 117 + front.date instanceof Date 118 + ? front.date.toISOString() 119 + : front.date 120 + ? new Date(front.date as string).toISOString() 121 + : new Date().toISOString(); 122 + 123 + // Connect to PDS 124 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 125 + const identifier = process.env.ATP_IDENTIFIER; 126 + const password = process.env.ATP_PASSWORD; 127 + const publicationUri = process.env.PUBLICATION_URI; 128 + 129 + if (!identifier || !password) { 130 + console.error("Set ATP_IDENTIFIER and ATP_PASSWORD in .env"); 131 + process.exit(1); 132 + } 133 + 134 + if (!publicationUri) { 135 + console.error("Set PUBLICATION_URI in .env (run scripts/setup-publication.ts first)"); 136 + process.exit(1); 137 + } 138 + 139 + const agent = new AtpAgent({ service }); 140 + await agent.login({ identifier, password }); 141 + 142 + // Build description from summary/rss frontmatter 143 + const description = (front.summary ?? front.rss ?? "") as string; 144 + 145 + // Build the site.standard.document record 146 + const record: Record<string, unknown> = { 147 + $type: COLLECTION, 148 + site: publicationUri, 149 + title, 150 + path: `/blog/${slug}`, 151 + publishedAt, 152 + content: { 153 + $type: "site.standard.document#markdown", 154 + value: body.trim(), 155 + }, 156 + textContent: stripMarkdown(body).slice(0, 100000), 157 + }; 158 + if (description) record.description = description; 159 + if (front.tags) record.tags = front.tags; 160 + 161 + // Use TID for the rkey (standard.site requires tid keys) 162 + const rkey = TID.nextStr(); 163 + 164 + console.log(`Publishing "${title}" as ${COLLECTION}/${rkey} (path: /blog/${slug})...`); 165 + 166 + const result = await agent.com.atproto.repo.createRecord({ 167 + repo: agent.session!.did, 168 + collection: COLLECTION, 169 + rkey, 170 + record, 171 + }); 172 + 173 + console.log(`Published: ${result.data.uri}`); 174 + console.log(`View: https://pdsls.dev/${result.data.uri}`); 175 + } 176 + 177 + main().catch((err) => { 178 + console.error(err); 179 + process.exit(1); 180 + });
+93
scripts/rekey-records.ts
··· 1 + // Rekey all site.standard.document records so the rkey matches the slug. 2 + // This makes Leaflet URLs work: cameron.leaflet.pub/<slug> 3 + // 4 + // For each record where rkey !== slug: 5 + // 1. Create new record with rkey = slug (same value) 6 + // 2. Delete old record with TID rkey 7 + 8 + import "dotenv/config"; 9 + import { AtpAgent } from "@atproto/api"; 10 + 11 + const COLLECTION = "site.standard.document"; 12 + const PUBLICATION_URI = 13 + process.env.PUBLICATION_URI || 14 + "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3md7ylshxzk2y"; 15 + 16 + const agent = new AtpAgent({ service: process.env.ATP_SERVICE || "https://bsky.social" }); 17 + 18 + async function main() { 19 + await agent.login({ 20 + identifier: process.env.ATP_IDENTIFIER!, 21 + password: process.env.ATP_PASSWORD!, 22 + }); 23 + 24 + const did = agent.session!.did; 25 + console.log(`Logged in as ${did}\n`); 26 + 27 + // List all records 28 + let cursor: string | undefined; 29 + const records: Array<{ uri: string; cid: string; value: Record<string, any> }> = []; 30 + do { 31 + const res = await agent.api.com.atproto.repo.listRecords({ 32 + repo: did, 33 + collection: COLLECTION, 34 + limit: 100, 35 + cursor, 36 + }); 37 + records.push(...(res.data.records as any[])); 38 + cursor = res.data.cursor; 39 + } while (cursor); 40 + 41 + // Filter to our publication 42 + const ours = records.filter((r) => r.value.site === PUBLICATION_URI); 43 + console.log(`Found ${ours.length} records in publication\n`); 44 + 45 + let changed = 0; 46 + let skipped = 0; 47 + 48 + for (const record of ours) { 49 + const oldRkey = record.uri.split("/").pop()!; 50 + const path: string = record.value.path || ""; 51 + const slug = path.split("/").filter(Boolean).pop() || ""; 52 + 53 + if (!slug) { 54 + console.log(`SKIP (no slug): ${record.value.title}`); 55 + skipped++; 56 + continue; 57 + } 58 + 59 + if (oldRkey === slug) { 60 + console.log(`OK: ${slug} — ${record.value.title}`); 61 + skipped++; 62 + continue; 63 + } 64 + 65 + console.log(`REKEY: ${oldRkey} → ${slug} — ${record.value.title}`); 66 + 67 + // Create new record with slug as rkey 68 + await agent.api.com.atproto.repo.putRecord({ 69 + repo: did, 70 + collection: COLLECTION, 71 + rkey: slug, 72 + record: record.value, 73 + }); 74 + 75 + // Delete old record 76 + await agent.api.com.atproto.repo.deleteRecord({ 77 + repo: did, 78 + collection: COLLECTION, 79 + rkey: oldRkey, 80 + }); 81 + 82 + changed++; 83 + // Small delay to be kind to the PDS 84 + await new Promise((r) => setTimeout(r, 200)); 85 + } 86 + 87 + console.log(`\nDone. Changed: ${changed}, Skipped: ${skipped}`); 88 + } 89 + 90 + main().catch((e) => { 91 + console.error(e); 92 + process.exit(1); 93 + });
+69
scripts/setup-publication.ts
··· 1 + // Create or update the site.standard.publication record. 2 + // Run once: npx tsx scripts/setup-publication.ts 3 + // Outputs the at:// URI to add to .env as PUBLICATION_URI. 4 + 5 + import { resolve } from "node:path"; 6 + import { config } from "dotenv"; 7 + import { AtpAgent } from "@atproto/api"; 8 + import { TID } from "@atproto/common-web"; 9 + 10 + config({ path: resolve(process.cwd(), ".env") }); 11 + 12 + const COLLECTION = "site.standard.publication"; 13 + 14 + async function main() { 15 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 16 + const identifier = process.env.ATP_IDENTIFIER; 17 + const password = process.env.ATP_PASSWORD; 18 + 19 + if (!identifier || !password) { 20 + console.error("Set ATP_IDENTIFIER and ATP_PASSWORD in .env"); 21 + process.exit(1); 22 + } 23 + 24 + const agent = new AtpAgent({ service }); 25 + await agent.login({ identifier, password }); 26 + 27 + // Check if a publication record already exists 28 + const existing = await agent.com.atproto.repo.listRecords({ 29 + repo: agent.session!.did, 30 + collection: COLLECTION, 31 + limit: 1, 32 + }); 33 + 34 + if (existing.data.records.length > 0) { 35 + const uri = existing.data.records[0].uri; 36 + console.log("Publication already exists:"); 37 + console.log(`PUBLICATION_URI=${uri}`); 38 + console.log(`View: https://pdsls.dev/${uri}`); 39 + return; 40 + } 41 + 42 + const rkey = TID.nextStr(); 43 + 44 + const record = { 45 + $type: COLLECTION, 46 + url: "https://cameron.stream", 47 + name: "cameron.stream", 48 + description: "Cameron Pfiffer's blog. AI systems, ATProto, Bayesian statistics, and whatever else.", 49 + }; 50 + 51 + console.log("Creating publication record..."); 52 + 53 + const result = await agent.com.atproto.repo.createRecord({ 54 + repo: agent.session!.did, 55 + collection: COLLECTION, 56 + rkey, 57 + record, 58 + }); 59 + 60 + console.log(`Created: ${result.data.uri}`); 61 + console.log(`\nAdd to .env:`); 62 + console.log(`PUBLICATION_URI=${result.data.uri}`); 63 + console.log(`\nView: https://pdsls.dev/${result.data.uri}`); 64 + } 65 + 66 + main().catch((err) => { 67 + console.error(err); 68 + process.exit(1); 69 + });
+90
scripts/upload-images.ts
··· 1 + // Upload blog images as blobs to the PDS and output a JSON mapping 2 + // of old paths -> CDN URLs for render-time rewriting. 3 + // 4 + // Usage: npx tsx scripts/upload-images.ts <images-dir> 5 + 6 + import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs"; 7 + import { join, relative, resolve } from "node:path"; 8 + import { config } from "dotenv"; 9 + import { AtpAgent } from "@atproto/api"; 10 + 11 + config({ path: resolve(process.cwd(), ".env") }); 12 + 13 + const MIME_TYPES: Record<string, string> = { 14 + ".jpg": "image/jpeg", 15 + ".jpeg": "image/jpeg", 16 + ".png": "image/png", 17 + ".webp": "image/webp", 18 + ".gif": "image/gif", 19 + ".svg": "image/svg+xml", 20 + }; 21 + 22 + function walkDir(dir: string): string[] { 23 + const files: string[] = []; 24 + for (const entry of readdirSync(dir)) { 25 + const full = join(dir, entry); 26 + if (statSync(full).isDirectory()) { 27 + files.push(...walkDir(full)); 28 + } else { 29 + const ext = full.slice(full.lastIndexOf(".")).toLowerCase(); 30 + if (MIME_TYPES[ext]) files.push(full); 31 + } 32 + } 33 + return files; 34 + } 35 + 36 + async function main() { 37 + const imagesDir = process.argv[2]; 38 + if (!imagesDir) { 39 + console.error("Usage: npx tsx scripts/upload-images.ts <images-dir>"); 40 + process.exit(1); 41 + } 42 + 43 + const service = process.env.ATP_SERVICE || "https://bsky.social"; 44 + const identifier = process.env.ATP_IDENTIFIER!; 45 + const password = process.env.ATP_PASSWORD!; 46 + 47 + const agent = new AtpAgent({ service }); 48 + await agent.login({ identifier, password }); 49 + const did = agent.session!.did; 50 + 51 + const absDir = resolve(imagesDir); 52 + const files = walkDir(absDir); 53 + 54 + console.log(`Found ${files.length} images to upload.\n`); 55 + 56 + const mapping: Record<string, string> = {}; 57 + 58 + for (const file of files) { 59 + const rel = relative(absDir, file); 60 + const oldPath = `/assets/images/${rel}`; 61 + const ext = file.slice(file.lastIndexOf(".")).toLowerCase(); 62 + const mimeType = MIME_TYPES[ext] || "application/octet-stream"; 63 + 64 + const data = readFileSync(file); 65 + 66 + try { 67 + const result = await agent.uploadBlob(new Uint8Array(data), { 68 + encoding: mimeType, 69 + }); 70 + const cid = result.data.blob.ref.toString(); 71 + const cdnUrl = `https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}@jpeg`; 72 + mapping[oldPath] = cdnUrl; 73 + console.log(`OK ${rel} -> ${cid}`); 74 + } catch (err: any) { 75 + console.error(`FAIL ${rel}: ${err.message}`); 76 + } 77 + 78 + await new Promise((r) => setTimeout(r, 100)); 79 + } 80 + 81 + // Write mapping to a JSON file 82 + const outPath = resolve(process.cwd(), "src/image-map.json"); 83 + writeFileSync(outPath, JSON.stringify(mapping, null, 2)); 84 + console.log(`\nMapping written to ${outPath}`); 85 + } 86 + 87 + main().catch((err) => { 88 + console.error(err); 89 + process.exit(1); 90 + });
+50
src/cache.ts
··· 1 + // Simple in-memory cache. Swap for Redis later if needed. 2 + 3 + const LIFE_TTL: Record<string, number> = { 4 + seconds: 30, 5 + minutes: 5 * 60, 6 + hours: 3600, 7 + max: 86400, 8 + }; 9 + 10 + type CacheEntry = { 11 + value: unknown; 12 + expires: number; 13 + }; 14 + 15 + const store = new Map<string, CacheEntry>(); 16 + 17 + type CacheTag = { 18 + $type: string; 19 + uri?: string; 20 + subject?: string; 21 + from?: string; 22 + }; 23 + 24 + type CachePolicy = { 25 + life?: string; 26 + tags?: CacheTag[]; 27 + }; 28 + 29 + export async function cacheGet(key: string): Promise<unknown | undefined> { 30 + const entry = store.get(key); 31 + if (!entry) return undefined; 32 + if (Date.now() > entry.expires) { 33 + store.delete(key); 34 + return undefined; 35 + } 36 + return entry.value; 37 + } 38 + 39 + export async function cacheSet( 40 + key: string, 41 + value: unknown, 42 + policy?: CachePolicy 43 + ): Promise<void> { 44 + const life = policy?.life ?? "hours"; 45 + const ttl = LIFE_TTL[life] ?? 3600; 46 + store.set(key, { 47 + value, 48 + expires: Date.now() + ttl * 1000, 49 + }); 50 + }
+60
src/components/about.tsx
··· 1 + // About page component — renders stream.cameron.about record. 2 + 3 + import { raw } from "hono/html"; 4 + import { getAbout, getProfile } from "../data.ts"; 5 + import { renderMarkdown } from "../markdown.ts"; 6 + 7 + export async function About() { 8 + const [content, profile] = await Promise.all([ 9 + getAbout(), 10 + getProfile(), 11 + ]); 12 + 13 + const avatarHtml = profile?.avatar 14 + ? `<a href="/"><img src="${profile.avatar}" alt="${profile.displayName ?? ""}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;vertical-align:middle;" /></a>` 15 + : ""; 16 + 17 + if (!content) { 18 + return ( 19 + <div> 20 + <nav class="site-nav"> 21 + {raw(avatarHtml)} 22 + <a href="/blog">Blog</a> 23 + <a href="/about" style="color:var(--site-text);font-weight:600">About</a> 24 + <a href="/endorsements">Endorsements</a> 25 + <a href="/annotations">Annotations</a> 26 + <a href="/semble">Semble</a> 27 + </nav> 28 + <p style="color:var(--site-text-secondary)">No about page found.</p> 29 + </div> 30 + ); 31 + } 32 + 33 + const html = await renderMarkdown(content); 34 + 35 + return ( 36 + <div> 37 + <nav class="site-nav"> 38 + {raw(avatarHtml)} 39 + <a href="/blog">Blog</a> 40 + <a href="/about" style="color:var(--site-text);font-weight:600">About</a> 41 + <a href="/endorsements">Endorsements</a> 42 + <a href="/annotations">Annotations</a> 43 + <a href="/semble">Semble</a> 44 + </nav> 45 + <div class="blog-content" style="margin-top:1em"> 46 + {raw(html)} 47 + </div> 48 + <footer style="margin-top:3em;padding-top:1em;border-top:1px solid var(--site-border);font-size:0.8125rem;color:var(--site-text-muted)"> 49 + <a 50 + href="https://pdsls.dev/at://did:plc:gfrmhdmjvxn2sjedzboeudef/stream.cameron.about/self" 51 + target="_blank" 52 + rel="noopener" 53 + style="color:var(--site-accent);text-decoration:none" 54 + > 55 + View record on PDS 56 + </a> 57 + </footer> 58 + </div> 59 + ); 60 + }
+164
src/components/annotations.tsx
··· 1 + // Annotations page — renders at.margin.annotation records from Cameron's PDS. 2 + 3 + import { raw } from "hono/html"; 4 + import { getProfile } from "../data.ts"; 5 + 6 + interface Annotation { 7 + uri: string; 8 + rkey: string; 9 + target: { source: string; selector?: { exact?: string } }; 10 + body?: { value?: string }; 11 + createdAt?: string; 12 + } 13 + 14 + async function listAnnotations(): Promise<Annotation[]> { 15 + const did = process.env.CAMERON_DID || "did:plc:gfrmhdmjvxn2sjedzboeudef"; 16 + const pds = "https://bsky.social"; 17 + let cursor: string | undefined; 18 + const all: Annotation[] = []; 19 + 20 + do { 21 + const url = new URL(`${pds}/xrpc/com.atproto.repo.listRecords`); 22 + url.searchParams.set("repo", did); 23 + url.searchParams.set("collection", "at.margin.annotation"); 24 + url.searchParams.set("limit", "100"); 25 + if (cursor) url.searchParams.set("cursor", cursor); 26 + 27 + const res = await fetch(url.toString()); 28 + if (!res.ok) break; 29 + const data = await res.json(); 30 + 31 + for (const r of data.records ?? []) { 32 + const rkey = r.uri.split("/").pop()!; 33 + all.push({ 34 + uri: r.uri, 35 + rkey, 36 + target: r.value.target ?? {}, 37 + body: r.value.body, 38 + createdAt: r.value.createdAt ?? r.value.created, 39 + }); 40 + } 41 + cursor = data.cursor; 42 + } while (cursor); 43 + 44 + // Sort newest first 45 + return all.sort( 46 + (a, b) => 47 + new Date(b.createdAt ?? 0).getTime() - 48 + new Date(a.createdAt ?? 0).getTime() 49 + ); 50 + } 51 + 52 + function escapeHtml(str: string): string { 53 + return str 54 + .replace(/&/g, "&amp;") 55 + .replace(/</g, "&lt;") 56 + .replace(/>/g, "&gt;") 57 + .replace(/"/g, "&quot;"); 58 + } 59 + 60 + function displayUrl(url: string): string { 61 + try { 62 + const u = new URL(url); 63 + return u.hostname + (u.pathname === "/" ? "" : u.pathname); 64 + } catch { 65 + return url; 66 + } 67 + } 68 + 69 + export async function Annotations() { 70 + const [annotations, profile] = await Promise.all([ 71 + listAnnotations(), 72 + getProfile(), 73 + ]); 74 + 75 + const avatarHtml = profile?.avatar 76 + ? `<a href="/" class="nav-avatar"><img src="${profile.avatar}" alt="${profile.displayName ?? ""}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;vertical-align:middle;" /></a>` 77 + : ""; 78 + 79 + return ( 80 + <div> 81 + <nav class="site-nav"> 82 + {raw(avatarHtml)} 83 + <a href="/blog">Blog</a> 84 + <a href="/about">About</a> 85 + <a href="/endorsements">Endorsements</a> 86 + <a href="/annotations" style="color:var(--site-text);font-weight:600"> 87 + Annotations 88 + </a> 89 + <a href="/semble">Semble</a> 90 + </nav> 91 + 92 + <h1 style="font-size:1.75rem;font-weight:700;margin:1em 0 0.25em"> 93 + Annotations 94 + </h1> 95 + <p style="color:var(--site-text-secondary);font-size:0.9rem;margin-bottom:1.5em;line-height:1.5"> 96 + Web annotations via{" "} 97 + <a 98 + href="https://margin.at" 99 + target="_blank" 100 + rel="noopener" 101 + style="color:var(--site-accent);text-decoration:none" 102 + > 103 + Margin 104 + </a> 105 + , stored as{" "} 106 + <a 107 + href="https://margin.at/lexicon" 108 + target="_blank" 109 + rel="noopener" 110 + style="color:var(--site-accent);text-decoration:none" 111 + > 112 + at.margin.annotation 113 + </a>{" "} 114 + records on my PDS. 115 + </p> 116 + 117 + <div class="annotations-list"> 118 + {annotations.map((a) => { 119 + const date = a.createdAt 120 + ? new Date(a.createdAt).toLocaleDateString("en-US", { 121 + month: "short", 122 + day: "numeric", 123 + year: "numeric", 124 + }) 125 + : ""; 126 + 127 + return ( 128 + <div class="annotation-item" key={a.rkey}> 129 + <div class="annotation-target"> 130 + <a 131 + href={a.target.source} 132 + target="_blank" 133 + rel="noopener" 134 + style="color:var(--site-accent);text-decoration:none;font-size:0.8125rem" 135 + > 136 + {escapeHtml(displayUrl(a.target.source))} 137 + </a> 138 + </div> 139 + {a.target.selector?.exact && ( 140 + <blockquote class="annotation-quote"> 141 + {escapeHtml(a.target.selector.exact)} 142 + </blockquote> 143 + )} 144 + {a.body?.value && ( 145 + <p class="annotation-body">{escapeHtml(a.body.value)}</p> 146 + )} 147 + <div class="annotation-meta"> 148 + {date && <span>{date}</span>} 149 + <a 150 + href={`https://pdsls.dev/${a.uri}`} 151 + target="_blank" 152 + rel="noopener" 153 + style="color:var(--site-text-muted);text-decoration:none;font-size:0.7rem" 154 + > 155 + record 156 + </a> 157 + </div> 158 + </div> 159 + ); 160 + })} 161 + </div> 162 + </div> 163 + ); 164 + }
+71
src/components/blog-index.tsx
··· 1 + // stream.cameron.BlogIndex — Lists all blog posts. 2 + 3 + import { raw } from "hono/html"; 4 + import { listBlogPosts, getProfile } from "../data.ts"; 5 + 6 + export async function BlogIndex() { 7 + const [posts, profile] = await Promise.all([ 8 + listBlogPosts(), 9 + getProfile(), 10 + ]); 11 + 12 + const avatarHtml = profile?.avatar 13 + ? `<a href="/"><img src="${profile.avatar}" alt="${profile.displayName ?? ""}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;vertical-align:middle;" /></a>` 14 + : ""; 15 + 16 + const postsHtml = posts 17 + .map((post) => { 18 + const date = new Date(post.publishedAt).toLocaleDateString("en-US", { 19 + year: "numeric", 20 + month: "short", 21 + day: "numeric", 22 + }); 23 + const tags = (post.tags ?? []) 24 + .map( 25 + (t) => 26 + `<span style="font-size:0.75rem;color:var(--site-text-muted);background:var(--site-code-bg);padding:0.1em 0.5em;border-radius:3px">${escapeHtml(t)}</span>` 27 + ) 28 + .join(" "); 29 + 30 + return `<div class="post-item"> 31 + <div class="post-date">${date}</div> 32 + <a href="/${post.slug}" class="post-title">${escapeHtml(post.title)}</a> 33 + ${post.description ? `<div class="post-summary">${escapeHtml(post.description)}</div>` : ""} 34 + ${tags ? `<div style="margin-top:0.4em;display:flex;gap:0.4em;flex-wrap:wrap">${tags}</div>` : ""} 35 + </div>`; 36 + }) 37 + .join(""); 38 + 39 + return ( 40 + <div> 41 + <nav class="site-nav"> 42 + {raw(avatarHtml)} 43 + <a href="/blog" style="color:var(--site-text);font-weight:600"> 44 + Blog 45 + </a> 46 + <a href="/about">About</a> 47 + <a href="/endorsements">Endorsements</a> 48 + <a href="/annotations">Annotations</a> 49 + <a href="/semble">Semble</a> 50 + </nav> 51 + 52 + <h1 style="font-size:1.5rem;font-weight:700;margin:1em 0 0.5em"> 53 + Blog 54 + </h1> 55 + 56 + {posts.length === 0 ? ( 57 + <p style="color:var(--site-text-secondary)">No posts yet.</p> 58 + ) : ( 59 + raw(postsHtml) 60 + )} 61 + </div> 62 + ); 63 + } 64 + 65 + function escapeHtml(str: string): string { 66 + return str 67 + .replace(/&/g, "&amp;") 68 + .replace(/</g, "&lt;") 69 + .replace(/>/g, "&gt;") 70 + .replace(/"/g, "&quot;"); 71 + }
+101
src/components/blog-post.tsx
··· 1 + // stream.cameron.BlogPost — Renders a single blog post. 2 + 3 + import { raw } from "hono/html"; 4 + import { getBlogPost, getProfile } from "../data.ts"; 5 + import { renderMarkdown } from "../markdown.ts"; 6 + import { MarginPanel } from "./margin.tsx"; 7 + 8 + export async function BlogPost({ slug }: { slug: string }) { 9 + const [post, profile] = await Promise.all([ 10 + getBlogPost(slug), 11 + getProfile(), 12 + ]); 13 + 14 + const avatarHtml = profile?.avatar 15 + ? `<a href="/" class="nav-avatar"><img src="${profile.avatar}" alt="${profile.displayName ?? ""}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;vertical-align:middle;" /></a>` 16 + : ""; 17 + 18 + if (!post) { 19 + return ( 20 + <div> 21 + <nav class="site-nav"> 22 + {raw(avatarHtml)} 23 + <a href="/blog">Blog</a> 24 + <a href="/about">About</a> 25 + <a href="/endorsements">Endorsements</a> 26 + <a href="/annotations">Annotations</a> 27 + <a href="/semble">Semble</a> 28 + </nav> 29 + <h1 style="font-size:1.5rem;font-weight:700;margin:2em 0 0.5em"> 30 + Not found 31 + </h1> 32 + <p style="color:var(--site-text-secondary)"> 33 + Post not found: <code>{slug}</code> 34 + </p> 35 + </div> 36 + ); 37 + } 38 + 39 + const date = new Date(post.publishedAt).toLocaleDateString("en-US", { 40 + year: "numeric", 41 + month: "long", 42 + day: "numeric", 43 + }); 44 + 45 + const html = await renderMarkdown(post.body); 46 + 47 + const atUri = post.uri; 48 + const siteBase = process.env.SITE_URL || "https://cameron.stream"; 49 + const pageUrl = `${siteBase}/${slug}`; 50 + 51 + return ( 52 + <div> 53 + <nav class="site-nav"> 54 + {raw(avatarHtml)} 55 + <a href="/blog">Blog</a> 56 + <a href="/about">About</a> 57 + <a href="/endorsements">Endorsements</a> 58 + <a href="/annotations">Annotations</a> 59 + <a href="/semble">Semble</a> 60 + </nav> 61 + 62 + <article style="margin-top:1em"> 63 + <header> 64 + <h1 style="font-size:1.75rem;font-weight:700;line-height:1.2;margin-bottom:0.25em"> 65 + {post.title} 66 + </h1> 67 + <div style="color:var(--site-text-muted);font-size:0.875rem;margin-bottom:2em"> 68 + {date} 69 + {post.tags && post.tags.length > 0 && ( 70 + <span style="margin-left:1em"> 71 + {post.tags.map((t) => ( 72 + <span 73 + key={t} 74 + style="font-size:0.75rem;color:var(--site-text-muted);background:var(--site-code-bg);padding:0.1em 0.5em;border-radius:3px;margin-left:0.3em" 75 + > 76 + {t} 77 + </span> 78 + ))} 79 + </span> 80 + )} 81 + </div> 82 + </header> 83 + 84 + <div class="blog-content">{raw(html)}</div> 85 + 86 + <MarginPanel pageUrl={pageUrl} /> 87 + 88 + <footer style="margin-top:3em;padding-top:1em;border-top:1px solid var(--site-border);font-size:0.8125rem;color:var(--site-text-muted)"> 89 + <a 90 + href={`https://pdsls.dev/${atUri}`} 91 + target="_blank" 92 + rel="noopener" 93 + style="color:var(--site-accent);text-decoration:none" 94 + > 95 + View record on PDS 96 + </a> 97 + </footer> 98 + </article> 99 + </div> 100 + ); 101 + }
+12
src/components/bluesky.tsx
··· 1 + // Bluesky feed page. 2 + 3 + import { Feed } from "./feed.tsx"; 4 + 5 + export function Bluesky() { 6 + return ( 7 + <div> 8 + <h1 style="font-size:1.5rem;font-weight:700;margin-bottom:0.75em">Bluesky</h1> 9 + <Feed limit={30} /> 10 + </div> 11 + ); 12 + }
+93
src/components/endorsements.tsx
··· 1 + // Endorsements page — renders fund.at.endorse records. 2 + 3 + import { getEndorsements, getProfile } from "../data.ts"; 4 + import { raw } from "hono/html"; 5 + 6 + export async function Endorsements() { 7 + const [endorsements, profile] = await Promise.all([ 8 + getEndorsements(), 9 + getProfile(), 10 + ]); 11 + 12 + const avatarHtml = profile?.avatar 13 + ? `<a href="/" class="nav-avatar"><img src="${profile.avatar}" alt="${profile.displayName ?? ""}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;vertical-align:middle;" /></a>` 14 + : ""; 15 + 16 + return ( 17 + <div> 18 + <nav class="site-nav"> 19 + {raw(avatarHtml)} 20 + <a href="/blog">Blog</a> 21 + <a href="/about">About</a> 22 + <a href="/endorsements" style="color:var(--site-text);font-weight:600"> 23 + Endorsements 24 + </a> 25 + <a href="/annotations">Annotations</a> 26 + <a href="/semble">Semble</a> 27 + </nav> 28 + 29 + <h1 style="font-size:1.75rem;font-weight:700;margin:1em 0 0.25em"> 30 + Endorsements 31 + </h1> 32 + <p style="color:var(--site-text-secondary);font-size:0.9rem;margin-bottom:1.5em;line-height:1.5"> 33 + Projects and people I endorse via{" "} 34 + <a 35 + href="https://at.fund" 36 + target="_blank" 37 + rel="noopener" 38 + style="color:var(--site-accent);text-decoration:none" 39 + > 40 + at.fund 41 + </a> 42 + . These are{" "} 43 + <a 44 + href="https://www.at.fund/lexicon" 45 + target="_blank" 46 + rel="noopener" 47 + style="color:var(--site-accent);text-decoration:none" 48 + > 49 + fund.at.endorse 50 + </a>{" "} 51 + records stored on my PDS. 52 + </p> 53 + 54 + <div class="endorsements-grid"> 55 + {endorsements.map((e) => { 56 + const isHandle = !e.uri.startsWith("did:"); 57 + const displayName = e.uri; 58 + const href = isHandle ? `https://${e.uri}` : `https://bsky.app/profile/${e.uri}`; 59 + const atFundHref = `https://at.fund/${e.uri}`; 60 + const date = new Date(e.createdAt).toLocaleDateString("en-US", { 61 + month: "short", 62 + day: "numeric", 63 + year: "numeric", 64 + }); 65 + 66 + return ( 67 + <div class="endorsement-card" key={e.uri}> 68 + <a 69 + href={href} 70 + target="_blank" 71 + rel="noopener" 72 + class="endorsement-name" 73 + > 74 + {displayName} 75 + </a> 76 + <div class="endorsement-meta"> 77 + <span>{date}</span> 78 + <a 79 + href={atFundHref} 80 + target="_blank" 81 + rel="noopener" 82 + style="color:var(--site-accent);text-decoration:none;font-size:0.75rem" 83 + > 84 + at.fund 85 + </a> 86 + </div> 87 + </div> 88 + ); 89 + })} 90 + </div> 91 + </div> 92 + ); 93 + }
+140
src/components/feed.tsx
··· 1 + // Bluesky feed component — server-rendered, no client JS. 2 + 3 + import { raw } from "hono/html"; 4 + import { getAuthorFeed, type FeedPost } from "../data.ts"; 5 + 6 + function escapeHtml(str: string): string { 7 + return str 8 + .replace(/&/g, "&amp;") 9 + .replace(/</g, "&lt;") 10 + .replace(/>/g, "&gt;") 11 + .replace(/"/g, "&quot;"); 12 + } 13 + 14 + function timeAgo(dateStr: string): string { 15 + const now = Date.now(); 16 + const then = new Date(dateStr).getTime(); 17 + const diff = now - then; 18 + const minutes = Math.floor(diff / 60000); 19 + if (minutes < 1) return "just now"; 20 + if (minutes < 60) return `${minutes}m`; 21 + const hours = Math.floor(minutes / 60); 22 + if (hours < 24) return `${hours}h`; 23 + const days = Math.floor(hours / 24); 24 + if (days < 30) return `${days}d`; 25 + return new Date(dateStr).toLocaleDateString("en-US", { 26 + month: "short", 27 + day: "numeric", 28 + }); 29 + } 30 + 31 + function postUrl(uri: string): string { 32 + // at://did:plc:.../app.bsky.feed.post/rkey -> bsky.app link 33 + const parts = uri.split("/"); 34 + const did = parts[2]; 35 + const rkey = parts[parts.length - 1]; 36 + return `https://bsky.app/profile/${did}/post/${rkey}`; 37 + } 38 + 39 + // Linkify URLs, @mentions, and facets in post text 40 + function truncateUrl(url: string, max = 40): string { 41 + try { 42 + const u = new URL(url); 43 + const path = u.pathname + u.search; 44 + const display = u.hostname + (path.length > 1 ? path : ""); 45 + return display.length > max ? display.slice(0, max) + "…" : display; 46 + } catch { 47 + return url.length > max ? url.slice(0, max) + "…" : url; 48 + } 49 + } 50 + 51 + function linkifyText(text: string): string { 52 + const escaped = escapeHtml(text); 53 + return escaped 54 + // URLs — truncate display text 55 + .replace( 56 + /(https?:\/\/[^\s<]+)/g, 57 + (_match, url) => 58 + `<a href="${url}" target="_blank" rel="noopener" class="feed-link">${truncateUrl(url)}</a>` 59 + ) 60 + // @mentions 61 + .replace( 62 + /@([\w.-]+\.[\w.-]+)/g, 63 + '<a href="https://bsky.app/profile/$1" target="_blank" rel="noopener" class="feed-mention">@$1</a>' 64 + ); 65 + } 66 + 67 + function renderImages(images: NonNullable<FeedPost["images"]>): string { 68 + if (images.length === 1) { 69 + return `<div class="feed-images feed-images-1"> 70 + <img src="${images[0].thumb}" alt="${escapeHtml(images[0].alt)}" loading="lazy" /> 71 + </div>`; 72 + } 73 + const grid = images 74 + .map( 75 + (img) => 76 + `<img src="${img.thumb}" alt="${escapeHtml(img.alt)}" loading="lazy" />` 77 + ) 78 + .join(""); 79 + return `<div class="feed-images feed-images-grid">${grid}</div>`; 80 + } 81 + 82 + function renderExternal(ext: NonNullable<FeedPost["external"]>): string { 83 + return `<a href="${escapeHtml(ext.uri)}" target="_blank" rel="noopener" class="feed-card"> 84 + ${ext.thumb ? `<img src="${ext.thumb}" alt="" class="feed-card-thumb" loading="lazy" />` : ""} 85 + <div class="feed-card-body"> 86 + <div class="feed-card-title">${escapeHtml(ext.title)}</div> 87 + <div class="feed-card-desc">${escapeHtml(ext.description).slice(0, 120)}</div> 88 + <div class="feed-card-url">${escapeHtml(new URL(ext.uri).hostname)}</div> 89 + </div> 90 + </a>`; 91 + } 92 + 93 + function renderQuote(qp: NonNullable<FeedPost["quotedPost"]>): string { 94 + return `<div class="feed-quote"> 95 + <div class="feed-quote-author"> 96 + ${qp.author.avatar ? `<img src="${qp.author.avatar}" alt="" class="feed-avatar-sm" />` : ""} 97 + <span class="feed-display-name">${escapeHtml(qp.author.displayName ?? qp.author.handle)}</span> 98 + <span class="feed-handle">@${escapeHtml(qp.author.handle)}</span> 99 + </div> 100 + <div class="feed-quote-text">${linkifyText(qp.text).slice(0, 300)}</div> 101 + </div>`; 102 + } 103 + 104 + function renderPost(post: FeedPost): string { 105 + const url = postUrl(post.uri); 106 + return `<article class="feed-post"> 107 + <div class="feed-post-header"> 108 + ${post.author.avatar ? `<img src="${post.author.avatar}" alt="" class="feed-avatar" />` : ""} 109 + <div> 110 + <span class="feed-display-name">${escapeHtml(post.author.displayName ?? post.author.handle)}</span> 111 + <a href="${url}" target="_blank" rel="noopener" class="feed-time">${timeAgo(post.createdAt)}</a> 112 + </div> 113 + </div> 114 + <div class="feed-text">${linkifyText(post.text)}</div> 115 + ${post.images ? renderImages(post.images) : ""} 116 + ${post.external ? renderExternal(post.external) : ""} 117 + ${post.quotedPost ? renderQuote(post.quotedPost) : ""} 118 + <div class="feed-stats"> 119 + <span title="Replies">${post.replyCount ? `💬 ${post.replyCount}` : ""}</span> 120 + <span title="Reposts">${post.repostCount ? `🔁 ${post.repostCount}` : ""}</span> 121 + <span title="Likes">${post.likeCount ? `♥ ${post.likeCount}` : ""}</span> 122 + </div> 123 + </article>`; 124 + } 125 + 126 + export async function Feed({ limit = 10 }: { limit?: number }) { 127 + const posts = await getAuthorFeed(limit); 128 + 129 + if (posts.length === 0) { 130 + return <div class="feed-empty">No posts yet.</div>; 131 + } 132 + 133 + const html = posts.map(renderPost).join(""); 134 + 135 + return ( 136 + <div class="feed"> 137 + {raw(html)} 138 + </div> 139 + ); 140 + }
+78
src/components/margin.tsx
··· 1 + // Margin annotations panel — renders at.margin.annotation records for a URL. 2 + // Gated behind ENABLE_MARGIN=true env var. 3 + 4 + import { getAnnotations, type MarginAnnotation } from "../data.ts"; 5 + 6 + function escapeHtml(str: string): string { 7 + return str 8 + .replace(/&/g, "&amp;") 9 + .replace(/</g, "&lt;") 10 + .replace(/>/g, "&gt;") 11 + .replace(/"/g, "&quot;"); 12 + } 13 + 14 + function formatDate(iso: string): string { 15 + return new Date(iso).toLocaleDateString("en-US", { 16 + month: "short", 17 + day: "numeric", 18 + year: "numeric", 19 + }); 20 + } 21 + 22 + export async function MarginPanel({ pageUrl }: { pageUrl: string }) { 23 + if (process.env.ENABLE_MARGIN !== "true") return <></>; 24 + 25 + const annotations = await getAnnotations(pageUrl); 26 + if (annotations.length === 0) return <></>; 27 + 28 + return ( 29 + <section class="margin-annotations"> 30 + <h3 style="font-size:0.875rem;font-weight:600;margin-bottom:0.75em;color:var(--site-text-muted)"> 31 + Margin annotations 32 + </h3> 33 + {annotations.map((a) => ( 34 + <div class="margin-annotation" key={a.id}> 35 + {a.target?.selector?.exact && ( 36 + <blockquote class="margin-highlight"> 37 + {escapeHtml(a.target.selector.exact)} 38 + </blockquote> 39 + )} 40 + {a.body?.value && ( 41 + <p class="margin-body">{escapeHtml(a.body.value)}</p> 42 + )} 43 + <div class="margin-meta"> 44 + {a.creator?.name && ( 45 + <span class="margin-author"> 46 + {a.creator.id ? ( 47 + <a 48 + href={`https://margin.at/user/${a.creator.id}`} 49 + target="_blank" 50 + rel="noopener" 51 + > 52 + {escapeHtml(a.creator.name)} 53 + </a> 54 + ) : ( 55 + escapeHtml(a.creator.name) 56 + )} 57 + </span> 58 + )} 59 + {a.created && ( 60 + <span class="margin-date">{formatDate(a.created)}</span> 61 + )} 62 + </div> 63 + </div> 64 + ))} 65 + <p style="font-size:0.75rem;color:var(--site-text-muted);margin-top:0.75em"> 66 + Powered by{" "} 67 + <a 68 + href={`https://margin.at/page?url=${encodeURIComponent(pageUrl)}`} 69 + target="_blank" 70 + rel="noopener" 71 + style="color:var(--site-accent);text-decoration:none" 72 + > 73 + Margin 74 + </a> 75 + </p> 76 + </section> 77 + ); 78 + }
+196
src/components/semble.tsx
··· 1 + // Semble page — renders collections and cards from the Semble AppView API. 2 + 3 + import { raw } from "hono/html"; 4 + import { 5 + getProfile, 6 + getSembleCollections, 7 + getSembleCards, 8 + type SembleCard, 9 + type SembleCollection, 10 + } from "../data.ts"; 11 + 12 + function escapeHtml(str: string): string { 13 + return str 14 + .replace(/&/g, "&amp;") 15 + .replace(/</g, "&lt;") 16 + .replace(/>/g, "&gt;") 17 + .replace(/"/g, "&quot;"); 18 + } 19 + 20 + function displayDomain(url: string): string { 21 + try { 22 + return new URL(url).hostname.replace(/^www\./, ""); 23 + } catch { 24 + return url; 25 + } 26 + } 27 + 28 + function sembleCollectionUrl(collection: SembleCollection): string { 29 + if (!collection.uri) return "https://semble.so"; 30 + // uri format: at://did:plc:xxx/network.cosmik.collection/rkey 31 + const rkey = collection.uri.split("/").pop(); 32 + const handle = collection.author?.handle ?? "cameron.stream"; 33 + return `https://semble.so/profile/${handle}/collections/${rkey}`; 34 + } 35 + 36 + function sembleCardUrl(card: SembleCard): string { 37 + return card.url; 38 + } 39 + 40 + export async function Semble() { 41 + const [collections, cards, profile] = await Promise.all([ 42 + getSembleCollections(), 43 + getSembleCards(30), 44 + getProfile(), 45 + ]); 46 + 47 + const avatarHtml = profile?.avatar 48 + ? `<a href="/" class="nav-avatar"><img src="${profile.avatar}" alt="${profile.displayName ?? ""}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;vertical-align:middle;" /></a>` 49 + : ""; 50 + 51 + return ( 52 + <div> 53 + <nav class="site-nav"> 54 + {raw(avatarHtml)} 55 + <a href="/blog">Blog</a> 56 + <a href="/about">About</a> 57 + <a href="/endorsements">Endorsements</a> 58 + <a href="/annotations">Annotations</a> 59 + <a href="/semble" style="color:var(--site-text);font-weight:600"> 60 + Semble 61 + </a> 62 + </nav> 63 + 64 + <h1 style="font-size:1.75rem;font-weight:700;margin:1em 0 0.25em"> 65 + Semble 66 + </h1> 67 + <p style="color:var(--site-text-secondary);font-size:0.9rem;margin-bottom:1.5em;line-height:1.5"> 68 + Collections and bookmarks on{" "} 69 + <a 70 + href="https://semble.so" 71 + target="_blank" 72 + rel="noopener" 73 + style="color:var(--site-accent);text-decoration:none" 74 + > 75 + Semble 76 + </a> 77 + , stored as{" "} 78 + <a 79 + href="https://docs.cosmik.network/semble/developer-guide/semble-lexicon-reference" 80 + target="_blank" 81 + rel="noopener" 82 + style="color:var(--site-accent);text-decoration:none" 83 + > 84 + network.cosmik.card 85 + </a>{" "} 86 + records on my PDS. 87 + </p> 88 + 89 + {collections.length > 0 && ( 90 + <div style="margin-bottom:2em"> 91 + <h2 style="font-size:1.1rem;font-weight:600;margin-bottom:0.75em"> 92 + Collections 93 + </h2> 94 + <div class="semble-collections"> 95 + {collections.map((c) => ( 96 + <a 97 + href={sembleCollectionUrl(c)} 98 + target="_blank" 99 + rel="noopener" 100 + class="semble-collection-card" 101 + key={c.id} 102 + > 103 + <div class="semble-collection-name"> 104 + {escapeHtml(c.name)} 105 + </div> 106 + {c.description && ( 107 + <div class="semble-collection-desc"> 108 + {escapeHtml(c.description)} 109 + </div> 110 + )} 111 + <div class="semble-collection-meta"> 112 + {c.cardCount} card{c.cardCount !== 1 ? "s" : ""} 113 + </div> 114 + </a> 115 + ))} 116 + </div> 117 + </div> 118 + )} 119 + 120 + <div> 121 + <h2 style="font-size:1.1rem;font-weight:600;margin-bottom:0.75em"> 122 + Recent cards 123 + </h2> 124 + {cards.length === 0 ? ( 125 + <p style="color:var(--site-text-muted);font-size:0.9rem"> 126 + No cards yet. 127 + </p> 128 + ) : ( 129 + <div class="semble-cards"> 130 + {cards 131 + .filter((c) => c.type === "URL") 132 + .map((card) => { 133 + const date = new Date(card.createdAt).toLocaleDateString( 134 + "en-US", 135 + { month: "short", day: "numeric", year: "numeric" } 136 + ); 137 + const domain = displayDomain(card.url); 138 + const title = 139 + card.cardContent?.title || domain; 140 + 141 + return ( 142 + <div class="semble-card-item" key={card.id}> 143 + <div> 144 + <a 145 + href={sembleCardUrl(card)} 146 + target="_blank" 147 + rel="noopener" 148 + style="color:var(--site-text);text-decoration:none;font-weight:600;font-size:0.9rem;line-height:1.3" 149 + > 150 + {escapeHtml(title)} 151 + </a> 152 + </div> 153 + {card.cardContent?.description && ( 154 + <p style="font-size:0.8125rem;color:var(--site-text-secondary);margin:0.2em 0;line-height:1.4"> 155 + {escapeHtml( 156 + card.cardContent.description.length > 160 157 + ? card.cardContent.description.slice(0, 160) + "..." 158 + : card.cardContent.description 159 + )} 160 + </p> 161 + )} 162 + {card.note?.text && ( 163 + <blockquote class="semble-card-note"> 164 + {escapeHtml(card.note.text)} 165 + </blockquote> 166 + )} 167 + <div class="semble-card-meta"> 168 + <span>{domain}</span> 169 + <span>{date}</span> 170 + {card.collections.length > 0 && ( 171 + <span> 172 + in{" "} 173 + {card.collections.map((c) => c.name).join(", ")} 174 + </span> 175 + )} 176 + </div> 177 + </div> 178 + ); 179 + })} 180 + </div> 181 + )} 182 + </div> 183 + 184 + <div style="margin-top:2em;padding-top:1em;border-top:1px solid var(--site-border)"> 185 + <a 186 + href={`https://semble.so/profile/cameron.stream`} 187 + target="_blank" 188 + rel="noopener" 189 + style="color:var(--site-accent);text-decoration:none;font-size:0.875rem" 190 + > 191 + View full profile on Semble 192 + </a> 193 + </div> 194 + </div> 195 + ); 196 + }
+60
src/components/site.tsx
··· 1 + // stream.cameron.Site — Landing page component. 2 + // Renders profile header and nav. 3 + 4 + import { raw } from "hono/html"; 5 + import { getProfile } from "../data.ts"; 6 + 7 + export async function Site() { 8 + const profile = await getProfile(); 9 + 10 + const avatarHtml = profile?.avatar 11 + ? `<img src="${profile.avatar}" alt="" style="width:80px;height:80px;border-radius:50%;object-fit:cover;" />` 12 + : ""; 13 + 14 + return ( 15 + <div> 16 + <div style="text-align:center;padding:3em 0 1em"> 17 + {raw(avatarHtml)} 18 + <h1 style="margin:0.5em 0 0;font-size:1.75rem;font-weight:700"> 19 + {profile?.displayName ?? "Cameron Pfiffer"} 20 + </h1> 21 + <p style="margin:0.25em 0;color:var(--site-text-secondary);font-size:0.9rem"> 22 + <a 23 + href={`https://bsky.app/profile/${profile?.handle ?? "cameron.stream"}`} 24 + target="_blank" 25 + rel="noopener" 26 + style="color:inherit;text-decoration:none" 27 + > 28 + @{profile?.handle ?? "cameron.stream"} 29 + </a> 30 + </p> 31 + {profile?.description && ( 32 + <p style="margin:1em auto 0;max-width:480px;color:var(--site-text-secondary);font-size:0.9rem;line-height:1.5"> 33 + {profile.description} 34 + </p> 35 + )} 36 + </div> 37 + 38 + <nav class="site-nav" style="justify-content:center"> 39 + <a href="/">Home</a> 40 + <a href="/blog">Blog</a> 41 + <a href="/about">About</a> 42 + <a href="/endorsements">Endorsements</a> 43 + <a href="/annotations">Annotations</a> 44 + <a href="/semble">Semble</a> 45 + <a href="/bluesky">Bluesky</a> 46 + <a href="https://github.com/cpfiffer" target="_blank" rel="noopener"> 47 + GitHub 48 + </a> 49 + </nav> 50 + </div> 51 + ); 52 + } 53 + 54 + function escapeHtml(str: string): string { 55 + return str 56 + .replace(/&/g, "&amp;") 57 + .replace(/</g, "&lt;") 58 + .replace(/>/g, "&gt;") 59 + .replace(/"/g, "&quot;"); 60 + }
+581
src/data.ts
··· 1 + // Data fetching layer — reads site.standard.document records from the PDS. 2 + 3 + import { AtUri } from "@atproto/syntax"; 4 + import { UnicodeString } from "@atproto/api"; 5 + 6 + const PDS = "https://bsky.social"; 7 + const COLLECTION = "site.standard.document"; 8 + const PUBLICATION_URI = 9 + process.env.PUBLICATION_URI || 10 + "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3md7ylshxzk2y"; 11 + 12 + export interface BlogPost { 13 + uri: string; 14 + rkey: string; 15 + slug: string; 16 + title: string; 17 + body: string; 18 + description?: string; 19 + tags?: string[]; 20 + publishedAt: string; 21 + updatedAt?: string; 22 + } 23 + 24 + export interface ProfileData { 25 + did: string; 26 + handle: string; 27 + displayName?: string; 28 + description?: string; 29 + avatar?: string; 30 + followersCount?: number; 31 + followsCount?: number; 32 + postsCount?: number; 33 + } 34 + 35 + function getDid(): string { 36 + return process.env.CAMERON_DID || "did:plc:gfrmhdmjvxn2sjedzboeudef"; 37 + } 38 + 39 + async function getRecord( 40 + repo: string, 41 + collection: string, 42 + rkey: string 43 + ): Promise<Record<string, unknown> | null> { 44 + const res = await fetch( 45 + `${PDS}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}` 46 + ); 47 + if (!res.ok) return null; 48 + const data = await res.json(); 49 + return data.value as Record<string, unknown>; 50 + } 51 + 52 + async function listRecords( 53 + repo: string, 54 + collection: string, 55 + limit = 100 56 + ): Promise<Array<{ uri: string; value: Record<string, unknown> }>> { 57 + const res = await fetch( 58 + `${PDS}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&limit=${limit}` 59 + ); 60 + if (!res.ok) return []; 61 + const data = await res.json(); 62 + return (data.records ?? []) as Array<{ 63 + uri: string; 64 + value: Record<string, unknown>; 65 + }>; 66 + } 67 + 68 + // Extract slug from the document's path field. 69 + // Handles /blog/my-slug, /3mif73hcqsc24 (Leaflet TID paths), etc. 70 + function slugFromPath(path: string | undefined): string { 71 + if (!path) return ""; 72 + const parts = path.split("/").filter(Boolean); 73 + return parts[parts.length - 1] || ""; 74 + } 75 + 76 + // Extract markdown body from the content union. 77 + // Handles our #markdown type and Leaflet's pub.leaflet.content block format. 78 + function bodyFromContent(content: unknown): string { 79 + if (!content || typeof content !== "object") return ""; 80 + const c = content as Record<string, unknown>; 81 + 82 + // Our markdown content type 83 + if (typeof c.value === "string") return c.value; 84 + 85 + // Leaflet block-based content 86 + if (c.$type === "pub.leaflet.content" && Array.isArray(c.pages)) { 87 + return leafletToMarkdown(c.pages); 88 + } 89 + 90 + return ""; 91 + } 92 + 93 + // Convert Leaflet block-based content to markdown. 94 + function leafletToMarkdown(pages: any[]): string { 95 + const lines: string[] = []; 96 + let inBlockquote = false; 97 + 98 + for (const page of pages) { 99 + if (page.$type !== "pub.leaflet.pages.linearDocument") continue; 100 + if (!Array.isArray(page.blocks)) continue; 101 + 102 + for (const wrapper of page.blocks) { 103 + const block = wrapper.block; 104 + if (!block) continue; 105 + 106 + const isQuote = block.$type === "pub.leaflet.blocks.blockquote"; 107 + 108 + // Close blockquote run when we hit a non-quote block 109 + if (!isQuote && inBlockquote) { 110 + lines.push(""); 111 + inBlockquote = false; 112 + } 113 + 114 + switch (block.$type) { 115 + case "pub.leaflet.blocks.text": 116 + lines.push(applyFacets(block.plaintext ?? "", block.facets)); 117 + lines.push(""); 118 + break; 119 + 120 + case "pub.leaflet.blocks.header": 121 + lines.push(`${"#".repeat(block.level ?? 2)} ${block.plaintext ?? ""}`); 122 + lines.push(""); 123 + break; 124 + 125 + case "pub.leaflet.blocks.code": 126 + lines.push(`\`\`\`${block.language ?? ""}`); 127 + lines.push(block.plaintext ?? ""); 128 + lines.push("```"); 129 + lines.push(""); 130 + break; 131 + 132 + case "pub.leaflet.blocks.blockquote": 133 + if (inBlockquote) { 134 + lines.push(">"); // blank quote line to separate paragraphs 135 + } 136 + lines.push( 137 + (applyFacets(block.plaintext ?? "", block.facets)) 138 + .split("\n") 139 + .map((l: string) => `> ${l}`) 140 + .join("\n") 141 + ); 142 + inBlockquote = true; 143 + break; 144 + 145 + case "pub.leaflet.blocks.unorderedList": 146 + if (Array.isArray(block.children)) { 147 + for (const item of block.children) { 148 + const text = item.content?.plaintext ?? ""; 149 + lines.push(`- ${applyFacets(text, item.content?.facets)}`); 150 + } 151 + lines.push(""); 152 + } 153 + break; 154 + 155 + case "pub.leaflet.blocks.image": { 156 + const ref = block.image?.ref?.$link; 157 + if (ref) { 158 + const did = getDid(); 159 + lines.push(`![](https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${ref}@jpeg)`); 160 + } else { 161 + lines.push("*[image]*"); 162 + } 163 + lines.push(""); 164 + break; 165 + } 166 + 167 + case "pub.leaflet.blocks.bskyPost": 168 + if (block.postRef?.uri) { 169 + const postDid = block.postRef.uri.split("/")[2]; 170 + const postRkey = block.postRef.uri.split("/").pop(); 171 + const host = block.clientHost || "bsky.app"; 172 + const postUrl = `https://${host}/profile/${postDid}/post/${postRkey}`; 173 + // Placeholder replaced at render time with oEmbed HTML 174 + lines.push(`<bsky-embed data-url="${postUrl}"></bsky-embed>`); 175 + lines.push(""); 176 + } 177 + break; 178 + 179 + default: 180 + // Unknown block type, try plaintext fallback 181 + if (block.plaintext) { 182 + lines.push(block.plaintext); 183 + lines.push(""); 184 + } 185 + break; 186 + } 187 + } 188 + } 189 + 190 + if (inBlockquote) { 191 + lines.push(""); 192 + } 193 + 194 + return lines.join("\n").trim(); 195 + } 196 + 197 + // Apply Leaflet richtext facets using UnicodeString for correct byte-offset slicing. 198 + function applyFacets(text: string, facets?: any[]): string { 199 + if (!facets || facets.length === 0) return text; 200 + 201 + const us = new UnicodeString(text); 202 + const sorted = [...facets] 203 + .filter((f) => f.index.byteStart <= f.index.byteEnd) 204 + .sort((a, b) => a.index.byteStart - b.index.byteStart); 205 + 206 + const parts: string[] = []; 207 + let cursor = 0; 208 + 209 + for (const facet of sorted) { 210 + const { byteStart, byteEnd } = facet.index; 211 + 212 + // Text before this facet 213 + if (cursor < byteStart) { 214 + parts.push(us.slice(cursor, byteStart)); 215 + } else if (cursor > byteStart) { 216 + continue; // overlapping facet, skip 217 + } 218 + 219 + let segment = us.slice(byteStart, byteEnd); 220 + if (segment.trim()) { 221 + for (const feature of facet.features ?? []) { 222 + switch (feature.$type) { 223 + case "pub.leaflet.richtext.facet#bold": 224 + segment = `**${segment}**`; 225 + break; 226 + case "pub.leaflet.richtext.facet#italic": 227 + segment = `*${segment}*`; 228 + break; 229 + case "pub.leaflet.richtext.facet#code": 230 + segment = `\`${segment}\``; 231 + break; 232 + case "pub.leaflet.richtext.facet#link": 233 + segment = `[${segment}](${feature.uri})`; 234 + break; 235 + case "pub.leaflet.richtext.facet#strikethrough": 236 + segment = `~~${segment}~~`; 237 + break; 238 + } 239 + } 240 + } 241 + parts.push(segment); 242 + cursor = byteEnd; 243 + } 244 + 245 + // Remaining text after last facet 246 + if (cursor < us.length) { 247 + parts.push(us.slice(cursor, us.length)); 248 + } 249 + 250 + return parts.join(""); 251 + } 252 + 253 + // Strip a leading H1 from markdown if it matches the document title, 254 + // since the blog-post component already renders the title in <h1>. 255 + function stripDuplicateTitle(body: string, title: string): string { 256 + const match = body.match(/^#\s+(.+)\n*/); 257 + if (match && match[1].trim() === title.trim()) { 258 + return body.slice(match[0].length); 259 + } 260 + return body; 261 + } 262 + 263 + function parseDocument( 264 + uri: string, 265 + v: Record<string, unknown> 266 + ): BlogPost { 267 + const parsed = new AtUri(uri); 268 + const title = v.title as string; 269 + const rawBody = bodyFromContent(v.content) || (v.textContent as string) || ""; 270 + return { 271 + uri, 272 + rkey: parsed.rkey, 273 + slug: slugFromPath(v.path as string | undefined), 274 + title, 275 + body: stripDuplicateTitle(rawBody, title), 276 + description: v.description as string | undefined, 277 + tags: v.tags as string[] | undefined, 278 + publishedAt: v.publishedAt as string, 279 + updatedAt: v.updatedAt as string | undefined, 280 + }; 281 + } 282 + 283 + export async function getProfile(): Promise<ProfileData | null> { 284 + const did = getDid(); 285 + const res = await fetch( 286 + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}` 287 + ); 288 + if (!res.ok) return null; 289 + const data = await res.json(); 290 + return { 291 + did: data.did, 292 + handle: data.handle, 293 + displayName: data.displayName, 294 + description: data.description, 295 + avatar: data.avatar, 296 + followersCount: data.followersCount, 297 + followsCount: data.followsCount, 298 + postsCount: data.postsCount, 299 + }; 300 + } 301 + 302 + export async function listBlogPosts(): Promise<BlogPost[]> { 303 + const did = getDid(); 304 + const records = await listRecords(did, COLLECTION); 305 + return records 306 + .filter((r) => (r.value.site as string) === PUBLICATION_URI) // only our publication 307 + .map((r) => parseDocument(r.uri, r.value)) 308 + .filter((p) => p.slug) 309 + .sort( 310 + (a, b) => 311 + new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime() 312 + ); 313 + } 314 + 315 + // Look up a document by its slug (path suffix). 316 + // Since documents use TID keys, we list all and find by path. 317 + export async function getBlogPost(slug: string): Promise<BlogPost | null> { 318 + const posts = await listBlogPosts(); 319 + return posts.find((p) => p.slug === slug) ?? null; 320 + } 321 + 322 + // --- About --- 323 + 324 + export async function getAbout(): Promise<string | null> { 325 + const did = getDid(); 326 + const record = await getRecord(did, "stream.cameron.about", "self"); 327 + if (!record) return null; 328 + return (record.content as string) ?? null; 329 + } 330 + 331 + // --- Margin annotations --- 332 + 333 + export interface MarginAnnotation { 334 + id: string; 335 + body?: { value?: string; format?: string }; 336 + target: { source: string; selector?: any; title?: string }; 337 + creator?: { name?: string; id?: string }; 338 + created?: string; 339 + motivation?: string; 340 + } 341 + 342 + export async function getAnnotations( 343 + pageUrl: string 344 + ): Promise<MarginAnnotation[]> { 345 + if (process.env.ENABLE_MARGIN !== "true") return []; 346 + 347 + try { 348 + const res = await fetch( 349 + `https://margin.at/api/annotations?url=${encodeURIComponent(pageUrl)}` 350 + ); 351 + if (!res.ok) return []; 352 + const data = await res.json(); 353 + return (data.items ?? []) as MarginAnnotation[]; 354 + } catch { 355 + return []; 356 + } 357 + } 358 + 359 + // --- Endorsements (fund.at.endorse) --- 360 + 361 + export interface Endorsement { 362 + uri: string; // endorsed entity (DID or hostname) 363 + createdAt: string; 364 + } 365 + 366 + export async function getEndorsements(): Promise<Endorsement[]> { 367 + const did = getDid(); 368 + const records = await listRecords(did, "fund.at.endorse"); 369 + return records 370 + .map((r) => ({ 371 + uri: r.value.uri as string, 372 + createdAt: r.value.createdAt as string, 373 + })) 374 + .sort( 375 + (a, b) => 376 + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() 377 + ); 378 + } 379 + 380 + // --- Bluesky feed --- 381 + 382 + export interface FeedPost { 383 + uri: string; 384 + cid: string; 385 + text: string; 386 + createdAt: string; 387 + author: { 388 + did: string; 389 + handle: string; 390 + displayName?: string; 391 + avatar?: string; 392 + }; 393 + likeCount: number; 394 + repostCount: number; 395 + replyCount: number; 396 + // Embedded images 397 + images?: Array<{ 398 + thumb: string; 399 + fullsize: string; 400 + alt: string; 401 + }>; 402 + // Embedded link card 403 + external?: { 404 + uri: string; 405 + title: string; 406 + description: string; 407 + thumb?: string; 408 + }; 409 + // Quote post 410 + quotedPost?: { 411 + uri: string; 412 + text: string; 413 + author: { 414 + handle: string; 415 + displayName?: string; 416 + avatar?: string; 417 + }; 418 + }; 419 + // Is this a reply? 420 + isReply: boolean; 421 + } 422 + 423 + // --- Semble (network.cosmik) --- 424 + 425 + const SEMBLE_API = "https://api.semble.so"; 426 + const SEMBLE_HANDLE = process.env.SEMBLE_HANDLE || "cameron.stream"; 427 + 428 + export interface SembleCardContent { 429 + url: string; 430 + title?: string; 431 + description?: string; 432 + imageUrl?: string; 433 + siteName?: string; 434 + author?: string; 435 + type?: string; 436 + } 437 + 438 + export interface SembleCard { 439 + id: string; 440 + type: string; 441 + url: string; 442 + uri?: string; 443 + cardContent: SembleCardContent; 444 + note?: { id: string; text: string }; 445 + createdAt: string; 446 + updatedAt: string; 447 + collections: Array<{ id: string; name: string }>; 448 + author: { 449 + id: string; 450 + name?: string; 451 + handle: string; 452 + avatarUrl?: string; 453 + }; 454 + } 455 + 456 + export interface SembleCollection { 457 + id: string; 458 + uri?: string; 459 + name: string; 460 + description?: string; 461 + accessType?: string; 462 + cardCount: number; 463 + createdAt: string; 464 + updatedAt: string; 465 + author: { 466 + id: string; 467 + name?: string; 468 + handle: string; 469 + avatarUrl?: string; 470 + }; 471 + } 472 + 473 + export async function getSembleCollections(): Promise<SembleCollection[]> { 474 + try { 475 + const res = await fetch( 476 + `${SEMBLE_API}/api/collections/user/${encodeURIComponent(SEMBLE_HANDLE)}` 477 + ); 478 + if (!res.ok) return []; 479 + const data = await res.json(); 480 + return (data.collections ?? []) as SembleCollection[]; 481 + } catch { 482 + return []; 483 + } 484 + } 485 + 486 + export async function getSembleCards(limit = 20): Promise<SembleCard[]> { 487 + try { 488 + const res = await fetch( 489 + `${SEMBLE_API}/api/cards/user/${encodeURIComponent(SEMBLE_HANDLE)}?limit=${limit}&sortOrder=desc` 490 + ); 491 + if (!res.ok) return []; 492 + const data = await res.json(); 493 + return (data.cards ?? []) as SembleCard[]; 494 + } catch { 495 + return []; 496 + } 497 + } 498 + 499 + export async function getAuthorFeed(limit = 20): Promise<FeedPost[]> { 500 + const did = getDid(); 501 + const res = await fetch( 502 + `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${encodeURIComponent(did)}&limit=${limit}&filter=posts_no_replies` 503 + ); 504 + if (!res.ok) return []; 505 + const data = await res.json(); 506 + 507 + return (data.feed ?? []).map((item: any) => { 508 + const post = item.post; 509 + const record = post.record; 510 + const embed = post.embed; 511 + 512 + // Extract images from embed 513 + let images: FeedPost["images"]; 514 + if (embed?.$type === "app.bsky.embed.images#view") { 515 + images = embed.images.map((img: any) => ({ 516 + thumb: img.thumb, 517 + fullsize: img.fullsize, 518 + alt: img.alt ?? "", 519 + })); 520 + } else if (embed?.$type === "app.bsky.embed.recordWithMedia#view") { 521 + if (embed.media?.$type === "app.bsky.embed.images#view") { 522 + images = embed.media.images.map((img: any) => ({ 523 + thumb: img.thumb, 524 + fullsize: img.fullsize, 525 + alt: img.alt ?? "", 526 + })); 527 + } 528 + } 529 + 530 + // Extract external link card 531 + let external: FeedPost["external"]; 532 + if (embed?.$type === "app.bsky.embed.external#view") { 533 + external = { 534 + uri: embed.external.uri, 535 + title: embed.external.title, 536 + description: embed.external.description, 537 + thumb: embed.external.thumb, 538 + }; 539 + } 540 + 541 + // Extract quote post 542 + let quotedPost: FeedPost["quotedPost"]; 543 + const quotedRecord = 544 + embed?.$type === "app.bsky.embed.record#view" 545 + ? embed.record 546 + : embed?.$type === "app.bsky.embed.recordWithMedia#view" 547 + ? embed.record?.record 548 + : null; 549 + if (quotedRecord?.value?.text) { 550 + quotedPost = { 551 + uri: quotedRecord.uri, 552 + text: quotedRecord.value.text, 553 + author: { 554 + handle: quotedRecord.author?.handle ?? "", 555 + displayName: quotedRecord.author?.displayName, 556 + avatar: quotedRecord.author?.avatar, 557 + }, 558 + }; 559 + } 560 + 561 + return { 562 + uri: post.uri, 563 + cid: post.cid, 564 + text: record.text ?? "", 565 + createdAt: record.createdAt, 566 + author: { 567 + did: post.author.did, 568 + handle: post.author.handle, 569 + displayName: post.author.displayName, 570 + avatar: post.author.avatar, 571 + }, 572 + likeCount: post.likeCount ?? 0, 573 + repostCount: post.repostCount ?? 0, 574 + replyCount: post.replyCount ?? 0, 575 + images, 576 + external, 577 + quotedPost, 578 + isReply: !!item.reply, 579 + }; 580 + }); 581 + }
+3
src/env.ts
··· 1 + import dotenv from "dotenv"; 2 + import { resolve } from "path"; 3 + dotenv.config({ path: resolve(process.cwd(), ".env") });
+5
src/image-map.json
··· 1 + { 2 + "/assets/images/grad-champ.jpg": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:gfrmhdmjvxn2sjedzboeudef/bafkreialgt5l6ruu4nmc7u6u4hht7o7px2bvg2opuqvcwg4pj6oaby7s3m@jpeg", 3 + "/assets/images/congo.jpg": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:gfrmhdmjvxn2sjedzboeudef/bafkreicbxgekbpduajixqrjfdbwh2zqm7gvq6s5pp4py34cuant6e3tjye@jpeg", 4 + "/assets/images/laugh.jpg": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:gfrmhdmjvxn2sjedzboeudef/bafkreibucfdarpc5pchlvgebu5uv2ghbvs2rbsh2qpgpniutegr3hew2ni@jpeg" 5 + }
+197
src/index.tsx
··· 1 + import "./env.ts"; 2 + import { Hono } from "hono"; 3 + import { raw } from "hono/html"; 4 + import { renderToReadableStream } from "hono/jsx/streaming"; 5 + import { serveStatic } from "@hono/node-server/serve-static"; 6 + import { serve } from "@hono/node-server"; 7 + import { Site } from "./components/site.tsx"; 8 + import { BlogIndex } from "./components/blog-index.tsx"; 9 + import { BlogPost } from "./components/blog-post.tsx"; 10 + import { About } from "./components/about.tsx"; 11 + import { Endorsements } from "./components/endorsements.tsx"; 12 + import { Annotations } from "./components/annotations.tsx"; 13 + import { Semble } from "./components/semble.tsx"; 14 + import { Bluesky } from "./components/bluesky.tsx"; 15 + import "./types.ts"; 16 + 17 + type JSXElement = 18 + | import("hono/utils/html").HtmlEscapedString 19 + | Promise<import("hono/utils/html").HtmlEscapedString>; 20 + 21 + const app = new Hono(); 22 + 23 + // --- Static files --- 24 + app.use("/public/*", serveStatic({ root: "./" })); 25 + 26 + // --- Theme init snippet (runs before paint to prevent flash) --- 27 + const themeInitScript = `(function(){var t=localStorage.getItem("theme");if(t==="dark"||t==="light")document.documentElement.setAttribute("data-theme",t)})()`; 28 + 29 + // --- Page shell --- 30 + function Shell({ 31 + children, 32 + title, 33 + }: { 34 + children: JSXElement; 35 + title?: string; 36 + }) { 37 + const pageTitle = title ? `${title} — cameron.stream` : "cameron.stream"; 38 + return ( 39 + <html lang="en"> 40 + <head> 41 + <meta charset="utf-8" /> 42 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 43 + <title>{pageTitle}</title> 44 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 45 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" /> 46 + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" /> 47 + <link rel="stylesheet" href="/public/host-primitives.css" /> 48 + <link rel="stylesheet" href="/public/host-theme.css" /> 49 + <link rel="stylesheet" href="/public/site.css" /> 50 + {raw(`<script>${themeInitScript}</script>`)} 51 + {raw(`<style> 52 + .page-container { 53 + max-width: var(--site-max-width); 54 + margin: 0 auto; 55 + padding: 0 1.25em 3em; 56 + } 57 + </style>`)} 58 + </head> 59 + <body> 60 + <div class="page-container">{children}</div> 61 + {raw(`<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme"></button>`)} 62 + <script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script> 63 + <script src="/public/theme.js"></script> 64 + </body> 65 + </html> 66 + ); 67 + } 68 + 69 + // --- Routes --- 70 + 71 + app.get("/", async (c) => { 72 + const stream = renderToReadableStream( 73 + <Shell> 74 + <Site /> 75 + </Shell> 76 + ); 77 + return c.body(stream, { 78 + headers: { 79 + "Content-Type": "text/html; charset=UTF-8", 80 + "Transfer-Encoding": "chunked", 81 + }, 82 + }); 83 + }); 84 + 85 + app.get("/blog", async (c) => { 86 + const stream = renderToReadableStream( 87 + <Shell title="Blog"> 88 + <BlogIndex /> 89 + </Shell> 90 + ); 91 + return c.body(stream, { 92 + headers: { 93 + "Content-Type": "text/html; charset=UTF-8", 94 + "Transfer-Encoding": "chunked", 95 + }, 96 + }); 97 + }); 98 + 99 + app.get("/about", async (c) => { 100 + const stream = renderToReadableStream( 101 + <Shell title="About"> 102 + <About /> 103 + </Shell> 104 + ); 105 + return c.body(stream, { 106 + headers: { 107 + "Content-Type": "text/html; charset=UTF-8", 108 + "Transfer-Encoding": "chunked", 109 + }, 110 + }); 111 + }); 112 + 113 + app.get("/endorsements", async (c) => { 114 + const stream = renderToReadableStream( 115 + <Shell title="Endorsements"> 116 + <Endorsements /> 117 + </Shell> 118 + ); 119 + return c.body(stream, { 120 + headers: { 121 + "Content-Type": "text/html; charset=UTF-8", 122 + "Transfer-Encoding": "chunked", 123 + }, 124 + }); 125 + }); 126 + 127 + app.get("/annotations", async (c) => { 128 + const stream = renderToReadableStream( 129 + <Shell title="Annotations"> 130 + <Annotations /> 131 + </Shell> 132 + ); 133 + return c.body(stream, { 134 + headers: { 135 + "Content-Type": "text/html; charset=UTF-8", 136 + "Transfer-Encoding": "chunked", 137 + }, 138 + }); 139 + }); 140 + 141 + app.get("/semble", async (c) => { 142 + const stream = renderToReadableStream( 143 + <Shell title="Semble"> 144 + <Semble /> 145 + </Shell> 146 + ); 147 + return c.body(stream, { 148 + headers: { 149 + "Content-Type": "text/html; charset=UTF-8", 150 + "Transfer-Encoding": "chunked", 151 + }, 152 + }); 153 + }); 154 + 155 + app.get("/bluesky", async (c) => { 156 + const stream = renderToReadableStream( 157 + <Shell title="Bluesky"> 158 + <Bluesky /> 159 + </Shell> 160 + ); 161 + return c.body(stream, { 162 + headers: { 163 + "Content-Type": "text/html; charset=UTF-8", 164 + "Transfer-Encoding": "chunked", 165 + }, 166 + }); 167 + }); 168 + 169 + app.get("/blog/:slug", async (c) => { 170 + // Redirect old /blog/slug URLs to /slug 171 + return c.redirect(`/${c.req.param("slug")}`, 301); 172 + }); 173 + 174 + // Catch-all: blog posts by slug (must be last) 175 + app.get("/:slug", async (c) => { 176 + const slug = c.req.param("slug"); 177 + const { getBlogPost } = await import("./data.ts"); 178 + const post = await getBlogPost(slug); 179 + if (!post) return c.notFound(); 180 + const stream = renderToReadableStream( 181 + <Shell title={post.title}> 182 + <BlogPost slug={slug} /> 183 + </Shell> 184 + ); 185 + return c.body(stream, { 186 + headers: { 187 + "Content-Type": "text/html; charset=UTF-8", 188 + "Transfer-Encoding": "chunked", 189 + }, 190 + }); 191 + }); 192 + 193 + // --- Start server --- 194 + const port = parseInt(process.env.PORT ?? "3002"); 195 + const hostname = process.env.HOST ?? "0.0.0.0"; 196 + console.log(`cameron.stream starting on http://${hostname}:${port}`); 197 + serve({ fetch: app.fetch, port, hostname });
+98
src/markdown.ts
··· 1 + import { Marked } from "marked"; 2 + import footnote from "marked-footnote"; 3 + import { createHighlighter, type Highlighter } from "shiki"; 4 + import imageMap from "./image-map.json" with { type: "json" }; 5 + 6 + let highlighter: Highlighter | null = null; 7 + 8 + async function getHighlighter(): Promise<Highlighter> { 9 + if (!highlighter) { 10 + highlighter = await createHighlighter({ 11 + themes: ["github-light", "github-dark"], 12 + langs: [ 13 + "javascript", 14 + "typescript", 15 + "python", 16 + "julia", 17 + "rust", 18 + "json", 19 + "bash", 20 + "yaml", 21 + "markdown", 22 + "html", 23 + "css", 24 + "sql", 25 + "r", 26 + "toml", 27 + ], 28 + }); 29 + } 30 + return highlighter; 31 + } 32 + 33 + export async function renderMarkdown(source: string): Promise<string> { 34 + const hl = await getHighlighter(); 35 + 36 + // Rewrite old Franklin image paths to CDN URLs 37 + const map = imageMap as Record<string, string>; 38 + const rewritten = source.replace( 39 + /!\[([^\]]*)\]\(([^)]+)\)/g, 40 + (match, alt, src) => { 41 + const cdnUrl = map[src]; 42 + return cdnUrl ? `![${alt}](${cdnUrl})` : match; 43 + } 44 + ); 45 + 46 + const marked = new Marked({ 47 + renderer: { 48 + code({ text, lang }) { 49 + const language = lang || "text"; 50 + try { 51 + return hl.codeToHtml(text, { 52 + lang: language, 53 + themes: { light: "github-light", dark: "github-dark" }, 54 + }); 55 + } catch { 56 + // Unknown language, fall back to plain 57 + return `<pre><code>${escapeHtml(text)}</code></pre>`; 58 + } 59 + }, 60 + }, 61 + }); 62 + 63 + marked.use(footnote()); 64 + 65 + let html = await marked.parse(rewritten); 66 + 67 + // Replace <bsky-embed> placeholders with oEmbed HTML 68 + const bskyRegex = /<bsky-embed data-url="([^"]+)"><\/bsky-embed>/g; 69 + const matches = [...html.matchAll(bskyRegex)]; 70 + for (const match of matches) { 71 + const url = match[1]; 72 + try { 73 + const res = await fetch( 74 + `https://embed.bsky.app/oembed?url=${encodeURIComponent(url)}` 75 + ); 76 + if (res.ok) { 77 + const data = await res.json(); 78 + html = html.replace(match[0], data.html); 79 + } 80 + } catch { 81 + // Keep as link fallback 82 + html = html.replace( 83 + match[0], 84 + `<p><a href="${url}" target="_blank" rel="noopener">Bluesky post</a></p>` 85 + ); 86 + } 87 + } 88 + 89 + return html; 90 + } 91 + 92 + function escapeHtml(str: string): string { 93 + return str 94 + .replace(/&/g, "&amp;") 95 + .replace(/</g, "&lt;") 96 + .replace(/>/g, "&gt;") 97 + .replace(/"/g, "&quot;"); 98 + }
+30
src/resolve.ts
··· 1 + const SLINGSHOT = "https://slingshot.microcosm.blue"; 2 + 3 + const didCache = new Map<string, { value: string; expires: number }>(); 4 + const CACHE_TTL = 5 * 60 * 1000; // 5 minutes 5 + 6 + export async function resolveDidToService( 7 + did: string, 8 + serviceId = "#atproto_pds" 9 + ): Promise<string> { 10 + const cacheKey = `${did}:${serviceId}`; 11 + const cached = didCache.get(cacheKey); 12 + if (cached && cached.expires > Date.now()) return cached.value; 13 + 14 + const res = await fetch( 15 + `${SLINGSHOT}/xrpc/com.bad-example.identity.resolveService?did=${encodeURIComponent(did)}&id=${encodeURIComponent(serviceId)}` 16 + ); 17 + if (!res.ok) { 18 + const body = await res.json().catch(() => ({ message: res.statusText })); 19 + throw new Error( 20 + `No ${serviceId} service found for DID: ${did} (${body.message ?? res.status})` 21 + ); 22 + } 23 + 24 + const data = (await res.json()) as { endpoint: string }; 25 + didCache.set(cacheKey, { 26 + value: data.endpoint, 27 + expires: Date.now() + CACHE_TTL, 28 + }); 29 + return data.endpoint; 30 + }
+27
src/types.ts
··· 1 + // Custom element IntrinsicElements declarations for Hono JSX 2 + 3 + declare module "hono/jsx" { 4 + namespace JSX { 5 + interface IntrinsicElements { 6 + "org-atsui-stack": Record<string, unknown>; 7 + "org-atsui-row": Record<string, unknown>; 8 + "org-atsui-fill": Record<string, unknown>; 9 + "org-atsui-grid": Record<string, unknown>; 10 + "org-atsui-clip": Record<string, unknown>; 11 + "org-atsui-cover": Record<string, unknown>; 12 + "org-atsui-avatar": Record<string, unknown>; 13 + "org-atsui-blob": Record<string, unknown>; 14 + "org-atsui-title": Record<string, unknown>; 15 + "org-atsui-heading": Record<string, unknown>; 16 + "org-atsui-text": Record<string, unknown>; 17 + "org-atsui-caption": Record<string, unknown>; 18 + "org-atsui-timestamp": Record<string, unknown>; 19 + "org-atsui-link": Record<string, unknown>; 20 + "org-atsui-card": Record<string, unknown>; 21 + "org-atsui-tabs": Record<string, unknown>; 22 + [key: `${string}-${string}`]: Record<string, unknown>; 23 + } 24 + } 25 + } 26 + 27 + export {};
+18
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "esnext", 5 + "moduleResolution": "bundler", 6 + "jsx": "react-jsx", 7 + "jsxImportSource": "hono/jsx", 8 + "strict": true, 9 + "esModuleInterop": true, 10 + "skipLibCheck": true, 11 + "noEmit": true, 12 + "allowImportingTsExtensions": true, 13 + "resolveJsonModule": true, 14 + "isolatedModules": true, 15 + "types": ["node"] 16 + }, 17 + "include": ["src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts"] 18 + }