Scratch space for learning atproto app development

Confidential client implementation

+7 -4
.env.template
···
# Environment Configuration
NODE_ENV="development" # Options: 'development', 'production'
PORT="8080" # The port your server will listen on
-
HOST="localhost" # Hostname for the server
PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id.
-
DB_PATH=":memory:" # The SQLite database path. Leave as ":memory:" to use a temporary in-memory database.
+
DB_PATH="db.sqlite" # The SQLite database path. Set as ":memory:" to use a temporary in-memory database.
-
# Secrets
-
# Must set this in production. May be generated with `openssl rand -base64 33`
+
# Secrets: *MUST* set this in production
+
+
# May be generated with `openssl rand -base64 33`
# COOKIE_SECRET=""
+
+
# May be generated with `./bin/gen-jwk` (requires `npm install` once first)
+
# PRIVATE_JWKS='[{"kty":"EC","kid":"123",...}]'
+2 -1
.gitignore
···
*.ntvs*
*.njsproj
*.sln
-
*.sw?
+
*.sw?
+
*.sqlite
+7
.prettierrc
···
+
{
+
"trailingComma": "all",
+
"tabWidth": 2,
+
"semi": false,
+
"singleQuote": true,
+
"useTabs": false
+
}
+7 -1
.vscode/settings.json
···
"url": "https://cdn.jsdelivr.net/npm/tsup/schema.json",
"fileMatch": ["package.json", "tsup.config.json"]
}
-
]
+
],
+
"[typescript]": {
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
+
},
+
"[javascript]": {
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
+
}
}
+15
bin/gen-jwk
···
+
#!/usr/bin/env node
+
+
'use strict'
+
+
const { JoseKey } = require('@atproto/jwk-jose')
+
+
async function main() {
+
const kid = Date.now().toString()
+
const key = await JoseKey.generate(['ES256'], kid)
+
const jwk = key.privateJwk
+
+
console.log(JSON.stringify(jwk))
+
}
+
+
main()
+308 -272
package-lock.json
···
"version": "0.0.1",
"license": "MIT",
"dependencies": {
-
"@atproto/api": "^0.13.4",
-
"@atproto/common": "^0.4.1",
-
"@atproto/identity": "^0.4.0",
-
"@atproto/lexicon": "^0.4.2",
-
"@atproto/oauth-client-node": "^0.2.2",
-
"@atproto/sync": "^0.1.4",
-
"@atproto/syntax": "^0.3.0",
-
"@atproto/xrpc-server": "^0.7.9",
+
"@atproto/api": "^0.15.6",
+
"@atproto/common": "^0.4.11",
+
"@atproto/identity": "^0.4.8",
+
"@atproto/jwk-jose": "^0.1.8",
+
"@atproto/lexicon": "^0.4.11",
+
"@atproto/oauth-client-node": "^0.2.24",
+
"@atproto/sync": "^0.1.26",
+
"@atproto/syntax": "^0.4.0",
+
"@atproto/xrpc-server": "^0.8.0",
"better-sqlite3": "^11.1.2",
"dotenv": "^16.4.5",
"envalid": "^8.0.0",
"express": "^4.19.2",
+
"http-terminator": "^3.2.0",
"iron-session": "^8.0.2",
"kysely": "^0.27.4",
"multiformats": "^9.9.0",
"pino": "^9.3.2",
-
"uhtml": "^4.5.9"
+
"uhtml": "^4.5.9",
+
"zod": "^3.25.67"
},
"devDependencies": {
"@atproto/lex-cli": "^0.4.1",
···
}
},
"node_modules/@atproto-labs/did-resolver": {
-
"version": "0.1.5",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.1.5.tgz",
-
"integrity": "sha512-uoCb+P0N4du5NiZt6ohVEbSDdijXBJlQwSlWLHX0rUDtEVV+g3aEGe7jUW94lWpqQmRlQ5xcyd9owleMibNxZw==",
+
"version": "0.1.13",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.1.13.tgz",
+
"integrity": "sha512-DG3YNaCKc6PAIv1Gsz3E1Kufw2t14OBxe4LdKK7KKLCNoex51hm+A5yMevShe3BSll+QosqWYIEgkPSc5xBoGQ==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/fetch": "0.1.1",
-
"@atproto-labs/pipe": "0.1.0",
-
"@atproto-labs/simple-store": "0.1.1",
-
"@atproto-labs/simple-store-memory": "0.1.1",
-
"@atproto/did": "0.1.3",
+
"@atproto-labs/fetch": "0.2.3",
+
"@atproto-labs/pipe": "0.1.1",
+
"@atproto-labs/simple-store": "0.2.0",
+
"@atproto-labs/simple-store-memory": "0.1.3",
+
"@atproto/did": "0.1.5",
"zod": "^3.23.8"
}
},
"node_modules/@atproto-labs/fetch": {
-
"version": "0.1.1",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.1.1.tgz",
-
"integrity": "sha512-X1zO1MDoJzEurbWXMAe1H8EZ995Xam/aXdxhGVrXmOMyPDuvBa1oxwh/kQNZRCKcMQUbiwkk+Jfq6ZkTuvGbww==",
+
"version": "0.2.3",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz",
+
"integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/pipe": "0.1.0"
-
},
-
"optionalDependencies": {
-
"zod": "^3.23.8"
+
"@atproto-labs/pipe": "0.1.1"
}
},
"node_modules/@atproto-labs/fetch-node": {
-
"version": "0.1.3",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/fetch-node/-/fetch-node-0.1.3.tgz",
-
"integrity": "sha512-KX3ogPJt6dXNppWImQ9omfhrc8t73WrJaxHMphRAqQL8jXxKW5NBCTjSuwroBkJ1pj1aValBrc5NpdYu+H/9Qg==",
+
"version": "0.1.9",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/fetch-node/-/fetch-node-0.1.9.tgz",
+
"integrity": "sha512-8sHDDXZEzQptLu8ddUU/8U+THS6dumgPynVX0/1PjUYd4S/FWyPcz6yMIiVChTfzKnZvYRRz47+qvOKhydrHQw==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/fetch": "0.1.1",
-
"@atproto-labs/pipe": "0.1.0",
+
"@atproto-labs/fetch": "0.2.3",
+
"@atproto-labs/pipe": "0.1.1",
"ipaddr.js": "^2.1.0",
-
"psl": "^1.9.0",
"undici": "^6.14.1"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto-labs/fetch-node/node_modules/ipaddr.js": {
···
}
},
"node_modules/@atproto-labs/handle-resolver": {
-
"version": "0.1.4",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.1.4.tgz",
-
"integrity": "sha512-tnGUD2mQ6c8xHs3eeVJgwYqM3FHoTZZbOcOGKqO1A5cuIG+gElwEhpWwpwX5LI7FF4J8eS9BOHLl3NFS7Q8QXg==",
+
"version": "0.1.8",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.1.8.tgz",
+
"integrity": "sha512-Y0ckccoCGDo/3g4thPkgp9QcORmc+qqEaCBCYCZYtfLIQp4775u22wd+4fyEyJP4DqoReKacninkICgRGfs3dQ==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/simple-store": "0.1.1",
-
"@atproto-labs/simple-store-memory": "0.1.1",
-
"@atproto/did": "0.1.3",
+
"@atproto-labs/simple-store": "0.2.0",
+
"@atproto-labs/simple-store-memory": "0.1.3",
+
"@atproto/did": "0.1.5",
"zod": "^3.23.8"
}
},
"node_modules/@atproto-labs/handle-resolver-node": {
-
"version": "0.1.7",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver-node/-/handle-resolver-node-0.1.7.tgz",
-
"integrity": "sha512-3pXUB8/twMPXUz+zMjSVTA5acxnizC7PF+EsjLKwirwVzLRrTcFQkyHXGTrdUfIQq+S1eLq7b6H7ZKqMOX9VQQ==",
+
"version": "0.1.16",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver-node/-/handle-resolver-node-0.1.16.tgz",
+
"integrity": "sha512-i2F989zjyC7b/odrV3/tOpIT1IDIxR3F0khPG4REfOWcmJ89QcP8BiejJ6KFJk3hbTJHq6X9/pTG1vesCvyIKA==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/fetch-node": "0.1.3",
-
"@atproto-labs/handle-resolver": "0.1.4",
-
"@atproto/did": "0.1.3"
+
"@atproto-labs/fetch-node": "0.1.9",
+
"@atproto-labs/handle-resolver": "0.1.8",
+
"@atproto/did": "0.1.5"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto-labs/identity-resolver": {
-
"version": "0.1.6",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.1.6.tgz",
-
"integrity": "sha512-kq1yhpImGG1IUE8QEKj2IjSfNrkG2VailZRuiFLYdcszDEBDzr9HN3ElV42ebxhofuSFgKOCrYWJIUiLuXo6Uw==",
+
"version": "0.1.18",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.1.18.tgz",
+
"integrity": "sha512-DArYXP1hzZJIBcojun0CWEF+TjAhlGKcVq/RwLiGfY1mKq2yPjCiXyHj+5L0+z9jBSZiAB7L65JgcjI2+MFiRg==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/did-resolver": "0.1.5",
-
"@atproto-labs/handle-resolver": "0.1.4",
-
"@atproto/syntax": "0.3.1"
+
"@atproto-labs/did-resolver": "0.1.13",
+
"@atproto-labs/handle-resolver": "0.1.8",
+
"@atproto/syntax": "0.4.0"
}
},
"node_modules/@atproto-labs/pipe": {
-
"version": "0.1.0",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.0.tgz",
-
"integrity": "sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w==",
+
"version": "0.1.1",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz",
+
"integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==",
"license": "MIT"
},
"node_modules/@atproto-labs/simple-store": {
-
"version": "0.1.1",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz",
-
"integrity": "sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg==",
+
"version": "0.2.0",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz",
+
"integrity": "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA==",
"license": "MIT"
},
"node_modules/@atproto-labs/simple-store-memory": {
-
"version": "0.1.1",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.1.tgz",
-
"integrity": "sha512-PCRqhnZ8NBNBvLku53O56T0lsVOtclfIrQU/rwLCc4+p45/SBPrRYNBi6YFq5rxZbK6Njos9MCmILV/KLQxrWA==",
+
"version": "0.1.3",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.3.tgz",
+
"integrity": "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/simple-store": "0.1.1",
+
"@atproto-labs/simple-store": "0.2.0",
"lru-cache": "^10.2.0"
}
},
"node_modules/@atproto/api": {
-
"version": "0.13.6",
-
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.6.tgz",
-
"integrity": "sha512-58emFFZhqY8nVWD3xFWK0yYqAmJ2un+NaTtZxBbRo00mGq1rz9VXTpVmfoHFcuXL1hoDQN3WyJfsub8r6xGOgg==",
+
"version": "0.15.16",
+
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.15.16.tgz",
+
"integrity": "sha512-ZNBrzBg2l0lHreKik1lJn8lrhAktwlY8NUPBU/hO9dwjAnDHQTiSzNFZt65dp9djmqZ75sX/VJ+heNuaJBvnhQ==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/common-web": "^0.3.0",
-
"@atproto/lexicon": "^0.4.1",
-
"@atproto/syntax": "^0.3.0",
-
"@atproto/xrpc": "^0.6.1",
+
"@atproto/common-web": "^0.4.2",
+
"@atproto/lexicon": "^0.4.11",
+
"@atproto/syntax": "^0.4.0",
+
"@atproto/xrpc": "^0.7.0",
"await-lock": "^2.2.2",
"multiformats": "^9.9.0",
-
"tlds": "^1.234.0"
+
"tlds": "^1.234.0",
+
"zod": "^3.23.8"
}
},
"node_modules/@atproto/common": {
-
"version": "0.4.7",
-
"resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.4.7.tgz",
-
"integrity": "sha512-C844ILV66sqHjQCJDb8tN/yZB2MBaLpZ1qptDT8zWRMx0uw7j/B6/EuN9R9a57Nj99Hhi93QkvQxOujURqpPeA==",
+
"version": "0.4.11",
+
"resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.4.11.tgz",
+
"integrity": "sha512-Knv0viYXNMfCdIE7jLUiWJKnnMfEwg+vz2epJQi8WOjqtqCFb3W/3Jn72ZiuovIfpdm13MaOiny6w2NErUQC6g==",
"license": "MIT",
"dependencies": {
-
"@atproto/common-web": "^0.3.2",
+
"@atproto/common-web": "^0.4.2",
"@ipld/dag-cbor": "^7.0.3",
"cbor-x": "^1.5.1",
"iso-datestring-validator": "^2.2.2",
"multiformats": "^9.9.0",
"pino": "^8.21.0"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/common-web": {
-
"version": "0.3.2",
-
"resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.2.tgz",
-
"integrity": "sha512-Vx0JtL1/CssJbFAb0UOdvTrkbUautsDfHNOXNTcX2vyPIxH9xOameSqLLunM1hZnOQbJwyjmQCt6TV+bhnanDg==",
+
"version": "0.4.2",
+
"resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.2.tgz",
+
"integrity": "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==",
"license": "MIT",
"dependencies": {
"graphemer": "^1.4.0",
···
}
},
"node_modules/@atproto/crypto": {
-
"version": "0.4.3",
-
"resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.3.tgz",
-
"integrity": "sha512-YSSUAvkx+ldpXw97NXZWfLx/prgh5YJ2K0BCw51JCJmXSRp6KhhwvOm4J+K/s5hwpssyuDCVTXknyS4PHwaK5g==",
+
"version": "0.4.4",
+
"resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.4.tgz",
+
"integrity": "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA==",
"license": "MIT",
"dependencies": {
"@noble/curves": "^1.7.0",
"@noble/hashes": "^1.6.1",
"uint8arrays": "3.0.0"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/did": {
-
"version": "0.1.3",
-
"resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.1.3.tgz",
-
"integrity": "sha512-ULD8Gw/KRRwLFZ2Z2L4DjmdOMrg8IYYlcjdSc+GQ2/QJSVnD2zaJJVTLd3vls121wGt/583rNaiZTT2DpBze4w==",
+
"version": "0.1.5",
+
"resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.1.5.tgz",
+
"integrity": "sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ==",
"license": "MIT",
"dependencies": {
"zod": "^3.23.8"
}
},
"node_modules/@atproto/identity": {
-
"version": "0.4.2",
-
"resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.2.tgz",
-
"integrity": "sha512-Z267XI84enuYQLV8hgDMVkGZqy8GtPI4PYVn1rz4YKwSaI+nGwADNtyK+ZZWFa0tTDKS6q6u4ae7B8RdrUlk8A==",
+
"version": "0.4.8",
+
"resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.8.tgz",
+
"integrity": "sha512-Z0sLnJ87SeNdAifT+rqpgE1Rc3layMMW25gfWNo4u40RGuRODbdfAZlTwBSU2r+Vk45hU+iE+xeQspfednCEnA==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/common-web": "^0.3.1",
-
"@atproto/crypto": "^0.4.1",
-
"axios": "^0.27.2"
+
"@atproto/common-web": "^0.4.2",
+
"@atproto/crypto": "^0.4.4"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/jwk": {
-
"version": "0.1.1",
-
"resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.1.1.tgz",
-
"integrity": "sha512-6h/bj1APUk7QcV9t/oA6+9DB5NZx9SZru9x+/pV5oHFI9Xz4ZuM5+dq1PfsJV54pZyqdnZ6W6M717cxoC7q7og==",
+
"version": "0.3.0",
+
"resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.3.0.tgz",
+
"integrity": "sha512-MIAXyNMGu1tCNHjqW/8jqfE/wgWCIoK2cJ0mR6UxwhNPvkbe35TcpRYJdtQu/E6MUd7TziyDBa/GO4dKAiePhQ==",
+
"license": "MIT",
"dependencies": {
"multiformats": "^9.9.0",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/jwk-jose": {
-
"version": "0.1.2",
-
"resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.2.tgz",
-
"integrity": "sha512-lDwc/6lLn2aZ/JpyyggyjLFsJPMntrVzryyGUx5aNpuTS8SIuc4Ky0REhxqfLopQXJJZCuRRjagHG3uP05/moQ==",
+
"version": "0.1.8",
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.8.tgz",
+
"integrity": "sha512-aoU2Q0GpIl388KhCcv9YvAxNscALUv3xzLq5gjVPdJ+zmqw94nGZNcjiNvpnbfS+VQM9e2DrrTuwmDXnxfrrSA==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/jwk": "0.1.1",
+
"@atproto/jwk": "0.3.0",
"jose": "^5.2.0"
}
},
"node_modules/@atproto/jwk-webcrypto": {
-
"version": "0.1.2",
-
"resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.2.tgz",
-
"integrity": "sha512-vTBUbUZXh0GI+6KJiPGukmI4BQEHFAij8fJJ4WnReF/hefAs3ISZtrWZHGBebz+q2EcExYlnhhlmxvDzV7veGw==",
+
"version": "0.1.8",
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.8.tgz",
+
"integrity": "sha512-oOW/G40f6M0NbTOo8uZgiSsFtcvlfNFldyxm+V+fVo5yKe6cvgvPNqckpqMsoBe6JYfImdc/zdVak9fCSSh41A==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/jwk": "0.1.1",
-
"@atproto/jwk-jose": "0.1.2"
+
"@atproto/jwk": "0.3.0",
+
"@atproto/jwk-jose": "0.1.8",
+
"zod": "^3.23.8"
}
},
"node_modules/@atproto/lex-cli": {
···
"lex": "dist/index.js"
}
},
+
"node_modules/@atproto/lex-cli/node_modules/@atproto/syntax": {
+
"version": "0.3.4",
+
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.4.tgz",
+
"integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==",
+
"dev": true,
+
"license": "MIT"
+
},
"node_modules/@atproto/lexicon": {
-
"version": "0.4.5",
-
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.5.tgz",
-
"integrity": "sha512-fljWqMGKn+XWtTprBcS3F1hGBREnQYh6qYHv2sjENucc7REms1gtmZXSerB9N6pVeHVNOnXiILdukeAcic5OEw==",
+
"version": "0.4.11",
+
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.11.tgz",
+
"integrity": "sha512-btefdnvNz2Ao2I+qbmj0F06HC8IlrM/IBz6qOBS50r0S6uDf5tOO+Mv2tSVdimFkdzyDdLtBI1sV36ONxz2cOw==",
"license": "MIT",
"dependencies": {
-
"@atproto/common-web": "^0.3.2",
-
"@atproto/syntax": "^0.3.1",
+
"@atproto/common-web": "^0.4.2",
+
"@atproto/syntax": "^0.4.0",
"iso-datestring-validator": "^2.2.2",
"multiformats": "^9.9.0",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/oauth-client": {
-
"version": "0.3.2",
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.3.2.tgz",
-
"integrity": "sha512-/HUlv5dnR1am4BQlVYSuevGf4mKJ5RMkElnum8lbwRDewKyzqHwdtJWeNcfcPFtDhUKg0U2pWfRv8ZZd6kk9dQ==",
+
"version": "0.4.0",
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.4.0.tgz",
+
"integrity": "sha512-uWVnlhennWkgvzqP0l53sFaw6DM6B4zmq0fv1xs05vt56Sjly8YirAj0GVDXlb37/BQRJQ1WOBzJVYDI3bH9uw==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/did-resolver": "0.1.5",
-
"@atproto-labs/fetch": "0.1.1",
-
"@atproto-labs/handle-resolver": "0.1.4",
-
"@atproto-labs/identity-resolver": "0.1.6",
-
"@atproto-labs/simple-store": "0.1.1",
-
"@atproto-labs/simple-store-memory": "0.1.1",
-
"@atproto/did": "0.1.3",
-
"@atproto/jwk": "0.1.1",
-
"@atproto/oauth-types": "0.2.1",
-
"@atproto/xrpc": "0.6.4",
+
"@atproto-labs/did-resolver": "0.1.13",
+
"@atproto-labs/fetch": "0.2.3",
+
"@atproto-labs/handle-resolver": "0.1.8",
+
"@atproto-labs/identity-resolver": "0.1.18",
+
"@atproto-labs/simple-store": "0.2.0",
+
"@atproto-labs/simple-store-memory": "0.1.3",
+
"@atproto/did": "0.1.5",
+
"@atproto/jwk": "0.3.0",
+
"@atproto/oauth-types": "0.3.0",
+
"@atproto/xrpc": "0.7.0",
"multiformats": "^9.9.0",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/oauth-client-node": {
-
"version": "0.2.2",
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-client-node/-/oauth-client-node-0.2.2.tgz",
-
"integrity": "sha512-IlO0ozTf+uDezfcdYU60U5gERDRc9DJgNRbm2IGEpHWBXEYBQlACQHlQ+yDGP8Ts3Xtfop2YXju8n+TdXdqeLQ==",
+
"version": "0.2.24",
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client-node/-/oauth-client-node-0.2.24.tgz",
+
"integrity": "sha512-WsUiFkHjlm80J2d4czP7msYZoxvKB4hDpZGw34RgMD4VLA2jt03GMH4wTQPIZxfV3/9yrgMoOW/BDC9Iv4MavA==",
"license": "MIT",
"dependencies": {
-
"@atproto-labs/did-resolver": "0.1.5",
-
"@atproto-labs/handle-resolver-node": "0.1.7",
-
"@atproto-labs/simple-store": "0.1.1",
-
"@atproto/did": "0.1.3",
-
"@atproto/jwk": "0.1.1",
-
"@atproto/jwk-jose": "0.1.2",
-
"@atproto/jwk-webcrypto": "0.1.2",
-
"@atproto/oauth-client": "0.3.2",
-
"@atproto/oauth-types": "0.2.1"
+
"@atproto-labs/did-resolver": "0.1.13",
+
"@atproto-labs/handle-resolver-node": "0.1.16",
+
"@atproto-labs/simple-store": "0.2.0",
+
"@atproto/did": "0.1.5",
+
"@atproto/jwk": "0.3.0",
+
"@atproto/jwk-jose": "0.1.8",
+
"@atproto/jwk-webcrypto": "0.1.8",
+
"@atproto/oauth-client": "0.4.0",
+
"@atproto/oauth-types": "0.3.0"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/oauth-types": {
-
"version": "0.2.1",
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.2.1.tgz",
-
"integrity": "sha512-hDisUXzcq5KU1HMuCYZ8Kcz7BePl7V11bFjjgZvND3mdSphiyBpJ8MCNn3QzAa6cXpFo0w9PDcYMAlCCRZHdVw==",
+
"version": "0.3.0",
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.3.0.tgz",
+
"integrity": "sha512-ptfsJARKODXfuOoDQag4a6PpEkDEj4Urz3jOmnQZy2YspPc/TNm1o0HglU0YehELv1vfhh9gEz40BJztPPhiLA==",
"license": "MIT",
"dependencies": {
-
"@atproto/jwk": "0.1.1",
+
"@atproto/jwk": "0.3.0",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/repo": {
-
"version": "0.5.3",
-
"resolved": "https://registry.npmjs.org/@atproto/repo/-/repo-0.5.3.tgz",
-
"integrity": "sha512-Lbp35SaK5149B9VnE6CVruo/iImNKQ49pPSR+5KuStHDCIyH0z/ynOrEJfpQjTzVu9kdio6bimo5zsl4F2fT2Q==",
+
"version": "0.8.2",
+
"resolved": "https://registry.npmjs.org/@atproto/repo/-/repo-0.8.2.tgz",
+
"integrity": "sha512-lP0g5Uw3TUC2Tc7te8YKCpRoIhBYI+Uvn11fupGEaMcMjgLdYtB0Kc0AiqWXF42KqlBG9dAEoJITi2GRzDNHUg==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/common": "^0.4.4",
-
"@atproto/common-web": "^0.3.1",
-
"@atproto/crypto": "^0.4.1",
-
"@atproto/lexicon": "^0.4.2",
-
"@ipld/car": "^3.2.3",
+
"@atproto/common": "^0.4.11",
+
"@atproto/common-web": "^0.4.2",
+
"@atproto/crypto": "^0.4.4",
+
"@atproto/lexicon": "^0.4.11",
"@ipld/dag-cbor": "^7.0.0",
"multiformats": "^9.9.0",
"uint8arrays": "3.0.0",
+
"varint": "^6.0.0",
"zod": "^3.23.8"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/sync": {
-
"version": "0.1.4",
-
"resolved": "https://registry.npmjs.org/@atproto/sync/-/sync-0.1.4.tgz",
-
"integrity": "sha512-IKF7UKJ78tNhXUpow2/SyQ98UmT9RBZAjBI6n04ssJz1gOTW2XzEvdU0lIfqfpLc++0h7p4GfzzyyUNLgBxd0g==",
+
"version": "0.1.26",
+
"resolved": "https://registry.npmjs.org/@atproto/sync/-/sync-0.1.26.tgz",
+
"integrity": "sha512-bpUIajtPrE3RgFW8mIfrI4EM/LJ4JjQhI5fsqc78zCHZawuflpllf1aH70roDWWiskMWoiLWnVRxdYXdeEgbXA==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/common": "^0.4.4",
-
"@atproto/identity": "^0.4.2",
-
"@atproto/lexicon": "^0.4.2",
-
"@atproto/repo": "^0.5.3",
-
"@atproto/syntax": "^0.3.0",
-
"@atproto/xrpc-server": "^0.7.1",
+
"@atproto/common": "^0.4.11",
+
"@atproto/identity": "^0.4.8",
+
"@atproto/lexicon": "^0.4.11",
+
"@atproto/repo": "^0.8.2",
+
"@atproto/syntax": "^0.4.0",
+
"@atproto/xrpc-server": "^0.8.0",
"multiformats": "^9.9.0",
-
"p-queue": "^6.6.2"
+
"p-queue": "^6.6.2",
+
"ws": "^8.12.0"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/syntax": {
-
"version": "0.3.1",
-
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.1.tgz",
-
"integrity": "sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw==",
+
"version": "0.4.0",
+
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.0.tgz",
+
"integrity": "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==",
"license": "MIT"
},
"node_modules/@atproto/xrpc": {
-
"version": "0.6.4",
-
"resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.4.tgz",
-
"integrity": "sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA==",
+
"version": "0.7.0",
+
"resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.0.tgz",
+
"integrity": "sha512-SfhP9dGx2qclaScFDb58Jnrmim5nk4geZXCqg6sB0I/KZhZEkr9iIx1hLCp+sxkIfEsmEJjeWO4B0rjUIJW5cw==",
"license": "MIT",
"dependencies": {
-
"@atproto/lexicon": "^0.4.3",
+
"@atproto/lexicon": "^0.4.11",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/xrpc-server": {
-
"version": "0.7.9",
-
"resolved": "https://registry.npmjs.org/@atproto/xrpc-server/-/xrpc-server-0.7.9.tgz",
-
"integrity": "sha512-x6CqV6KycIUyZs+J4V+wujc3R98QIkVRU4KmbUgAJ9AtJuTDnOOEbUFrNVVes45UfjJw4ztg021R0M2y0aI3fQ==",
+
"version": "0.8.0",
+
"resolved": "https://registry.npmjs.org/@atproto/xrpc-server/-/xrpc-server-0.8.0.tgz",
+
"integrity": "sha512-jDAEVHVhM4IvC0y491gXBuD4b1D9/XrM3HaEronRneAdNZ0qE0nsiJNqiHfQ6r4BvFdHnABM9KyHV9EQTvmxfg==",
"license": "MIT",
"dependencies": {
-
"@atproto/common": "^0.4.7",
-
"@atproto/crypto": "^0.4.3",
-
"@atproto/lexicon": "^0.4.5",
-
"@atproto/xrpc": "^0.6.7",
+
"@atproto/common": "^0.4.11",
+
"@atproto/crypto": "^0.4.4",
+
"@atproto/lexicon": "^0.4.11",
+
"@atproto/xrpc": "^0.7.0",
"cbor-x": "^1.5.1",
"express": "^4.17.2",
"http-errors": "^2.0.0",
···
"uint8arrays": "3.0.0",
"ws": "^8.12.0",
"zod": "^3.23.8"
-
}
-
},
-
"node_modules/@atproto/xrpc-server/node_modules/@atproto/xrpc": {
-
"version": "0.6.7",
-
"resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.7.tgz",
-
"integrity": "sha512-pbzZIONIskyGKxxG3s2wB7rQ2W1xu3ycfeYhKwk/E/ippeJFVxcof64iSC7f22+7JSKUJcxBeZ1piBB82vLj7g==",
-
"license": "MIT",
-
"dependencies": {
-
"@atproto/lexicon": "^0.4.5",
-
"zod": "^3.23.8"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
···
"node": ">=18"
}
},
-
"node_modules/@ipld/car": {
-
"version": "3.2.4",
-
"resolved": "https://registry.npmjs.org/@ipld/car/-/car-3.2.4.tgz",
-
"integrity": "sha512-rezKd+jk8AsTGOoJKqzfjLJ3WVft7NZNH95f0pfPbicROvzTyvHCNy567HzSUd6gRXZ9im29z5ZEv9Hw49jSYw==",
-
"dependencies": {
-
"@ipld/dag-cbor": "^7.0.0",
-
"multiformats": "^9.5.4",
-
"varint": "^6.0.0"
-
}
-
},
"node_modules/@ipld/dag-cbor": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz",
···
},
"node_modules/@noble/curves": {
-
"version": "1.8.1",
-
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz",
-
"integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
+
"version": "1.9.2",
+
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz",
+
"integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==",
"license": "MIT",
"dependencies": {
-
"@noble/hashes": "1.7.1"
+
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
···
},
"node_modules/@noble/hashes": {
-
"version": "1.7.1",
-
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
-
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
+
"version": "1.8.0",
+
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
···
"node": ">=8"
},
-
"node_modules/asynckit": {
-
"version": "0.4.0",
-
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
-
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
···
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="
-
},
-
"node_modules/axios": {
-
"version": "0.27.2",
-
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
-
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
-
"dependencies": {
-
"follow-redirects": "^1.14.9",
-
"form-data": "^4.0.0"
-
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
···
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true
},
-
"node_modules/combined-stream": {
-
"version": "1.0.8",
-
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-
"dependencies": {
-
"delayed-stream": "~1.0.0"
-
},
-
"engines": {
-
"node": ">= 0.8"
-
}
-
},
"node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
···
"url": "https://github.com/sponsors/ljharb"
},
-
"node_modules/delayed-stream": {
-
"version": "1.0.0",
-
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+
"node_modules/delay": {
+
"version": "5.0.0",
+
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
+
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
+
"license": "MIT",
"engines": {
-
"node": ">=0.4.0"
+
"node": ">=10"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
},
"node_modules/depd": {
···
"node": ">=8.6.0"
},
+
"node_modules/fast-printf": {
+
"version": "1.6.10",
+
"resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz",
+
"integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==",
+
"license": "BSD-3-Clause",
+
"engines": {
+
"node": ">=10.0"
+
}
+
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
···
"node": ">= 0.8"
},
-
"node_modules/follow-redirects": {
-
"version": "1.15.8",
-
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.8.tgz",
-
"integrity": "sha512-xgrmBhBToVKay1q2Tao5LI26B83UhrB/vM1avwVSDzt8rx3rO6AizBAaF46EgksTVr+rFTQaqZZ9MVBfUe4nig==",
-
"funding": [
-
{
-
"type": "individual",
-
"url": "https://github.com/sponsors/RubenVerborgh"
-
}
-
],
-
"engines": {
-
"node": ">=4.0"
-
},
-
"peerDependenciesMeta": {
-
"debug": {
-
"optional": true
-
}
-
}
-
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
···
"url": "https://github.com/sponsors/isaacs"
},
-
"node_modules/form-data": {
-
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-
"dependencies": {
-
"asynckit": "^0.4.0",
-
"combined-stream": "^1.0.8",
-
"mime-types": "^2.1.12"
-
},
-
"engines": {
-
"node": ">= 6"
-
}
-
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
···
},
"engines": {
"node": ">= 0.8"
+
}
+
},
+
"node_modules/http-terminator": {
+
"version": "3.2.0",
+
"resolved": "https://registry.npmjs.org/http-terminator/-/http-terminator-3.2.0.tgz",
+
"integrity": "sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==",
+
"license": "BSD-3-Clause",
+
"dependencies": {
+
"delay": "^5.0.0",
+
"p-wait-for": "^3.2.0",
+
"roarr": "^7.0.4",
+
"type-fest": "^2.3.3"
+
},
+
"engines": {
+
"node": ">=14"
},
"node_modules/human-signals": {
···
"node": ">=8"
},
+
"node_modules/p-wait-for": {
+
"version": "3.2.0",
+
"resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz",
+
"integrity": "sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==",
+
"license": "MIT",
+
"dependencies": {
+
"p-timeout": "^3.0.0"
+
},
+
"engines": {
+
"node": ">=8"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
"node_modules/package-json-from-dist": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
···
"node": ">= 0.10"
},
-
"node_modules/psl": {
-
"version": "1.13.0",
-
"resolved": "https://registry.npmjs.org/psl/-/psl-1.13.0.tgz",
-
"integrity": "sha512-BFwmFXiJoFqlUpZ5Qssolv15DMyc84gTBds1BjsV1BfXEo1UyyD7GsmN67n7J77uRhoSNW1AXtXKPLcBFQn9Aw==",
-
"license": "MIT",
-
"dependencies": {
-
"punycode": "^2.3.1"
-
}
-
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
···
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+
"dev": true,
"engines": {
"node": ">=6"
···
"node_modules/rate-limiter-flexible": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.4.2.tgz",
-
"integrity": "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="
+
"integrity": "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw==",
+
"license": "ISC"
},
"node_modules/raw-body": {
"version": "2.5.2",
···
"url": "https://github.com/sponsors/isaacs"
},
+
"node_modules/roarr": {
+
"version": "7.21.1",
+
"resolved": "https://registry.npmjs.org/roarr/-/roarr-7.21.1.tgz",
+
"integrity": "sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ==",
+
"license": "BSD-3-Clause",
+
"dependencies": {
+
"fast-printf": "^1.6.9",
+
"safe-stable-stringify": "^2.4.3",
+
"semver-compare": "^1.0.0"
+
},
+
"engines": {
+
"node": ">=18.0"
+
}
+
},
"node_modules/rollup": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz",
···
"node": ">=10"
},
+
"node_modules/semver-compare": {
+
"version": "1.0.0",
+
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+
"license": "MIT"
+
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
···
"node": "*"
},
+
"node_modules/type-fest": {
+
"version": "2.19.0",
+
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+
"license": "(MIT OR CC0-1.0)",
+
"engines": {
+
"node": ">=12.20"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
···
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
},
"node_modules/undici": {
-
"version": "6.21.0",
-
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz",
-
"integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==",
+
"version": "6.21.3",
+
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
+
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
···
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
-
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="
+
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
+
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
···
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
-
"version": "8.18.0",
-
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
-
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+
"version": "8.18.2",
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
+
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
+
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
···
},
"node_modules/zod": {
-
"version": "3.23.8",
-
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
-
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+
"version": "3.25.67",
+
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
+
"integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==",
+
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
+12 -9
package.json
···
"clean": "rimraf dist coverage"
},
"dependencies": {
-
"@atproto/api": "^0.13.4",
-
"@atproto/common": "^0.4.1",
-
"@atproto/identity": "^0.4.0",
-
"@atproto/lexicon": "^0.4.2",
-
"@atproto/oauth-client-node": "^0.2.2",
-
"@atproto/sync": "^0.1.4",
-
"@atproto/syntax": "^0.3.0",
-
"@atproto/xrpc-server": "^0.7.9",
+
"@atproto/api": "^0.15.6",
+
"@atproto/common": "^0.4.11",
+
"@atproto/identity": "^0.4.8",
+
"@atproto/jwk-jose": "^0.1.8",
+
"@atproto/lexicon": "^0.4.11",
+
"@atproto/oauth-client-node": "^0.2.24",
+
"@atproto/sync": "^0.1.26",
+
"@atproto/syntax": "^0.4.0",
+
"@atproto/xrpc-server": "^0.8.0",
"better-sqlite3": "^11.1.2",
"dotenv": "^16.4.5",
"envalid": "^8.0.0",
"express": "^4.19.2",
+
"http-terminator": "^3.2.0",
"iron-session": "^8.0.2",
"kysely": "^0.27.4",
"multiformats": "^9.9.0",
"pino": "^9.3.2",
-
"uhtml": "^4.5.9"
+
"uhtml": "^4.5.9",
+
"zod": "^3.25.67"
},
"devDependencies": {
"@atproto/lex-cli": "^0.4.1",
+58 -20
src/auth/client.ts
···
-
import { NodeOAuthClient } from '@atproto/oauth-client-node'
+
import assert from 'node:assert'
+
import { Keyset } from '@atproto/jwk'
+
import { JoseKey } from '@atproto/jwk-jose'
+
import {
+
AppViewHandleResolver,
+
atprotoLoopbackClientMetadata,
+
DidResolverCommon,
+
NodeOAuthClient,
+
OAuthClientMetadataInput,
+
} from '@atproto/oauth-client-node'
+
import type { Database } from '#/db'
-
import { env } from '#/lib/env'
+
import { env } from '#/env'
import { SessionStore, StateStore } from './storage'
-
export const createClient = async (db: Database) => {
-
const publicUrl = env.PUBLIC_URL
-
const url = publicUrl || `http://127.0.0.1:${env.PORT}`
-
const enc = encodeURIComponent
+
export async function createOAuthClient(db: Database) {
+
assert(
+
!env.PUBLIC_URL || env.PRIVATE_JWKS,
+
'ATProto requires backend clients to be confidential',
+
)
+
+
// Confidential client require a keyset accessible on the internet. Non
+
// internet clients (e.g. development) cannot expose a keyset on the internet
+
// so they can't be private..
+
const keyset =
+
env.PUBLIC_URL && env.PRIVATE_JWKS
+
? new Keyset(
+
await Promise.all(
+
env.PRIVATE_JWKS.map((jwk) => JoseKey.fromJWK(jwk)),
+
),
+
)
+
: undefined
+
+
// If a keyset is defined (meaning the client is confidential). Let's make
+
// sure it has a private key for signing. Note: findPrivateKey will throw if
+
// the keyset does no contain a suitable private key.
+
const pk = keyset?.findPrivateKey({ use: 'sig' })
+
+
const clientMetadata: OAuthClientMetadataInput = env.PUBLIC_URL
+
? {
+
client_name: 'Statusphere Example App',
+
client_id: `${env.PUBLIC_URL}/oauth-client-metadata.json`,
+
jwks_uri: `${env.PUBLIC_URL}/.well-known/jwks.json`,
+
redirect_uris: [`${env.PUBLIC_URL}/oauth/callback`],
+
scope: 'atproto transition:generic',
+
grant_types: ['authorization_code', 'refresh_token'],
+
response_types: ['code'],
+
application_type: 'web',
+
token_endpoint_auth_method: pk ? 'private_key_jwt' : 'none',
+
token_endpoint_auth_signing_alg: pk ? pk[1] : undefined,
+
dpop_bound_access_tokens: true,
+
}
+
: atprotoLoopbackClientMetadata(
+
`http://localhost?${new URLSearchParams([
+
['redirect_uri', `http://127.0.0.1:${env.PORT}/oauth/callback`],
+
['scope', `atproto transition:generic`],
+
])}`,
+
)
+
return new NodeOAuthClient({
-
clientMetadata: {
-
client_name: 'AT Protocol Express App',
-
client_id: publicUrl
-
? `${url}/client-metadata.json`
-
: `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc('atproto transition:generic')}`,
-
client_uri: url,
-
redirect_uris: [`${url}/oauth/callback`],
-
scope: 'atproto transition:generic',
-
grant_types: ['authorization_code', 'refresh_token'],
-
response_types: ['code'],
-
application_type: 'web',
-
token_endpoint_auth_method: 'none',
-
dpop_bound_access_tokens: true,
-
},
+
keyset,
+
clientMetadata,
stateStore: new StateStore(db),
sessionStore: new SessionStore(db),
})
+11 -3
src/auth/storage.ts
···
NodeSavedState,
NodeSavedStateStore,
} from '@atproto/oauth-client-node'
-
import type { Database } from '#/db'
+
import { Database } from '#/db'
export class StateStore implements NodeSavedStateStore {
constructor(private db: Database) {}
async get(key: string): Promise<NodeSavedState | undefined> {
-
const result = await this.db.selectFrom('auth_state').selectAll().where('key', '=', key).executeTakeFirst()
+
const result = await this.db
+
.selectFrom('auth_state')
+
.selectAll()
+
.where('key', '=', key)
+
.executeTakeFirst()
if (!result) return
return JSON.parse(result.state) as NodeSavedState
}
···
export class SessionStore implements NodeSavedSessionStore {
constructor(private db: Database) {}
async get(key: string): Promise<NodeSavedSession | undefined> {
-
const result = await this.db.selectFrom('auth_session').selectAll().where('key', '=', key).executeTakeFirst()
+
const result = await this.db
+
.selectFrom('auth_session')
+
.selectAll()
+
.where('key', '=', key)
+
.executeTakeFirst()
if (!result) return
return JSON.parse(result.session) as NodeSavedSession
}
+42
src/context.ts
···
+
import { NodeOAuthClient } from '@atproto/oauth-client-node'
+
import { Firehose } from '@atproto/sync'
+
import { pino } from 'pino'
+
+
import { createOAuthClient } from '#/auth/client'
+
import { Database } from '#/db'
+
import { createDb } from '#/db'
+
import {
+
BidirectionalResolver,
+
createBidirectionalResolver,
+
createIdResolver,
+
} from '#/id-resolver'
+
import { createIngester } from '#/ingester'
+
+
/**
+
* Application state passed to the router and elsewhere
+
*/
+
export type AppContext = {
+
db: Database
+
ingester: Firehose
+
logger: pino.Logger
+
oauthClient: NodeOAuthClient
+
resolver: BidirectionalResolver
+
}
+
+
export async function createAppContext(): Promise<AppContext> {
+
const logger = pino({ name: 'server start' })
+
+
const db = await createDb()
+
const oauthClient = await createOAuthClient(db)
+
const baseIdResolver = createIdResolver()
+
const ingester = createIngester(db, baseIdResolver)
+
const resolver = createBidirectionalResolver(baseIdResolver)
+
+
return {
+
db,
+
ingester,
+
logger,
+
oauthClient,
+
resolver,
+
}
+
}
+17 -10
src/db.ts
···
import SqliteDb from 'better-sqlite3'
import {
Kysely,
-
Migrator,
-
SqliteDialect,
Migration,
MigrationProvider,
+
Migrator,
+
SqliteDialect,
} from 'kysely'
+
+
import { env } from '#/env'
// Types
···
// APIs
-
export const createDb = (location: string): Database => {
-
return new Kysely<DatabaseSchema>({
+
export async function createDb(options?: {
+
/** @default true */
+
migrate?: boolean
+
}): Promise<Database> {
+
const db = new Kysely<DatabaseSchema>({
dialect: new SqliteDialect({
-
database: new SqliteDb(location),
+
database: new SqliteDb(env.DB_PATH),
}),
})
-
}
+
+
if (options?.migrate !== false) {
+
const migrator = new Migrator({ db, provider: migrationProvider })
+
const { error } = await migrator.migrateToLatest()
+
if (error) throw error
+
}
-
export const migrateToLatest = async (db: Database) => {
-
const migrator = new Migrator({ db, provider: migrationProvider })
-
const { error } = await migrator.migrateToLatest()
-
if (error) throw error
+
return db
}
export type Database = Kysely<DatabaseSchema>
-1
src/id-resolver.ts
···
const HOUR = 60e3 * 60
const DAY = HOUR * 24
-
export function createIdResolver() {
return new IdResolver({
didCache: new MemoryCache(HOUR, DAY),
+25 -91
src/index.ts
···
-
import events from 'node:events'
-
import type http from 'node:http'
-
import express, { type Express } from 'express'
-
import { pino } from 'pino'
-
import type { OAuthClient } from '@atproto/oauth-client-node'
-
import { Firehose } from '@atproto/sync'
+
import { createHttpTerminator } from 'http-terminator'
+
import { once } from 'node:events'
-
import { createDb, migrateToLatest } from '#/db'
-
import { env } from '#/lib/env'
-
import { createIngester } from '#/ingester'
+
import { createAppContext } from '#/context'
+
import { env } from '#/env'
+
import { run } from '#/lib/process'
import { createRouter } from '#/routes'
-
import { createClient } from '#/auth/client'
-
import { createBidirectionalResolver, createIdResolver, BidirectionalResolver } from '#/id-resolver'
-
import type { Database } from '#/db'
-
import { IdResolver, MemoryCache } from '@atproto/identity'
-
-
// Application state passed to the router and elsewhere
-
export type AppContext = {
-
db: Database
-
ingester: Firehose
-
logger: pino.Logger
-
oauthClient: OAuthClient
-
resolver: BidirectionalResolver
-
}
-
-
export class Server {
-
constructor(
-
public app: express.Application,
-
public server: http.Server,
-
public ctx: AppContext
-
) {}
-
-
static async create() {
-
const { NODE_ENV, HOST, PORT, DB_PATH } = env
-
const logger = pino({ name: 'server start' })
-
-
// Set up the SQLite database
-
const db = createDb(DB_PATH)
-
await migrateToLatest(db)
-
-
// Create the atproto utilities
-
const oauthClient = await createClient(db)
-
const baseIdResolver = createIdResolver()
-
const ingester = createIngester(db, baseIdResolver)
-
const resolver = createBidirectionalResolver(baseIdResolver)
-
const ctx = {
-
db,
-
ingester,
-
logger,
-
oauthClient,
-
resolver,
-
}
-
-
// Subscribe to events on the firehose
-
ingester.start()
+
import { startServer } from '#/lib/http'
-
// Create our server
-
const app: Express = express()
-
app.set('trust proxy', true)
+
run(async (killSignal) => {
+
// Create the application context
+
const ctx = await createAppContext()
-
// Routes & middlewares
-
const router = createRouter(ctx)
-
app.use(express.json())
-
app.use(express.urlencoded({ extended: true }))
-
app.use(router)
-
app.use((_req, res) => res.sendStatus(404))
+
// Create the HTTP router
+
const router = createRouter(ctx)
-
// Bind our server to the port
-
const server = app.listen(env.PORT)
-
await events.once(server, 'listening')
-
logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`)
-
-
return new Server(app, server, ctx)
-
}
+
// Start the HTTP server
+
const { terminate } = await startServer(router, { port: env.PORT })
-
async close() {
-
this.ctx.logger.info('sigint received, shutting down')
-
await this.ctx.ingester.destroy()
-
return new Promise<void>((resolve) => {
-
this.server.close(() => {
-
this.ctx.logger.info('server closed')
-
resolve()
-
})
-
})
-
}
-
}
+
const url = env.PUBLIC_URL || `http://localhost:${env.PORT}`
+
ctx.logger.info(`Server (${env.NODE_ENV}) running at ${url}`)
-
const run = async () => {
-
const server = await Server.create()
+
// Subscribe to events on the firehose
+
ctx.ingester.start()
-
const onCloseSignal = async () => {
-
setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s
-
await server.close()
-
process.exit()
-
}
+
// Wait for a termination signal
+
if (!killSignal.aborted) await once(killSignal, 'abort')
+
ctx.logger.info(`Signal received, shutting down...`)
-
process.on('SIGINT', onCloseSignal)
-
process.on('SIGTERM', onCloseSignal)
-
}
+
// Gracefully shutdown the http server
+
await terminate()
-
run()
+
// Close the firehose connection
+
await ctx.ingester.destroy()
+
})
+7 -4
src/ingester.ts
···
import pino from 'pino'
import { IdResolver } from '@atproto/identity'
-
import { Firehose } from '@atproto/sync'
+
import { Event, Firehose } from '@atproto/sync'
import type { Database } from '#/db'
import * as Status from '#/lexicon/types/xyz/statusphere/status'
···
const logger = pino({ name: 'firehose ingestion' })
return new Firehose({
idResolver,
-
handleEvent: async (evt) => {
+
handleEvent: async (evt: Event) => {
// Watch for write events
if (evt.event === 'create' || evt.event === 'update') {
const now = new Date()
···
evt.collection === 'xyz.statusphere.status'
) {
// Remove the status from our SQLite
-
await db.deleteFrom('status').where('uri', '=', evt.uri.toString()).execute()
+
await db
+
.deleteFrom('status')
+
.where('uri', '=', evt.uri.toString())
+
.execute()
}
},
-
onError: (err) => {
+
onError: (err: unknown) => {
logger.error({ err }, 'error on firehose ingestion')
},
filterCollections: ['xyz.statusphere.status'],
+3 -2
src/lib/env.ts src/env.ts
···
import dotenv from 'dotenv'
-
import { cleanEnv, host, port, str, testOnly } from 'envalid'
+
import { cleanEnv, port, str, testOnly } from 'envalid'
+
import { privateKeys } from '#/lib/envalid-private-keys'
dotenv.config()
···
devDefault: testOnly('test'),
choices: ['development', 'production', 'test'],
}),
-
HOST: host({ devDefault: testOnly('localhost') }),
PORT: port({ devDefault: testOnly(3000) }),
PUBLIC_URL: str({}),
DB_PATH: str({ devDefault: ':memory:' }),
COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }),
+
PRIVATE_JWKS: privateKeys({ default: undefined }),
})
+17
src/lib/envalid-private-keys.ts
···
+
import { Jwk, jwkValidator } from '@atproto/jwk'
+
import { makeValidator } from 'envalid'
+
import { z } from 'zod'
+
+
export type PrivateKey = Jwk & { kid: string }
+
+
const privateKeySchema = z.intersection(
+
jwkValidator,
+
z.object({ kid: z.string().nonempty() })
+
) satisfies z.ZodType<PrivateKey>
+
+
const privateKeysSchema = z.array(privateKeySchema).nonempty()
+
+
export const privateKeys = makeValidator((input) => {
+
const value = JSON.parse(input)
+
return privateKeysSchema.parse(value)
+
})
+95
src/lib/http.ts
···
+
import { createHttpTerminator } from 'http-terminator'
+
import { once } from 'node:events'
+
import type {
+
IncomingMessage,
+
RequestListener,
+
ServerResponse,
+
} from 'node:http'
+
import { createServer } from 'node:http'
+
+
export type NextFunction = (err?: unknown) => void
+
+
export type Handler<
+
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse<Req> = ServerResponse<Req>,
+
> = (req: Req, res: Res, next: NextFunction) => void
+
+
export type AsyncHandler<
+
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse<Req> = ServerResponse<Req>,
+
> = (req: Req, res: Res, next: NextFunction) => Promise<void>
+
+
// Helper function for defining routes
+
export function handler<
+
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse<Req> = ServerResponse<Req>,
+
>(fn: Handler<Req, Res> | AsyncHandler<Req, Res>): Handler<Req, Res> {
+
return (req, res, next) => {
+
// NodeJS prefers objects over functions for garbage collection,
+
const nextSafe = nextOnce.bind({ next })
+
try {
+
const result = fn(req, res, nextSafe)
+
if (result instanceof Promise) result.catch(nextSafe)
+
} catch (err) {
+
nextSafe(err)
+
}
+
}
+
+
function nextOnce(this: { next: NextFunction | null }, err?: unknown) {
+
const { next } = this
+
this.next = null
+
next?.(err)
+
}
+
}
+
+
export function formHandler<
+
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse<Req & { body: unknown }> = ServerResponse<
+
Req & { body: unknown }
+
>,
+
>(fn: AsyncHandler<Req & { body: unknown }, Res>): Handler<Req, Res> {
+
return handler(async (req, res, next) => {
+
if (req.method !== 'POST') {
+
return void res.writeHead(405).end()
+
}
+
if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') {
+
return void res.writeHead(415).end('Unsupported Media Type')
+
}
+
+
// Read the request payload
+
const chunks: Uint8Array[] = []
+
for await (const chunk of req) chunks.push(chunk)
+
const payload = Buffer.concat(chunks).toString('utf-8')
+
+
// Parse the Form URL-encoded payload
+
const body = payload ? Object.fromEntries(new URLSearchParams(payload)) : {}
+
+
// Define the body property on the request object
+
Object.defineProperty(req, 'body', {
+
value: body,
+
writable: false,
+
enumerable: true,
+
configurable: true,
+
})
+
+
// Call the provided async handler with the modified request
+
return fn(req as Req & { body: unknown }, res, next)
+
})
+
}
+
+
export async function startServer(
+
requestListener: RequestListener,
+
{
+
port,
+
gracefulTerminationTimeout,
+
}: { port?: number; gracefulTerminationTimeout?: number } = {},
+
) {
+
const server = createServer(requestListener)
+
const { terminate } = createHttpTerminator({
+
gracefulTerminationTimeout,
+
server,
+
})
+
server.listen(port)
+
await once(server, 'listening')
+
return { server, terminate }
+
}
+20
src/lib/process.ts
···
+
const SIGNALS = ['SIGINT', 'SIGTERM'] as const
+
+
export async function run<F extends (signal: AbortSignal) => unknown>(
+
fn: F
+
): Promise<Awaited<ReturnType<F>>> {
+
const killController = new AbortController()
+
+
const abort = (signal?: string) => {
+
for (const sig of SIGNALS) process.off(sig, abort)
+
killController.abort(signal)
+
}
+
+
for (const sig of SIGNALS) process.on(sig, abort)
+
+
try {
+
return (await fn(killController.signal)) as Awaited<ReturnType<F>>
+
} finally {
+
abort()
+
}
+
}
+2 -3
src/pages/home.ts
···
'💙',
'🥹',
'😧',
-
'😤',
'🙃',
'😉',
'😎',
···
type Props = {
statuses: Status[]
-
didHandleMap: Record<string, string>
+
didHandleMap: Record<string, string | undefined>
profile?: { displayName?: string }
myStatus?: Status
}
···
value="${status}"
>
${status}
-
</button>`
+
</button>`,
)}
</form>
${statuses.map((status, i) => {
+79 -77
src/routes.ts
···
-
import assert from 'node:assert'
-
import path from 'node:path'
-
import type { IncomingMessage, ServerResponse } from 'node:http'
+
import { Agent } from '@atproto/api'
+
import { TID } from '@atproto/common'
import { OAuthResolverError } from '@atproto/oauth-client-node'
import { isValidHandle } from '@atproto/syntax'
-
import { TID } from '@atproto/common'
-
import { Agent } from '@atproto/api'
-
import express from 'express'
+
import express, { Request, Response } from 'express'
import { getIronSession } from 'iron-session'
-
import type { AppContext } from '#/index'
+
import assert from 'node:assert'
+
import type { IncomingMessage, ServerResponse } from 'node:http'
+
import path from 'node:path'
+
+
import type { AppContext } from '#/context'
+
import * as Profile from '#/lexicon/types/app/bsky/actor/profile'
+
import * as Status from '#/lexicon/types/xyz/statusphere/status'
+
import { env } from '#/env'
+
import { formHandler, handler } from '#/lib/http'
+
import { page } from '#/lib/view'
import { home } from '#/pages/home'
import { login } from '#/pages/login'
-
import { env } from '#/lib/env'
-
import { page } from '#/lib/view'
-
import * as Status from '#/lexicon/types/xyz/statusphere/status'
-
import * as Profile from '#/lexicon/types/app/bsky/actor/profile'
type Session = { did: string }
-
-
// Helper function for defining routes
-
const handler =
-
(fn: express.Handler) =>
-
async (
-
req: express.Request,
-
res: express.Response,
-
next: express.NextFunction
-
) => {
-
try {
-
await fn(req, res, next)
-
} catch (err) {
-
next(err)
-
}
-
}
// Helper function to get the Atproto Agent for the active session
async function getSessionAgent(
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>,
-
ctx: AppContext
+
res: ServerResponse,
+
ctx: AppContext,
) {
const session = await getIronSession<Session>(req, res, {
cookieName: 'sid',
···
}
export const createRouter = (ctx: AppContext) => {
-
const router = express.Router()
+
const app = express()
// Static assets
-
router.use('/public', express.static(path.join(__dirname, 'pages', 'public')))
+
app.use('/public', express.static(path.join(__dirname, 'pages', 'public')))
// OAuth metadata
-
router.get(
-
'/client-metadata.json',
-
handler((_req, res) => {
-
return res.json(ctx.oauthClient.clientMetadata)
-
})
+
app.get(
+
'/oauth-client-metadata.json',
+
handler(async (req: Request, res: Response) => {
+
res.json(ctx.oauthClient.clientMetadata)
+
}),
+
)
+
+
// Public keys
+
app.get(
+
'/.well-known/jwks.json',
+
handler(async (req: Request, res: Response) => {
+
res.json(ctx.oauthClient.jwks)
+
}),
)
// OAuth callback to complete session creation
-
router.get(
+
app.get(
'/oauth/callback',
-
handler(async (req, res) => {
+
handler(async (req: Request, res: Response) => {
const params = new URLSearchParams(req.originalUrl.split('?')[1])
try {
const { session } = await ctx.oauthClient.callback(params)
···
assert(!clientSession.did, 'session already exists')
clientSession.did = session.did
await clientSession.save()
+
return res.redirect('/')
} catch (err) {
ctx.logger.error({ err }, 'oauth callback failed')
return res.redirect('/?error')
}
-
return res.redirect('/')
-
})
+
}),
)
// Login page
-
router.get(
+
app.get(
'/login',
-
handler(async (_req, res) => {
-
return res.type('html').send(page(login({})))
-
})
+
handler(async (req: Request, res: Response) => {
+
res.type('html').send(page(login({})))
+
}),
)
// Login handler
-
router.post(
+
app.post(
'/login',
-
handler(async (req, res) => {
+
formHandler(async (req: Request, res: Response) => {
// Validate
const handle = req.body?.handle
if (typeof handle !== 'string' || !isValidHandle(handle)) {
-
return res.type('html').send(page(login({ error: 'invalid handle' })))
+
return void res
+
.type('html')
+
.send(page(login({ error: 'invalid handle' })))
}
// Initiate the OAuth flow
···
const url = await ctx.oauthClient.authorize(handle, {
scope: 'atproto transition:generic',
})
-
return res.redirect(url.toString())
+
res.redirect(url.toString())
} catch (err) {
ctx.logger.error({ err }, 'oauth authorize failed')
-
return res.type('html').send(
+
res.type('html').send(
page(
login({
error:
err instanceof OAuthResolverError
? err.message
: "couldn't initiate login",
-
})
-
)
+
}),
+
),
)
}
-
})
+
}),
)
// Logout handler
-
router.post(
+
app.post(
'/logout',
-
handler(async (req, res) => {
+
formHandler(async (req: Request, res: Response) => {
const session = await getIronSession<Session>(req, res, {
cookieName: 'sid',
password: env.COOKIE_SECRET,
})
await session.destroy()
return res.redirect('/')
-
})
+
}),
)
// Homepage
-
router.get(
+
app.get(
'/',
-
handler(async (req, res) => {
+
handler(async (req: Request, res: Response) => {
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
···
// Map user DIDs to their domain-name handles
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
-
statuses.map((s) => s.authorDid)
+
statuses.map((s) => s.authorDid),
)
if (!agent) {
// Serve the logged-out view
-
return res.type('html').send(page(home({ statuses, didHandleMap })))
+
return void res
+
.type('html')
+
.send(page(home({ statuses, didHandleMap })))
}
// Fetch additional information about the logged-in user
-
const profileResponse = await agent.com.atproto.repo.getRecord({
-
repo: agent.assertDid,
-
collection: 'app.bsky.actor.profile',
-
rkey: 'self',
-
}).catch(() => undefined);
+
const profileResponse = await agent.com.atproto.repo
+
.getRecord({
+
repo: agent.assertDid,
+
collection: 'app.bsky.actor.profile',
+
rkey: 'self',
+
})
+
.catch(() => undefined)
-
const profileRecord = profileResponse?.data;
+
const profileRecord = profileResponse?.data
-
const profile = profileRecord &&
+
const profile =
+
profileRecord &&
Profile.isRecord(profileRecord.value) &&
Profile.validateRecord(profileRecord.value).success
? profileRecord.value
: {}
// Serve the logged-in view
-
return res.type('html').send(
+
res.type('html').send(
page(
home({
statuses,
didHandleMap,
profile,
myStatus,
-
})
-
)
+
}),
+
),
)
-
})
+
}),
)
// "Set status" handler
-
router.post(
+
app.post(
'/status',
-
handler(async (req, res) => {
+
formHandler(async (req: Request, res: Response) => {
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
if (!agent) {
-
return res
+
return void res
.status(401)
.type('html')
.send('<h1>Error: Session required</h1>')
···
createdAt: new Date().toISOString(),
}
if (!Status.validateRecord(record).success) {
-
return res
+
return void res
.status(400)
.type('html')
.send('<h1>Error: Invalid status</h1>')
···
uri = res.data.uri
} catch (err) {
ctx.logger.warn({ err }, 'failed to write record')
-
return res
+
return void res
.status(500)
.type('html')
.send('<h1>Error: Failed to write record</h1>')
···
} catch (err) {
ctx.logger.warn(
{ err },
-
'failed to update computed view; ignoring as it should be caught by the firehose'
+
'failed to update computed view; ignoring as it should be caught by the firehose',
)
}
return res.redirect('/')
-
})
+
}),
)
-
return router
+
return app
}