Scratch space for learning atproto app development

Compare changes

Choose any two refs to compare.

+11 -10
.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.
+
NODE_ENV="development" # Options: 'development', 'production'
+
PORT="8080" # The port your server will listen on
+
DB_PATH=":memory:" # The SQLite database path. Set as ":memory:" to use a temporary in-memory database.
+
# PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id.
+
# LOG_LEVEL="info" # Options: 'fatal', 'error', 'warn', 'info', 'debug'
+
# PDS_URL="https://my.pds" # The default PDS for login and sign-ups
-
# Rate Limiting
-
COMMON_RATE_LIMIT_WINDOW_MS="1000" # Window size for rate limiting (ms)
-
COMMON_RATE_LIMIT_MAX_REQUESTS="20" # Max number of requests per window per IP
+
# Secrets below *MUST* be set in production
-
# Secrets
-
# Must set this in production. May be generated with `openssl rand -base64 33`
+
# May be generated with `openssl rand -base64 33`
# COOKIE_SECRET=""
+
+
# May be generated with `./bin/gen-jwk` (requires `npm install` once first)
+
# PRIVATE_KEYS='[{"kty":"EC","kid":"123",...}]'
+1
.gitignore
···
dist-ssr
*.local
.env
+
*.sqlite
# Editor directories and files
!.vscode/extensions.json
+7
.prettierrc
···
+
{
+
"trailingComma": "all",
+
"tabWidth": 2,
+
"semi": false,
+
"singleQuote": true,
+
"useTabs": false
+
}
+1 -1
.vscode/settings.json
···
{
"editor.formatOnSave": true,
-
"editor.defaultFormatter": "biomejs.biome",
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit",
+21
LICENSE
···
+
MIT License
+
+
Copyright (c) 2025 Bluesky PBC, and Contributors
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
SOFTWARE.
+56 -8
README.md
···
-
# AT Protocol Express App
+
# AT Protocol "Statusphere" Example App
-
A demo application covering:
-
- public firehose ingestion
-
- identity and login with OAuth
-
- writing to the network
+
An example application covering:
+
+
- Signin via OAuth
+
- Fetch information about users (profiles)
+
- Listen to the network firehose for new data
+
- Publish data on the user's account using a custom schema
+
+
See https://atproto.com/guides/applications for a guide through the codebase.
## Getting Started
-
### Development
+
```sh
-
pnpm i
+
git clone https://github.com/bluesky-social/statusphere-example-app.git
+
cd statusphere-example-app
cp .env.template .env
-
pnpm run dev
+
npm install
+
npm run dev
# Navigate to http://localhost:8080
```
+
+
## Deploying
+
+
In production, you will need a private key to sign OAuth tokens request. Use the
+
following command to generate a new private key:
+
+
```sh
+
./bin/gen-jwk
+
```
+
+
The generated key must be added to the environment variables (`.env` file) in `PRIVATE_KEYS`.
+
+
```env
+
PRIVATE_KEYS='[{"kty":"EC","kid":"12",...}]'
+
```
+
+
> [!NOTE]
+
>
+
> The `PRIVATE_KEYS` can contain multiple keys. The first key in the array is
+
> the most recent one, and it will be used to sign new tokens. When a key is
+
> removed, all associated sessions will be invalidated.
+
+
Make sure to also set the `COOKIE_SECRET`, which is used to sign session
+
cookies, in your environment variables (`.env` file). You should use a random
+
string for this:
+
+
```sh
+
openssl rand -base64 33
+
```
+
+
Finally, set the `PUBLIC_URL` to the URL where your app will be accessible. This
+
will allow the authorization servers to download the app's public keys.
+
+
```env
+
PUBLIC_URL="https://your-app-url.com"
+
```
+
+
> [!NOTE]
+
>
+
> You can use services like [ngrok](https://ngrok.com/) to expose your local
+
> server to the internet for testing purposes. Just set the `PUBLIC_URL` to the
+
> ngrok URL.
+15
bin/gen-jwk
···
+
#!/usr/bin/env node
+
+
'use strict'
+
+
const { JoseKey } = require('@atproto/oauth-client-node')
+
+
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()
+156
lexicons/defs.json
···
+
{
+
"lexicon": 1,
+
"id": "com.atproto.label.defs",
+
"defs": {
+
"label": {
+
"type": "object",
+
"description": "Metadata tag on an atproto resource (eg, repo or record).",
+
"required": ["src", "uri", "val", "cts"],
+
"properties": {
+
"ver": {
+
"type": "integer",
+
"description": "The AT Protocol version of the label object."
+
},
+
"src": {
+
"type": "string",
+
"format": "did",
+
"description": "DID of the actor who created this label."
+
},
+
"uri": {
+
"type": "string",
+
"format": "uri",
+
"description": "AT URI of the record, repository (account), or other resource that this label applies to."
+
},
+
"cid": {
+
"type": "string",
+
"format": "cid",
+
"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
+
},
+
"val": {
+
"type": "string",
+
"maxLength": 128,
+
"description": "The short string name of the value or type of this label."
+
},
+
"neg": {
+
"type": "boolean",
+
"description": "If true, this is a negation label, overwriting a previous label."
+
},
+
"cts": {
+
"type": "string",
+
"format": "datetime",
+
"description": "Timestamp when this label was created."
+
},
+
"exp": {
+
"type": "string",
+
"format": "datetime",
+
"description": "Timestamp at which this label expires (no longer applies)."
+
},
+
"sig": {
+
"type": "bytes",
+
"description": "Signature of dag-cbor encoded label."
+
}
+
}
+
},
+
"selfLabels": {
+
"type": "object",
+
"description": "Metadata tags on an atproto record, published by the author within the record.",
+
"required": ["values"],
+
"properties": {
+
"values": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "#selfLabel" },
+
"maxLength": 10
+
}
+
}
+
},
+
"selfLabel": {
+
"type": "object",
+
"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
+
"required": ["val"],
+
"properties": {
+
"val": {
+
"type": "string",
+
"maxLength": 128,
+
"description": "The short string name of the value or type of this label."
+
}
+
}
+
},
+
"labelValueDefinition": {
+
"type": "object",
+
"description": "Declares a label value and its expected interpretations and behaviors.",
+
"required": ["identifier", "severity", "blurs", "locales"],
+
"properties": {
+
"identifier": {
+
"type": "string",
+
"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
+
"maxLength": 100,
+
"maxGraphemes": 100
+
},
+
"severity": {
+
"type": "string",
+
"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
+
"knownValues": ["inform", "alert", "none"]
+
},
+
"blurs": {
+
"type": "string",
+
"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
+
"knownValues": ["content", "media", "none"]
+
},
+
"defaultSetting": {
+
"type": "string",
+
"description": "The default setting for this label.",
+
"knownValues": ["ignore", "warn", "hide"],
+
"default": "warn"
+
},
+
"adultOnly": {
+
"type": "boolean",
+
"description": "Does the user need to have adult content enabled in order to configure this label?"
+
},
+
"locales": {
+
"type": "array",
+
"items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
+
}
+
}
+
},
+
"labelValueDefinitionStrings": {
+
"type": "object",
+
"description": "Strings which describe the label in the UI, localized into a specific language.",
+
"required": ["lang", "name", "description"],
+
"properties": {
+
"lang": {
+
"type": "string",
+
"description": "The code of the language these strings are written in.",
+
"format": "language"
+
},
+
"name": {
+
"type": "string",
+
"description": "A short human-readable name for the label.",
+
"maxGraphemes": 64,
+
"maxLength": 640
+
},
+
"description": {
+
"type": "string",
+
"description": "A longer description of what the label means and why it might be applied.",
+
"maxGraphemes": 10000,
+
"maxLength": 100000
+
}
+
}
+
},
+
"labelValue": {
+
"type": "string",
+
"knownValues": [
+
"!hide",
+
"!no-promote",
+
"!warn",
+
"!no-unauthenticated",
+
"dmca-violation",
+
"doxxing",
+
"porn",
+
"sexual",
+
"nudity",
+
"nsfl",
+
"gore"
+
]
+
}
+
}
+
}
+49
lexicons/profile.json
···
+
{
+
"lexicon": 1,
+
"id": "app.bsky.actor.profile",
+
"defs": {
+
"main": {
+
"type": "record",
+
"description": "A declaration of a Bluesky account profile.",
+
"key": "literal:self",
+
"record": {
+
"type": "object",
+
"properties": {
+
"displayName": {
+
"type": "string",
+
"maxGraphemes": 64,
+
"maxLength": 640
+
},
+
"description": {
+
"type": "string",
+
"description": "Free-form profile description text.",
+
"maxGraphemes": 256,
+
"maxLength": 2560
+
},
+
"avatar": {
+
"type": "blob",
+
"description": "Small image to be displayed next to posts from account. AKA, 'profile picture'",
+
"accept": ["image/png", "image/jpeg"],
+
"maxSize": 1000000
+
},
+
"banner": {
+
"type": "blob",
+
"description": "Larger horizontal image to display behind profile view.",
+
"accept": ["image/png", "image/jpeg"],
+
"maxSize": 1000000
+
},
+
"labels": {
+
"type": "union",
+
"description": "Self-label values, specific to the Bluesky application, on the overall account.",
+
"refs": ["com.atproto.label.defs#selfLabels"]
+
},
+
"joinedViaStarterPack": {
+
"type": "ref",
+
"ref": "com.atproto.repo.strongRef"
+
},
+
"createdAt": { "type": "string", "format": "datetime" }
+
}
+
}
+
}
+
}
+
}
+4 -4
lexicons/status.json
···
{
"lexicon": 1,
-
"id": "com.example.status",
+
"id": "xyz.statusphere.status",
"defs": {
"main": {
"type": "record",
-
"key": "literal:self",
+
"key": "tid",
"record": {
"type": "object",
-
"required": ["status", "updatedAt"],
+
"required": ["status", "createdAt"],
"properties": {
"status": {
"type": "string",
···
"maxGraphemes": 1,
"maxLength": 32
},
-
"updatedAt": { "type": "string", "format": "datetime" }
+
"createdAt": { "type": "string", "format": "datetime" }
}
}
}
+15
lexicons/strongRef.json
···
+
{
+
"lexicon": 1,
+
"id": "com.atproto.repo.strongRef",
+
"description": "A URI with a content-hash fingerprint.",
+
"defs": {
+
"main": {
+
"type": "object",
+
"required": ["uri", "cid"],
+
"properties": {
+
"uri": { "type": "string", "format": "at-uri" },
+
"cid": { "type": "string", "format": "cid" }
+
}
+
}
+
}
+
}
+1279 -697
package-lock.json
···
"version": "0.0.1",
"license": "MIT",
"dependencies": {
-
"@atproto/identity": "^0.4.0",
-
"@atproto/jwk-jose": "0.1.2-rc.0",
-
"@atproto/lexicon": "0.4.1-rc.0",
-
"@atproto/oauth-client-node": "0.0.2-rc.2",
-
"@atproto/repo": "0.4.2-rc.0",
-
"@atproto/syntax": "^0.3.0",
-
"@atproto/xrpc-server": "0.5.4-rc.0",
+
"@atproto/api": "^0.15.6",
+
"@atproto/common": "^0.4.11",
+
"@atproto/identity": "^0.4.8",
+
"@atproto/lexicon": "^0.4.11",
+
"@atproto/oauth-client-node": "^0.3.1",
+
"@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",
-
"pino-http": "^10.0.0",
-
"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.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.1.2-rc.0.tgz",
-
"integrity": "sha512-5lVxhLG9P1G1XjGXQr7fhk6mBM5vpbCalrfuVXqU5xQADvObLjEtpxpJuLheAacaV2pUMFDml+53ZLYWXCgFIg==",
+
"version": "0.2.0",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.0.tgz",
+
"integrity": "sha512-y9GOx2gUETynDKmANnBrU5DTf+u0AwKBJpGns1vDDOYMdLdRCFIeYy3UH+TI8YOkcEazjgF5Q3m+LjwriE1KqQ==",
+
"license": "MIT",
"dependencies": {
-
"@atproto-labs/fetch": "0.1.0",
-
"@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.1-rc.0",
+
"@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.0",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.1.0.tgz",
-
"integrity": "sha512-uirja+uA/C4HNk7vayM+AJqsccxQn2wVziUHxbsjJGt/K6Q8ZOKDaEX2+GrcXvpUVcqUKh+94JFjuzH+CAEUlg==",
+
"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.0",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/fetch-node/-/fetch-node-0.1.0.tgz",
-
"integrity": "sha512-DUHgaGw8LBqiGg51pUDuWK/alMcmNbpcK7ALzlF2Gw//TNLTsgrj0qY9aEtK+np9rEC+x/o3bN4SGnuQEpgqIg==",
+
"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.0",
-
"@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": {
+
"version": "2.2.0",
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
+
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+
"license": "MIT",
+
"engines": {
+
"node": ">= 10"
}
},
"node_modules/@atproto-labs/handle-resolver": {
-
"version": "0.1.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.1.2-rc.0.tgz",
-
"integrity": "sha512-sxk/Zr1hWyBBcg1HhZ8N/Tw1Iue/6+V6bzu2c8zYhO9VfKgCBp3FFU1/i3MpgR2AlsEqZpcjv6zj4KAnMHiLUg==",
+
"version": "0.3.0",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.0.tgz",
+
"integrity": "sha512-TREelvXB6P2eHxx6QjINRkBzUZu/aXWrdY9iN57shQe3C8rzsHNEHHuTVvRa33Hc7vFdQbZN0TnCgKveoyiL/A==",
+
"license": "MIT",
"dependencies": {
-
"@atproto-labs/simple-store": "0.1.1",
-
"@atproto-labs/simple-store-memory": "0.1.1",
-
"@atproto/did": "0.1.1-rc.0",
+
"@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.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver-node/-/handle-resolver-node-0.1.2-rc.0.tgz",
-
"integrity": "sha512-wP1c0fqxdhnIQVxFgD3Z6fiToq1ri9ECTCSPoy/1zbNJ+KWrr0V6BSONF/I5MytEbQaICBh8bvZuurvX0OjbNw==",
+
"version": "0.1.18",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver-node/-/handle-resolver-node-0.1.18.tgz",
+
"integrity": "sha512-/qo14c3I+kagT1UWSp3lTIzwDetfkxvF3Y3VlX2NyQ2jHwgtIAJ81KFNqe7t82NpQDjWiM5h4bdjvdbFIh5djQ==",
+
"license": "MIT",
"dependencies": {
-
"@atproto-labs/fetch-node": "0.1.0",
-
"@atproto-labs/handle-resolver": "0.1.2-rc.0",
-
"@atproto/did": "0.1.1-rc.0"
+
"@atproto-labs/fetch-node": "0.1.9",
+
"@atproto-labs/handle-resolver": "0.3.0",
+
"@atproto/did": "0.1.5"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto-labs/identity-resolver": {
-
"version": "0.1.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.1.2-rc.0.tgz",
-
"integrity": "sha512-4TLjNRbufeGduac3c/No4teJ411qNgyBQck7eY5e2K8XrzS2a/xX/bq3JP91DrvERHiP3yE22PB6ATQkuALgXA==",
+
"version": "0.2.0",
+
"resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.2.0.tgz",
+
"integrity": "sha512-X4UpU9qSgbuBVRXw0kpYqdVRtjNGezmaetyQIwWHNdUl1+ILu4GhinSk1MBXamzgg/07/BVCU0r4LRIPg2Wiow==",
+
"license": "MIT",
"dependencies": {
-
"@atproto-labs/did-resolver": "0.1.2-rc.0",
-
"@atproto-labs/handle-resolver": "0.1.2-rc.0",
-
"@atproto/syntax": "0.3.0"
+
"@atproto-labs/did-resolver": "0.2.0",
+
"@atproto-labs/handle-resolver": "0.3.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.0-rc.1",
-
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.0-rc.1.tgz",
-
"integrity": "sha512-h2+M6OoMLnNzqf2KDxsbRkg3/1k2IMWH33PQI31GkiQHIdt3B+MIXvJwXePu0KnMUL/Lvv2Zk01BKiDnjd4LEw==",
+
"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-rc.0",
-
"@atproto/syntax": "^0.3.0",
-
"@atproto/xrpc": "^0.6.0-rc.0",
+
"@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.1",
-
"resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.4.1.tgz",
-
"integrity": "sha512-uL7kQIcBTbvkBDNfxMXL6lBH4fO2DQpHd2BryJxMtbw/4iEPKe9xBYApwECHhEIk9+zhhpTRZ15FJ3gxTXN82Q==",
+
"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.0",
+
"@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.0",
-
"resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.0.tgz",
-
"integrity": "sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==",
+
"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",
"multiformats": "^9.9.0",
"uint8arrays": "3.0.0",
-
"zod": "^3.21.4"
+
"zod": "^3.23.8"
}
},
"node_modules/@atproto/common/node_modules/pino": {
···
}
},
"node_modules/@atproto/crypto": {
-
"version": "0.4.0",
-
"resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.0.tgz",
-
"integrity": "sha512-Kj/4VgJ7hzzXvE42L0rjzP6lM0tai+OfPnP1rxJ+UZg/YUDtuewL4uapnVoWXvlNceKgaLZH98g5n9gXBVTe5Q==",
+
"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.1.0",
-
"@noble/hashes": "^1.3.1",
+
"@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.1-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.1.1-rc.0.tgz",
-
"integrity": "sha512-rbO6kQv/bKsMGqAqr1M4o7cmJf893gYzabr1CmJ0rr/FNdXHfr0b9s2lRphA6zCS0wPdT4/mw6/LWiCrnBmi9w==",
+
"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.0",
-
"resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.0.tgz",
-
"integrity": "sha512-KKdVlqBgkFuTUx3KFiiQe0LuK9kopej1bhKm6SHRPEYbSEPFmRZQMY9TAjWJQrvQt8DpQzz6kVGjASFEjd3teQ==",
+
"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.0",
-
"@atproto/crypto": "^0.4.0",
-
"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.4.0",
+
"resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.4.0.tgz",
+
"integrity": "sha512-tvp4iZrzqEzKCeTOKz50/o6WdsZzOuWmWjF6On5QAp04fLwLpsFu2Hixgx/lA1KBO0O4sns7YSGcAqSSX6Rdog==",
+
"license": "MIT",
"dependencies": {
"multiformats": "^9.9.0",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/jwk-jose": {
-
"version": "0.1.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.2-rc.0.tgz",
-
"integrity": "sha512-guqGhgQjOx6OxxDWBENRa30G3CJ91Rqw+5NEwiv4GfhmmM/szS983kZIydmXpySpyyZhGAPZfkOfHai+HrLsXg==",
+
"version": "0.1.9",
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.9.tgz",
+
"integrity": "sha512-HT9GcUe6htDxI5OSYXWdeS6QZ9lpuDDvJk508ppi8a48E/1f8eumoM0QhgbFRF9IKAnnFrtnZDOAvljQzFKwwQ==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/jwk": "0.1.1",
+
"@atproto/jwk": "0.4.0",
"jose": "^5.2.0"
}
},
"node_modules/@atproto/jwk-webcrypto": {
-
"version": "0.1.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.2-rc.0.tgz",
-
"integrity": "sha512-TlLaJulKDWDhXQ8Wujte4l2RPe/Ym+jAnFR/+lwZbcGQHAUsatBMCKzvYVv3TtqXL3B5gIC9ry12+C7oQ5yE/Q==",
+
"version": "0.1.9",
+
"resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.9.tgz",
+
"integrity": "sha512-ecciePHT0JEDZNAbMKSkdqoBYsjvhwuVno0jsS600SZmuvi2fAMhGraDZ5ZOO5M0hHHBiDbN7Ar/qcnIwyoxsA==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/jwk": "0.1.1",
-
"@atproto/jwk-jose": "0.1.2-rc.0"
+
"@atproto/jwk": "0.4.0",
+
"@atproto/jwk-jose": "0.1.9",
+
"zod": "^3.23.8"
}
},
"node_modules/@atproto/lex-cli": {
···
"lex": "dist/index.js"
}
},
-
"node_modules/@atproto/lex-cli/node_modules/@atproto/lexicon": {
-
"version": "0.4.1",
-
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.1.tgz",
-
"integrity": "sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA==",
+
"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,
-
"dependencies": {
-
"@atproto/common-web": "^0.3.0",
-
"@atproto/syntax": "^0.3.0",
-
"iso-datestring-validator": "^2.2.2",
-
"multiformats": "^9.9.0",
-
"zod": "^3.23.8"
-
}
+
"license": "MIT"
},
"node_modules/@atproto/lexicon": {
-
"version": "0.4.1-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.1-rc.0.tgz",
-
"integrity": "sha512-CSYO8MWbxTXTLQMEJ1mTXD2pDxIXO2oCK/FVw9T/BeXLMcvwmeVgKAaytd1AGFkapX8IMAAtjBB3cnaltuHwbg==",
+
"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.0",
-
"@atproto/syntax": "^0.3.0",
+
"@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.1.2-rc.2",
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.1.2-rc.2.tgz",
-
"integrity": "sha512-FBYyEKEU1BFoW1ASFzsmw1oOpVPj/nkoR753OZItgNwl9i+Tr4kAA9TqeXGa6Ol3dh7K67oaxHw7DChdEqbtSg==",
+
"version": "0.4.2",
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.4.2.tgz",
+
"integrity": "sha512-wHRYcrh+iKQvMramYqE6PHs5Y/L2LYFzrEnyUMf83CjD3GYFwbSN5pwot6EFXONxRwuRjxpXsCSlFzZwx9YFvw==",
+
"license": "MIT",
"dependencies": {
-
"@atproto-labs/did-resolver": "0.1.2-rc.0",
-
"@atproto-labs/fetch": "0.1.0",
-
"@atproto-labs/handle-resolver": "0.1.2-rc.0",
-
"@atproto-labs/identity-resolver": "0.1.2-rc.0",
-
"@atproto-labs/simple-store": "0.1.1",
-
"@atproto-labs/simple-store-memory": "0.1.1",
-
"@atproto/api": "0.13.0-rc.1",
-
"@atproto/did": "0.1.1-rc.0",
-
"@atproto/jwk": "0.1.1",
-
"@atproto/oauth-types": "0.1.2-rc.0",
-
"@atproto/xrpc": "0.6.0-rc.0",
+
"@atproto-labs/did-resolver": "0.2.0",
+
"@atproto-labs/fetch": "0.2.3",
+
"@atproto-labs/handle-resolver": "0.3.0",
+
"@atproto-labs/identity-resolver": "0.2.0",
+
"@atproto-labs/simple-store": "0.2.0",
+
"@atproto-labs/simple-store-memory": "0.1.3",
+
"@atproto/did": "0.1.5",
+
"@atproto/jwk": "0.4.0",
+
"@atproto/oauth-types": "0.3.1",
+
"@atproto/xrpc": "0.7.0",
"multiformats": "^9.9.0",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/oauth-client-node": {
-
"version": "0.0.2-rc.2",
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-client-node/-/oauth-client-node-0.0.2-rc.2.tgz",
-
"integrity": "sha512-MxR2C84h6XjTB28RpXfctKLvB6Ot68tiOlsOSigeSTKnNJ5SRD2wISz2647P8dxOec81ugMu8wa5BKcZ5Ry7nw==",
+
"version": "0.3.1",
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-client-node/-/oauth-client-node-0.3.1.tgz",
+
"integrity": "sha512-k37YC7Ke4+btX05oAqHqkkM8r2Ya/tssWANx7/GMwN3PXPP5PK1C/pkxJrGsN/hpjn3I4W9lVTOlC7nigEX7sw==",
+
"license": "MIT",
"dependencies": {
-
"@atproto-labs/did-resolver": "0.1.2-rc.0",
-
"@atproto-labs/handle-resolver-node": "0.1.2-rc.0",
-
"@atproto-labs/simple-store": "0.1.1",
-
"@atproto/did": "0.1.1-rc.0",
-
"@atproto/jwk": "0.1.1",
-
"@atproto/jwk-jose": "0.1.2-rc.0",
-
"@atproto/jwk-webcrypto": "0.1.2-rc.0",
-
"@atproto/oauth-client": "0.1.2-rc.2",
-
"@atproto/oauth-types": "0.1.2-rc.0"
+
"@atproto-labs/did-resolver": "0.2.0",
+
"@atproto-labs/handle-resolver-node": "0.1.18",
+
"@atproto-labs/simple-store": "0.2.0",
+
"@atproto/did": "0.1.5",
+
"@atproto/jwk": "0.4.0",
+
"@atproto/jwk-jose": "0.1.9",
+
"@atproto/jwk-webcrypto": "0.1.9",
+
"@atproto/oauth-client": "0.4.2",
+
"@atproto/oauth-types": "0.3.1"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/oauth-types": {
-
"version": "0.1.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.1.2-rc.0.tgz",
-
"integrity": "sha512-q/AxPSdLf2xTgC4K1cU35HVl6T4T0LJ/QJmvqXwjpbiNWEqooIQIP9sTp2CqqSLsWpe26z3fIoA3R+oTR1EJsA==",
+
"version": "0.3.1",
+
"resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.3.1.tgz",
+
"integrity": "sha512-l8ahtm74lmBOs5boi5q7mqzF2D37+cIYqVmbCrpexNeJfg2BXu0sBxREt0ADxP25Td9pX+u6FnefCOQtI/YAZw==",
+
"license": "MIT",
"dependencies": {
-
"@atproto/jwk": "0.1.1",
+
"@atproto/jwk": "0.4.0",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/repo": {
-
"version": "0.4.2-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/repo/-/repo-0.4.2-rc.0.tgz",
-
"integrity": "sha512-y8zXAR23r6qlsTmbzXaBEHYjvlgeNlAKj9eJ6V17JtT+4FVdW246alhsgSsglJ2Uv/e24RC1r90yNJNRxqDzXw==",
+
"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.1",
-
"@atproto/common-web": "^0.3.0",
-
"@atproto/crypto": "^0.4.0",
-
"@atproto/lexicon": "^0.4.1-rc.0",
-
"@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.26",
+
"resolved": "https://registry.npmjs.org/@atproto/sync/-/sync-0.1.26.tgz",
+
"integrity": "sha512-bpUIajtPrE3RgFW8mIfrI4EM/LJ4JjQhI5fsqc78zCHZawuflpllf1aH70roDWWiskMWoiLWnVRxdYXdeEgbXA==",
+
"license": "MIT",
+
"dependencies": {
+
"@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",
+
"ws": "^8.12.0"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@atproto/syntax": {
-
"version": "0.3.0",
-
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz",
-
"integrity": "sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA=="
+
"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.0-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.0-rc.0.tgz",
-
"integrity": "sha512-TOmynXvbA57Y6KR050UeiDfdzQoAnmgB0zu0qrvhYiu7oeg64fYzvOa7stWxSIP1nhrGqgexxICR1CnOnCEHjg==",
+
"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.1-rc.0",
+
"@atproto/lexicon": "^0.4.11",
"zod": "^3.23.8"
}
},
"node_modules/@atproto/xrpc-server": {
-
"version": "0.5.4-rc.0",
-
"resolved": "https://registry.npmjs.org/@atproto/xrpc-server/-/xrpc-server-0.5.4-rc.0.tgz",
-
"integrity": "sha512-Vrx1gEoZfJtYoZhSxkbWQsU2r0DuJO/BuvMQGw9Nd66owmF5nPDVvYVd0pJhIDoaSxImTTIEeDWlNNl3WCSBPA==",
+
"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.1",
-
"@atproto/crypto": "^0.4.0",
-
"@atproto/lexicon": "^0.4.1-rc.0",
-
"@atproto/xrpc": "^0.6.0-rc.0",
+
"@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"
+
},
+
"engines": {
+
"node": ">=18.7.0"
}
},
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
···
"darwin"
]
},
+
"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
+
"version": "2.2.0",
+
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz",
+
"integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==",
+
"cpu": [
+
"x64"
+
],
+
"optional": true,
+
"os": [
+
"darwin"
+
]
+
},
+
"node_modules/@cbor-extract/cbor-extract-linux-arm": {
+
"version": "2.2.0",
+
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz",
+
"integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==",
+
"cpu": [
+
"arm"
+
],
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
+
"version": "2.2.0",
+
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz",
+
"integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==",
+
"cpu": [
+
"arm64"
+
],
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@cbor-extract/cbor-extract-linux-x64": {
+
"version": "2.2.0",
+
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz",
+
"integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==",
+
"cpu": [
+
"x64"
+
],
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@cbor-extract/cbor-extract-win32-x64": {
+
"version": "2.2.0",
+
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz",
+
"integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==",
+
"cpu": [
+
"x64"
+
],
+
"optional": true,
+
"os": [
+
"win32"
+
]
+
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
···
"node": ">=12"
}
},
+
"node_modules/@esbuild/aix-ppc64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
+
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
+
"cpu": [
+
"ppc64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"aix"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/android-arm": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
+
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"android"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/android-arm64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
+
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"android"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/android-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
+
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"android"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
"node_modules/@esbuild/darwin-arm64": {
-
"version": "0.23.0",
-
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz",
-
"integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==",
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
+
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
"cpu": [
"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/@esbuild/darwin-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
+
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"darwin"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/freebsd-arm64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
+
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"freebsd"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/freebsd-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
+
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"freebsd"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-arm": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
+
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-arm64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
+
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-ia32": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
+
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
+
"cpu": [
+
"ia32"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-loong64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
+
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
+
"cpu": [
+
"loong64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-mips64el": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
+
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
+
"cpu": [
+
"mips64el"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-ppc64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
+
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
+
"cpu": [
+
"ppc64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-riscv64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
+
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
+
"cpu": [
+
"riscv64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-s390x": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
+
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
+
"cpu": [
+
"s390x"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/linux-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
+
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/netbsd-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
+
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"netbsd"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/openbsd-arm64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
+
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"openbsd"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/openbsd-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
+
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"openbsd"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/sunos-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
+
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"sunos"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/win32-arm64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
+
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"win32"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/win32-ia32": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
+
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
+
"cpu": [
+
"ia32"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"win32"
+
],
+
"engines": {
+
"node": ">=18"
+
}
+
},
+
"node_modules/@esbuild/win32-x64": {
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
+
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"win32"
+
],
+
"engines": {
+
"node": ">=18"
}
},
"node_modules/@ipld/dag-cbor": {
···
"node": ">=12"
}
},
-
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
-
"version": "6.2.1",
-
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
-
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-
"dev": true,
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
-
}
-
},
-
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
-
"version": "9.2.2",
-
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-
"dev": true
-
},
-
"node_modules/@isaacs/cliui/node_modules/string-width": {
-
"version": "5.1.2",
-
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
-
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-
"dev": true,
-
"dependencies": {
-
"eastasianwidth": "^0.2.0",
-
"emoji-regex": "^9.2.2",
-
"strip-ansi": "^7.0.1"
-
},
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
-
"version": "8.1.0",
-
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
-
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-
"dev": true,
-
"dependencies": {
-
"ansi-styles": "^6.1.0",
-
"string-width": "^5.0.1",
-
"strip-ansi": "^7.0.1"
-
},
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-
}
-
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
···
}
},
"node_modules/@noble/curves": {
-
"version": "1.5.0",
-
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz",
-
"integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==",
+
"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.4.0"
+
"@noble/hashes": "1.8.0"
+
},
+
"engines": {
+
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
-
"version": "1.4.0",
-
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
-
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+
"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": ">= 16"
+
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
···
"url": "https://opencollective.com/preact"
}
},
+
"node_modules/@rollup/rollup-android-arm-eabi": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz",
+
"integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"android"
+
]
+
},
+
"node_modules/@rollup/rollup-android-arm64": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz",
+
"integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"android"
+
]
+
},
"node_modules/@rollup/rollup-darwin-arm64": {
-
"version": "4.20.0",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
-
"integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz",
+
"integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==",
"cpu": [
"arm64"
],
···
"darwin"
]
},
+
"node_modules/@rollup/rollup-darwin-x64": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz",
+
"integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"darwin"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz",
+
"integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz",
+
"integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==",
+
"cpu": [
+
"arm"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz",
+
"integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz",
+
"integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz",
+
"integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==",
+
"cpu": [
+
"ppc64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz",
+
"integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==",
+
"cpu": [
+
"riscv64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz",
+
"integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==",
+
"cpu": [
+
"s390x"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz",
+
"integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-linux-x64-musl": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz",
+
"integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"linux"
+
]
+
},
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz",
+
"integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==",
+
"cpu": [
+
"arm64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"win32"
+
]
+
},
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz",
+
"integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==",
+
"cpu": [
+
"ia32"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"win32"
+
]
+
},
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz",
+
"integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==",
+
"cpu": [
+
"x64"
+
],
+
"dev": true,
+
"optional": true,
+
"os": [
+
"win32"
+
]
+
},
"node_modules/@ts-morph/common": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.17.0.tgz",
···
"@types/node": "*"
}
},
-
"node_modules/@types/cors": {
-
"version": "2.8.17",
-
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
-
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
-
"dev": true,
-
"dependencies": {
-
"@types/node": "*"
-
}
-
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
···
"dev": true
},
"node_modules/@types/node": {
-
"version": "22.2.0",
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz",
-
"integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==",
+
"version": "22.5.4",
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
+
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dev": true,
"dependencies": {
-
"undici-types": "~6.13.0"
+
"undici-types": "~6.19.2"
}
},
"node_modules/@types/qs": {
···
"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",
···
"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",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
···
},
"node_modules/better-sqlite3": {
-
"version": "11.1.2",
-
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.1.2.tgz",
-
"integrity": "sha512-gujtFwavWU4MSPT+h9B+4pkvZdyOUkH54zgLdIrMmmmd4ZqiBIrRNBzNzYVFO417xo882uP5HBu4GjOfaSrIQw==",
+
"version": "11.2.1",
+
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz",
+
"integrity": "sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA==",
"hasInstallScript": true,
"dependencies": {
"bindings": "^1.5.0",
···
"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",
···
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
-
"node_modules/cors": {
-
"version": "2.8.5",
-
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
-
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
-
"dependencies": {
-
"object-assign": "^4",
-
"vary": "^1"
-
},
-
"engines": {
-
"node": ">= 0.10"
-
}
-
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.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": {
···
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
+
"node_modules/emoji-regex": {
+
"version": "9.2.2",
+
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+
"dev": true
+
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
···
},
"node_modules/esbuild": {
-
"version": "0.23.0",
-
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz",
-
"integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==",
+
"version": "0.23.1",
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
+
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
"dev": true,
"hasInstallScript": true,
"bin": {
···
"node": ">=18"
},
"optionalDependencies": {
-
"@esbuild/aix-ppc64": "0.23.0",
-
"@esbuild/android-arm": "0.23.0",
-
"@esbuild/android-arm64": "0.23.0",
-
"@esbuild/android-x64": "0.23.0",
-
"@esbuild/darwin-arm64": "0.23.0",
-
"@esbuild/darwin-x64": "0.23.0",
-
"@esbuild/freebsd-arm64": "0.23.0",
-
"@esbuild/freebsd-x64": "0.23.0",
-
"@esbuild/linux-arm": "0.23.0",
-
"@esbuild/linux-arm64": "0.23.0",
-
"@esbuild/linux-ia32": "0.23.0",
-
"@esbuild/linux-loong64": "0.23.0",
-
"@esbuild/linux-mips64el": "0.23.0",
-
"@esbuild/linux-ppc64": "0.23.0",
-
"@esbuild/linux-riscv64": "0.23.0",
-
"@esbuild/linux-s390x": "0.23.0",
-
"@esbuild/linux-x64": "0.23.0",
-
"@esbuild/netbsd-x64": "0.23.0",
-
"@esbuild/openbsd-arm64": "0.23.0",
-
"@esbuild/openbsd-x64": "0.23.0",
-
"@esbuild/sunos-x64": "0.23.0",
-
"@esbuild/win32-arm64": "0.23.0",
-
"@esbuild/win32-ia32": "0.23.0",
-
"@esbuild/win32-x64": "0.23.0"
+
"@esbuild/aix-ppc64": "0.23.1",
+
"@esbuild/android-arm": "0.23.1",
+
"@esbuild/android-arm64": "0.23.1",
+
"@esbuild/android-x64": "0.23.1",
+
"@esbuild/darwin-arm64": "0.23.1",
+
"@esbuild/darwin-x64": "0.23.1",
+
"@esbuild/freebsd-arm64": "0.23.1",
+
"@esbuild/freebsd-x64": "0.23.1",
+
"@esbuild/linux-arm": "0.23.1",
+
"@esbuild/linux-arm64": "0.23.1",
+
"@esbuild/linux-ia32": "0.23.1",
+
"@esbuild/linux-loong64": "0.23.1",
+
"@esbuild/linux-mips64el": "0.23.1",
+
"@esbuild/linux-ppc64": "0.23.1",
+
"@esbuild/linux-riscv64": "0.23.1",
+
"@esbuild/linux-s390x": "0.23.1",
+
"@esbuild/linux-x64": "0.23.1",
+
"@esbuild/netbsd-x64": "0.23.1",
+
"@esbuild/openbsd-arm64": "0.23.1",
+
"@esbuild/openbsd-x64": "0.23.1",
+
"@esbuild/sunos-x64": "0.23.1",
+
"@esbuild/win32-arm64": "0.23.1",
+
"@esbuild/win32-ia32": "0.23.1",
+
"@esbuild/win32-x64": "0.23.1"
},
"node_modules/escape-html": {
···
"node": ">=6"
},
+
"node_modules/eventemitter3": {
+
"version": "4.0.7",
+
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
+
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
···
"node": ">=0.8.x"
},
+
"node_modules/execa": {
+
"version": "5.1.1",
+
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+
"dev": true,
+
"dependencies": {
+
"cross-spawn": "^7.0.3",
+
"get-stream": "^6.0.0",
+
"human-signals": "^2.1.0",
+
"is-stream": "^2.0.0",
+
"merge-stream": "^2.0.0",
+
"npm-run-path": "^4.0.1",
+
"onetime": "^5.1.2",
+
"signal-exit": "^3.0.3",
+
"strip-final-newline": "^2.0.0"
+
},
+
"engines": {
+
"node": ">=10"
+
},
+
"funding": {
+
"url": "https://github.com/sindresorhus/execa?sponsor=1"
+
}
+
},
+
"node_modules/execa/node_modules/signal-exit": {
+
"version": "3.0.7",
+
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+
"dev": true
+
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
···
"node": ">= 0.10.0"
},
-
"node_modules/express-rate-limit": {
-
"version": "7.4.0",
-
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.0.tgz",
-
"integrity": "sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg==",
-
"engines": {
-
"node": ">= 16"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/express-rate-limit"
-
},
-
"peerDependencies": {
-
"express": "4 || 5 || ^5.0.0-beta.1"
-
}
-
},
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
···
},
"engines": {
"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": {
···
"node": ">= 0.8"
},
-
"node_modules/follow-redirects": {
-
"version": "1.15.6",
-
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
-
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
-
"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",
···
},
"funding": {
"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": {
···
},
"node_modules/gc-hook": {
-
"version": "0.3.1",
-
"resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz",
-
"integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A=="
-
},
-
"node_modules/get-caller-file": {
-
"version": "2.0.5",
-
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-
"engines": {
-
"node": "6.* || 8.* || >= 10.*"
-
}
+
"version": "0.4.1",
+
"resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.4.1.tgz",
+
"integrity": "sha512-uiF+uUftDVLr+VRdudsdsT3/LQYnv2ntwhRH964O7xXDI57Smrek5olv75Wb8Nnz6U+7iVTRXsBlxKcsaDTJTQ=="
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
···
"url": "https://github.com/sponsors/ljharb"
},
+
"node_modules/get-stream": {
+
"version": "6.0.1",
+
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+
"dev": true,
+
"engines": {
+
"node": ">=10"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
"node_modules/get-tsconfig": {
-
"version": "4.7.6",
-
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz",
-
"integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==",
+
"version": "4.8.0",
+
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz",
+
"integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==",
"dev": true,
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
···
"node": ">= 0.4"
},
-
"node_modules/helmet": {
-
"version": "7.1.0",
-
"resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz",
-
"integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==",
-
"engines": {
-
"node": ">=16.0.0"
-
}
-
},
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
···
"node": ">= 0.8"
},
-
"node_modules/http-status-codes": {
-
"version": "2.3.0",
-
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
-
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="
+
"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": {
+
"version": "2.1.0",
+
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+
"dev": true,
+
"engines": {
+
"node": ">=10.17.0"
+
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
···
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/ipaddr.js": {
-
"version": "2.2.0",
-
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
-
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+
"version": "1.9.1",
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
-
"node": ">= 10"
+
"node": ">= 0.10"
},
"node_modules/iron-session": {
-
"version": "8.0.2",
-
"resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.2.tgz",
-
"integrity": "sha512-p4Yf1moQr6gnCcXu5vCaxVKRKDmR9PZcQDfp7ZOgbsSHUsgaNti6OgDB2BdgxC2aS6V/6Hu4O0wYlj92sbdIJg==",
+
"version": "8.0.3",
+
"resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.3.tgz",
+
"integrity": "sha512-WtDX0griBliMoR6hGoU3SlefW+VSbfHrIVqURQ0Nbg/Pd+nj7VDsKV+sx0FHjyUCaO02YoYV5v+kW0PqvFJISQ==",
"funding": [
"https://github.com/sponsors/vvo",
"https://github.com/sponsors/brc-dd"
···
"node": ">=0.10.0"
},
+
"node_modules/is-fullwidth-code-point": {
+
"version": "3.0.0",
+
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+
"dev": true,
+
"engines": {
+
"node": ">=8"
+
}
+
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
···
"node": ">=0.12.0"
},
+
"node_modules/is-stream": {
+
"version": "2.0.1",
+
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+
"dev": true,
+
"engines": {
+
"node": ">=8"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
···
},
"node_modules/jose": {
-
"version": "5.6.3",
-
"resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz",
-
"integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==",
+
"version": "5.8.0",
+
"resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
+
"integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
"funding": {
"url": "https://github.com/sponsors/panva"
···
},
"node_modules/micromatch": {
-
"version": "4.0.7",
-
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+
"version": "4.0.8",
+
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": {
"braces": "^3.0.3",
···
},
"engines": {
"node": ">= 0.6"
+
}
+
},
+
"node_modules/mimic-fn": {
+
"version": "2.1.0",
+
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+
"dev": true,
+
"engines": {
+
"node": ">=6"
},
"node_modules/mimic-response": {
···
"thenify-all": "^1.0.0"
},
-
"node_modules/nanoid": {
-
"version": "3.3.7",
-
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
-
"dev": true,
-
"funding": [
-
{
-
"type": "github",
-
"url": "https://github.com/sponsors/ai"
-
}
-
],
-
"optional": true,
-
"peer": true,
-
"bin": {
-
"nanoid": "bin/nanoid.cjs"
-
},
-
"engines": {
-
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-
}
-
},
"node_modules/napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
···
},
"node_modules/node-abi": {
-
"version": "3.65.0",
-
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz",
-
"integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==",
+
"version": "3.67.0",
+
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz",
+
"integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==",
"dependencies": {
"semver": "^7.3.5"
},
···
"node": ">=0.10.0"
},
+
"node_modules/npm-run-path": {
+
"version": "4.0.1",
+
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+
"dev": true,
+
"dependencies": {
+
"path-key": "^3.0.0"
+
},
+
"engines": {
+
"node": ">=8"
+
}
+
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+
"dev": true,
"engines": {
"node": ">=0.10.0"
···
"wrappy": "1"
},
+
"node_modules/onetime": {
+
"version": "5.1.2",
+
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+
"dev": true,
+
"dependencies": {
+
"mimic-fn": "^2.1.0"
+
},
+
"engines": {
+
"node": ">=6"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
+
"node_modules/p-finally": {
+
"version": "1.0.0",
+
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
+
"engines": {
+
"node": ">=4"
+
}
+
},
+
"node_modules/p-queue": {
+
"version": "6.6.2",
+
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+
"dependencies": {
+
"eventemitter3": "^4.0.4",
+
"p-timeout": "^3.2.0"
+
},
+
"engines": {
+
"node": ">=8"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
+
"node_modules/p-timeout": {
+
"version": "3.2.0",
+
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+
"dependencies": {
+
"p-finally": "^1.0.0"
+
},
+
"engines": {
+
"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_modules/picocolors": {
-
"version": "1.0.1",
-
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+
"version": "1.1.0",
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true
},
"node_modules/picomatch": {
···
},
"node_modules/pino": {
-
"version": "9.3.2",
-
"resolved": "https://registry.npmjs.org/pino/-/pino-9.3.2.tgz",
-
"integrity": "sha512-WtARBjgZ7LNEkrGWxMBN/jvlFiE17LTbBoH0konmBU684Kd0uIiDwBXlcTCW7iJnA6HfIKwUssS/2AC6cDEanw==",
+
"version": "9.4.0",
+
"resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz",
+
"integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
···
"split2": "^4.0.0"
},
-
"node_modules/pino-http": {
-
"version": "10.2.0",
-
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.2.0.tgz",
-
"integrity": "sha512-am03BxnV3Ckx68OkbH0iZs3indsrH78wncQ6w1w51KroIbvJZNImBKX2X1wjdY8lSyaJ0UrX/dnO2DY3cTeCRw==",
-
"dependencies": {
-
"get-caller-file": "^2.0.5",
-
"pino": "^9.0.0",
-
"pino-std-serializers": "^7.0.0",
-
"process-warning": "^3.0.0"
-
}
-
},
-
"node_modules/pino-http/node_modules/process-warning": {
-
"version": "3.0.0",
-
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz",
-
"integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="
-
},
"node_modules/pino-pretty": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.2.2.tgz",
···
"node": ">= 6"
},
-
"node_modules/postcss": {
-
"version": "8.4.41",
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
-
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
-
"dev": true,
-
"funding": [
-
{
-
"type": "opencollective",
-
"url": "https://opencollective.com/postcss/"
-
},
-
{
-
"type": "tidelift",
-
"url": "https://tidelift.com/funding/github/npm/postcss"
-
},
-
{
-
"type": "github",
-
"url": "https://github.com/sponsors/ai"
-
}
-
],
-
"optional": true,
-
"peer": true,
-
"dependencies": {
-
"nanoid": "^3.3.7",
-
"picocolors": "^1.0.1",
-
"source-map-js": "^1.2.0"
-
},
-
"engines": {
-
"node": "^10 || ^12 || >=14"
-
}
-
},
"node_modules/postcss-load-config": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
···
"node": ">= 0.10"
},
-
"node_modules/proxy-addr/node_modules/ipaddr.js": {
-
"version": "1.9.1",
-
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
-
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
-
"engines": {
-
"node": ">= 0.10"
-
}
-
},
-
"node_modules/psl": {
-
"version": "1.9.0",
-
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
-
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
-
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
···
"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.20.0",
-
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
-
"integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
+
"version": "4.21.2",
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz",
+
"integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.5"
···
"npm": ">=8.0.0"
},
"optionalDependencies": {
-
"@rollup/rollup-android-arm-eabi": "4.20.0",
-
"@rollup/rollup-android-arm64": "4.20.0",
-
"@rollup/rollup-darwin-arm64": "4.20.0",
-
"@rollup/rollup-darwin-x64": "4.20.0",
-
"@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
-
"@rollup/rollup-linux-arm-musleabihf": "4.20.0",
-
"@rollup/rollup-linux-arm64-gnu": "4.20.0",
-
"@rollup/rollup-linux-arm64-musl": "4.20.0",
-
"@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
-
"@rollup/rollup-linux-riscv64-gnu": "4.20.0",
-
"@rollup/rollup-linux-s390x-gnu": "4.20.0",
-
"@rollup/rollup-linux-x64-gnu": "4.20.0",
-
"@rollup/rollup-linux-x64-musl": "4.20.0",
-
"@rollup/rollup-win32-arm64-msvc": "4.20.0",
-
"@rollup/rollup-win32-ia32-msvc": "4.20.0",
-
"@rollup/rollup-win32-x64-msvc": "4.20.0",
+
"@rollup/rollup-android-arm-eabi": "4.21.2",
+
"@rollup/rollup-android-arm64": "4.21.2",
+
"@rollup/rollup-darwin-arm64": "4.21.2",
+
"@rollup/rollup-darwin-x64": "4.21.2",
+
"@rollup/rollup-linux-arm-gnueabihf": "4.21.2",
+
"@rollup/rollup-linux-arm-musleabihf": "4.21.2",
+
"@rollup/rollup-linux-arm64-gnu": "4.21.2",
+
"@rollup/rollup-linux-arm64-musl": "4.21.2",
+
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.2",
+
"@rollup/rollup-linux-riscv64-gnu": "4.21.2",
+
"@rollup/rollup-linux-s390x-gnu": "4.21.2",
+
"@rollup/rollup-linux-x64-gnu": "4.21.2",
+
"@rollup/rollup-linux-x64-musl": "4.21.2",
+
"@rollup/rollup-win32-arm64-msvc": "4.21.2",
+
"@rollup/rollup-win32-ia32-msvc": "4.21.2",
+
"@rollup/rollup-win32-x64-msvc": "4.21.2",
"fsevents": "~2.3.2"
},
···
},
"node_modules/safe-stable-stringify": {
-
"version": "2.4.3",
-
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
-
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
+
"version": "2.5.0",
+
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"engines": {
"node": ">=10"
···
"engines": {
"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",
···
},
"node_modules/sonic-boom": {
-
"version": "4.0.1",
-
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz",
-
"integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==",
+
"version": "4.1.0",
+
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz",
+
"integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==",
"dependencies": {
"atomic-sleep": "^1.0.0"
···
"node": ">= 8"
},
-
"node_modules/source-map-js": {
-
"version": "1.2.0",
-
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
-
"dev": true,
-
"optional": true,
-
"peer": true,
-
"engines": {
-
"node": ">=0.10.0"
-
}
-
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
···
"safe-buffer": "~5.2.0"
},
+
"node_modules/string-width": {
+
"version": "5.1.2",
+
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+
"dev": true,
+
"dependencies": {
+
"eastasianwidth": "^0.2.0",
+
"emoji-regex": "^9.2.2",
+
"strip-ansi": "^7.0.1"
+
},
+
"engines": {
+
"node": ">=12"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
···
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
-
"node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
-
"version": "3.0.0",
-
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-
"dev": true,
-
"engines": {
-
"node": ">=8"
-
}
-
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
···
"dev": true,
"engines": {
"node": ">=8"
+
}
+
},
+
"node_modules/strip-final-newline": {
+
"version": "2.0.0",
+
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+
"dev": true,
+
"engines": {
+
"node": ">=6"
},
"node_modules/strip-json-comments": {
···
},
-
"node_modules/tsup/node_modules/execa": {
-
"version": "5.1.1",
-
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
-
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
-
"dev": true,
-
"dependencies": {
-
"cross-spawn": "^7.0.3",
-
"get-stream": "^6.0.0",
-
"human-signals": "^2.1.0",
-
"is-stream": "^2.0.0",
-
"merge-stream": "^2.0.0",
-
"npm-run-path": "^4.0.1",
-
"onetime": "^5.1.2",
-
"signal-exit": "^3.0.3",
-
"strip-final-newline": "^2.0.0"
-
},
-
"engines": {
-
"node": ">=10"
-
},
-
"funding": {
-
"url": "https://github.com/sindresorhus/execa?sponsor=1"
-
}
-
},
-
"node_modules/tsup/node_modules/get-stream": {
-
"version": "6.0.1",
-
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
-
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
-
"dev": true,
-
"engines": {
-
"node": ">=10"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/tsup/node_modules/human-signals": {
-
"version": "2.1.0",
-
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
-
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
-
"dev": true,
-
"engines": {
-
"node": ">=10.17.0"
-
}
-
},
-
"node_modules/tsup/node_modules/is-stream": {
-
"version": "2.0.1",
-
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
-
"dev": true,
-
"engines": {
-
"node": ">=8"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/tsup/node_modules/mimic-fn": {
-
"version": "2.1.0",
-
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
-
"dev": true,
-
"engines": {
-
"node": ">=6"
-
}
-
},
"node_modules/tsup/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
-
"node_modules/tsup/node_modules/npm-run-path": {
-
"version": "4.0.1",
-
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
-
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
-
"dev": true,
-
"dependencies": {
-
"path-key": "^3.0.0"
-
},
-
"engines": {
-
"node": ">=8"
-
}
-
},
-
"node_modules/tsup/node_modules/onetime": {
-
"version": "5.1.2",
-
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
-
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
-
"dev": true,
-
"dependencies": {
-
"mimic-fn": "^2.1.0"
-
},
-
"engines": {
-
"node": ">=6"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/tsup/node_modules/signal-exit": {
-
"version": "3.0.7",
-
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-
"dev": true
-
},
-
"node_modules/tsup/node_modules/strip-final-newline": {
-
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
-
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
-
"dev": true,
-
"engines": {
-
"node": ">=6"
-
}
-
},
"node_modules/tsx": {
-
"version": "4.17.0",
-
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz",
-
"integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==",
+
"version": "4.19.0",
+
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.0.tgz",
+
"integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==",
"dev": true,
"dependencies": {
"esbuild": "~0.23.0",
···
},
"engines": {
"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": {
···
"integrity": "sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA=="
},
"node_modules/uhtml": {
-
"version": "4.5.9",
-
"resolved": "https://registry.npmjs.org/uhtml/-/uhtml-4.5.9.tgz",
-
"integrity": "sha512-WAfIK/E3ZJpaFl0MSzGSB54r7I8Vc8ZyUlOsN8GnLnEaxuioOUyKAS6q/N/xQ5GD9vFFBnx6q+3N3Eq9KNCvTQ==",
+
"version": "4.5.11",
+
"resolved": "https://registry.npmjs.org/uhtml/-/uhtml-4.5.11.tgz",
+
"integrity": "sha512-Jbcrdmc5rwLUJotyX7mi1jBkAnGjjQ9hg0xomKXl7JfHL5KMvpOUJCAWA7FY+IMcAWqZM2NsJMVlwJQjLK4gNw==",
"dependencies": {
"@webreflection/uparser": "^0.3.3",
"custom-function": "^1.0.6",
"domconstants": "^1.1.6",
-
"gc-hook": "^0.3.1",
+
"gc-hook": "^0.4.1",
"html-escaper": "^3.0.3",
"htmlparser2": "^9.1.0",
"udomdiff": "^1.1.0"
},
"optionalDependencies": {
-
"@preact/signals-core": "^1.6.0",
+
"@preact/signals-core": "^1.8.0",
"@webreflection/signal": "^2.1.2"
},
···
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
},
"node_modules/undici": {
-
"version": "6.19.7",
-
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.7.tgz",
-
"integrity": "sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==",
+
"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/undici-types": {
-
"version": "6.13.0",
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
-
"integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==",
+
"version": "6.19.8",
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"node_modules/unpipe": {
···
"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",
···
"node": ">= 8"
},
+
"node_modules/wrap-ansi": {
+
"version": "8.1.0",
+
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+
"dev": true,
+
"dependencies": {
+
"ansi-styles": "^6.1.0",
+
"string-width": "^5.0.1",
+
"strip-ansi": "^7.0.1"
+
},
+
"engines": {
+
"node": ">=12"
+
},
+
"funding": {
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+
}
+
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
···
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
-
"node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
-
"version": "3.0.0",
-
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-
"dev": true,
-
"engines": {
-
"node": ">=8"
-
}
-
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
···
"node": ">=8"
},
+
"node_modules/wrap-ansi/node_modules/ansi-styles": {
+
"version": "6.2.1",
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+
"dev": true,
+
"engines": {
+
"node": ">=12"
+
},
+
"funding": {
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
+
}
+
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"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/yaml": {
-
"version": "2.5.0",
-
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
-
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
-
"dev": true,
-
"optional": true,
-
"peer": true,
-
"bin": {
-
"yaml": "bin.mjs"
-
},
-
"engines": {
-
"node": ">= 14"
-
}
-
},
"node_modules/yesno": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz",
···
},
"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"
+10 -9
package.json
···
"clean": "rimraf dist coverage"
},
"dependencies": {
-
"@atproto/identity": "^0.4.0",
-
"@atproto/jwk-jose": "0.1.2-rc.0",
-
"@atproto/lexicon": "0.4.1-rc.0",
-
"@atproto/oauth-client-node": "0.0.2-rc.2",
-
"@atproto/repo": "0.4.2-rc.0",
-
"@atproto/syntax": "^0.3.0",
-
"@atproto/xrpc-server": "0.5.4-rc.0",
+
"@atproto/api": "^0.15.6",
+
"@atproto/common": "^0.4.11",
+
"@atproto/identity": "^0.4.8",
+
"@atproto/lexicon": "^0.4.11",
+
"@atproto/oauth-client-node": "^0.3.1",
+
"@atproto/sync": "^0.1.26",
+
"@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",
-
"pino-http": "^10.0.0",
-
"uhtml": "^4.5.9"
+
"uhtml": "^4.5.9",
+
"zod": "^3.25.67"
},
"devDependencies": {
"@atproto/lex-cli": "^0.4.1",
+57 -19
src/auth/client.ts
···
-
import { JoseKey } from '@atproto/jwk-jose'
-
import { NodeOAuthClient } from '@atproto/oauth-client-node'
+
import {
+
Keyset,
+
JoseKey,
+
atprotoLoopbackClientMetadata,
+
NodeOAuthClient,
+
OAuthClientMetadataInput,
+
} from '@atproto/oauth-client-node'
+
import assert from 'node:assert'
+
import type { Database } from '#/db'
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}`
+
export async function createOAuthClient(db: Database) {
+
// 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_KEYS
+
? new Keyset(
+
await Promise.all(
+
env.PRIVATE_KEYS.map((jwk) => JoseKey.fromJWK(jwk)),
+
),
+
)
+
: undefined
+
+
assert(
+
!env.PUBLIC_URL || keyset?.size,
+
'ATProto requires backend clients to be confidential. Make sure to set the PRIVATE_KEYS environment variable.',
+
)
+
+
// 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 not 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.alg : 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=${encodeURIComponent(`${url}/oauth/callback`)}`,
-
client_uri: url,
-
redirect_uris: [`${url}/oauth/callback`],
-
scope: 'profile offline_access',
-
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),
+
plcDirectoryUrl: env.PLC_URL,
+
handleResolver: env.PDS_URL,
})
}
-63
src/auth/session.ts
···
-
'use server'
-
-
import assert from 'node:assert'
-
import type { IncomingMessage, ServerResponse } from 'node:http'
-
import { getIronSession } from 'iron-session'
-
import { env } from '#/env'
-
import { AppContext } from '#/config'
-
-
export type Session = { did: string }
-
-
export async function createSession(
-
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>,
-
did: string
-
) {
-
const session = await getSessionRaw(req, res)
-
assert(!session.did, 'session already exists')
-
session.did = did
-
await session.save()
-
return { did: session.did }
-
}
-
-
export async function destroySession(
-
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>
-
) {
-
const session = await getSessionRaw(req, res)
-
await session.destroy()
-
return null
-
}
-
-
export async function getSession(
-
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>
-
) {
-
const session = await getSessionRaw(req, res)
-
if (!session.did) return null
-
return { did: session.did }
-
}
-
-
export async function getSessionAgent(
-
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>,
-
ctx: AppContext
-
) {
-
const session = await getSessionRaw(req, res)
-
if (!session.did) return null
-
return await ctx.oauthClient.restore(session.did).catch(async (err) => {
-
ctx.logger.warn({ err }, 'oauth restore failed')
-
await destroySession(req, res)
-
return null
-
})
-
}
-
-
async function getSessionRaw(
-
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>
-
) {
-
return await getIronSession<Session>(req, res, {
-
cookieName: 'sid',
-
password: env.COOKIE_SECRET,
-
})
-
}
-13
src/config.ts
···
-
import type { OAuthClient } from '@atproto/oauth-client-node'
-
import type pino from 'pino'
-
import type { Database } from '#/db'
-
import type { Ingester } from '#/firehose/ingester'
-
import { Resolver } from '#/ident/types'
-
-
export type AppContext = {
-
db: Database
-
ingester: Ingester
-
logger: pino.Logger
-
oauthClient: OAuthClient
-
resolver: Resolver
-
}
+46
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 { createDb, Database, migrateToLatest } from '#/db'
+
import { createIngester } from '#/ingester'
+
import { env } from '#/env'
+
import {
+
BidirectionalResolver,
+
createBidirectionalResolver,
+
} from '#/id-resolver'
+
+
/**
+
* Application state passed to the router and elsewhere
+
*/
+
export type AppContext = {
+
db: Database
+
ingester: Firehose
+
logger: pino.Logger
+
oauthClient: NodeOAuthClient
+
resolver: BidirectionalResolver
+
destroy: () => Promise<void>
+
}
+
+
export async function createAppContext(): Promise<AppContext> {
+
const db = createDb(env.DB_PATH)
+
await migrateToLatest(db)
+
const oauthClient = await createOAuthClient(db)
+
const ingester = createIngester(db)
+
const logger = pino({ name: 'server', level: env.LOG_LEVEL })
+
const resolver = createBidirectionalResolver(oauthClient)
+
+
return {
+
db,
+
ingester,
+
logger,
+
oauthClient,
+
resolver,
+
+
async destroy() {
+
await ingester.destroy()
+
await db.destroy()
+
},
+
}
+
}
-20
src/db/index.ts
···
-
import SqliteDb from 'better-sqlite3'
-
import { Kysely, Migrator, SqliteDialect } from 'kysely'
-
import { migrationProvider } from './migrations'
-
import type { DatabaseSchema } from './schema'
-
-
export const createDb = (location: string): Database => {
-
return new Kysely<DatabaseSchema>({
-
dialect: new SqliteDialect({
-
database: new SqliteDb(location),
-
}),
-
})
-
}
-
-
export const migrateToLatest = async (db: Database) => {
-
const migrator = new Migrator({ db, provider: migrationProvider })
-
const { error } = await migrator.migrateToLatest()
-
if (error) throw error
-
}
-
-
export type Database = Kysely<DatabaseSchema>
-36
src/db/migrations.ts
···
-
import type { Kysely, Migration, MigrationProvider } from 'kysely'
-
-
const migrations: Record<string, Migration> = {}
-
-
export const migrationProvider: MigrationProvider = {
-
async getMigrations() {
-
return migrations
-
},
-
}
-
-
migrations['001'] = {
-
async up(db: Kysely<unknown>) {
-
await db.schema
-
.createTable('status')
-
.addColumn('authorDid', 'varchar', (col) => col.primaryKey())
-
.addColumn('status', 'varchar', (col) => col.notNull())
-
.addColumn('updatedAt', 'varchar', (col) => col.notNull())
-
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
-
.execute()
-
await db.schema
-
.createTable('auth_session')
-
.addColumn('key', 'varchar', (col) => col.primaryKey())
-
.addColumn('session', 'varchar', (col) => col.notNull())
-
.execute()
-
await db.schema
-
.createTable('auth_state')
-
.addColumn('key', 'varchar', (col) => col.primaryKey())
-
.addColumn('state', 'varchar', (col) => col.notNull())
-
.execute()
-
},
-
async down(db: Kysely<unknown>) {
-
await db.schema.dropTable('auth_state').execute()
-
await db.schema.dropTable('auth_session').execute()
-
await db.schema.dropTable('status').execute()
-
},
-
}
-26
src/db/schema.ts
···
-
export type DatabaseSchema = {
-
status: Status
-
auth_session: AuthSession
-
auth_state: AuthState
-
}
-
-
export type Status = {
-
authorDid: string
-
status: string
-
updatedAt: string
-
indexedAt: string
-
}
-
-
export type AuthSession = {
-
key: string
-
session: AuthSessionJson
-
}
-
-
export type AuthState = {
-
key: string
-
state: AuthStateJson
-
}
-
-
type AuthStateJson = string
-
-
type AuthSessionJson = string
+94
src/db.ts
···
+
import SqliteDb from 'better-sqlite3'
+
import {
+
Kysely,
+
Migrator,
+
SqliteDialect,
+
Migration,
+
MigrationProvider,
+
} from 'kysely'
+
+
// Types
+
+
export type DatabaseSchema = {
+
status: Status
+
auth_session: AuthSession
+
auth_state: AuthState
+
}
+
+
export type Status = {
+
uri: string
+
authorDid: string
+
status: string
+
createdAt: string
+
indexedAt: string
+
}
+
+
export type AuthSession = {
+
key: string
+
session: AuthSessionJson
+
}
+
+
export type AuthState = {
+
key: string
+
state: AuthStateJson
+
}
+
+
type AuthStateJson = string
+
+
type AuthSessionJson = string
+
+
// Migrations
+
+
const migrations: Record<string, Migration> = {}
+
+
const migrationProvider: MigrationProvider = {
+
async getMigrations() {
+
return migrations
+
},
+
}
+
+
migrations['001'] = {
+
async up(db: Kysely<unknown>) {
+
await db.schema
+
.createTable('status')
+
.addColumn('uri', 'varchar', (col) => col.primaryKey())
+
.addColumn('authorDid', 'varchar', (col) => col.notNull())
+
.addColumn('status', 'varchar', (col) => col.notNull())
+
.addColumn('createdAt', 'varchar', (col) => col.notNull())
+
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
+
.execute()
+
await db.schema
+
.createTable('auth_session')
+
.addColumn('key', 'varchar', (col) => col.primaryKey())
+
.addColumn('session', 'varchar', (col) => col.notNull())
+
.execute()
+
await db.schema
+
.createTable('auth_state')
+
.addColumn('key', 'varchar', (col) => col.primaryKey())
+
.addColumn('state', 'varchar', (col) => col.notNull())
+
.execute()
+
},
+
async down(db: Kysely<unknown>) {
+
await db.schema.dropTable('auth_state').execute()
+
await db.schema.dropTable('auth_session').execute()
+
await db.schema.dropTable('status').execute()
+
},
+
}
+
+
// APIs
+
+
export const createDb = (location: string): Database => {
+
return new Kysely<DatabaseSchema>({
+
dialect: new SqliteDialect({
+
database: new SqliteDb(location),
+
}),
+
})
+
}
+
+
export const migrateToLatest = async (db: Database) => {
+
const migrator = new Migrator({ db, provider: migrationProvider })
+
const { error } = await migrator.migrateToLatest()
+
if (error) throw error
+
}
+
+
export type Database = Kysely<DatabaseSchema>
+12 -5
src/env.ts
···
import dotenv from 'dotenv'
-
import { cleanEnv, host, num, port, str, testOnly } from 'envalid'
+
import { cleanEnv, port, str, testOnly, url } from 'envalid'
+
import { envalidJsonWebKeys as keys } from '#/lib/jwk'
dotenv.config()
···
devDefault: testOnly('test'),
choices: ['development', 'production', 'test'],
}),
-
HOST: host({ devDefault: testOnly('localhost') }),
PORT: port({ devDefault: testOnly(3000) }),
-
PUBLIC_URL: str({}),
+
PUBLIC_URL: url({ default: undefined }),
DB_PATH: str({ devDefault: ':memory:' }),
COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }),
-
COMMON_RATE_LIMIT_MAX_REQUESTS: num({ devDefault: testOnly(1000) }),
-
COMMON_RATE_LIMIT_WINDOW_MS: num({ devDefault: testOnly(1000) }),
+
PRIVATE_KEYS: keys({ default: undefined }),
+
LOG_LEVEL: str({
+
devDefault: 'debug',
+
default: 'info',
+
choices: ['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent'],
+
}),
+
PDS_URL: url({ default: undefined }),
+
PLC_URL: url({ default: undefined }),
+
FIREHOSE_URL: url({ default: undefined }),
})
-189
src/firehose/firehose.ts
···
-
import type { RepoRecord } from '@atproto/lexicon'
-
import { cborToLexRecord, readCar } from '@atproto/repo'
-
import { AtUri } from '@atproto/syntax'
-
import { Subscription } from '@atproto/xrpc-server'
-
import type { CID } from 'multiformats/cid'
-
import {
-
type Account,
-
type Commit,
-
type Identity,
-
type RepoEvent,
-
isAccount,
-
isCommit,
-
isIdentity,
-
isValidRepoEvent,
-
} from './lexicons'
-
-
type Opts = {
-
service?: string
-
getCursor?: () => Promise<number | undefined>
-
setCursor?: (cursor: number) => Promise<void>
-
subscriptionReconnectDelay?: number
-
filterCollections?: string[]
-
excludeIdentity?: boolean
-
excludeAccount?: boolean
-
excludeCommit?: boolean
-
}
-
-
export class Firehose {
-
public sub: Subscription<RepoEvent>
-
private abortController: AbortController
-
-
constructor(public opts: Opts) {
-
this.abortController = new AbortController()
-
this.sub = new Subscription({
-
service: opts.service ?? 'https://bsky.network',
-
method: 'com.atproto.sync.subscribeRepos',
-
signal: this.abortController.signal,
-
getParams: async () => {
-
if (!opts.getCursor) return undefined
-
const cursor = await opts.getCursor()
-
return { cursor }
-
},
-
validate: (value: unknown) => {
-
try {
-
return isValidRepoEvent(value)
-
} catch (err) {
-
console.error('repo subscription skipped invalid message', err)
-
}
-
},
-
})
-
}
-
-
async *run(): AsyncGenerator<Event> {
-
try {
-
for await (const evt of this.sub) {
-
try {
-
if (isCommit(evt) && !this.opts.excludeCommit) {
-
const parsed = await parseCommit(evt)
-
for (const write of parsed) {
-
if (!this.opts.filterCollections || this.opts.filterCollections.includes(write.uri.collection)) {
-
yield write
-
}
-
}
-
} else if (isAccount(evt) && !this.opts.excludeAccount) {
-
const parsed = parseAccount(evt)
-
if (parsed) {
-
yield parsed
-
}
-
} else if (isIdentity(evt) && !this.opts.excludeIdentity) {
-
yield parseIdentity(evt)
-
}
-
} catch (err) {
-
console.error('repo subscription could not handle message', err)
-
}
-
if (this.opts.setCursor && typeof evt.seq === 'number') {
-
await this.opts.setCursor(evt.seq)
-
}
-
}
-
} catch (err) {
-
console.error('repo subscription errored', err)
-
setTimeout(() => this.run(), this.opts.subscriptionReconnectDelay ?? 3000)
-
}
-
}
-
-
destroy() {
-
this.abortController.abort()
-
}
-
}
-
-
export const parseCommit = async (evt: Commit): Promise<CommitEvt[]> => {
-
const car = await readCar(evt.blocks)
-
-
const evts: CommitEvt[] = []
-
-
for (const op of evt.ops) {
-
const uri = new AtUri(`at://${evt.repo}/${op.path}`)
-
-
const meta: CommitMeta = {
-
uri,
-
author: uri.host,
-
collection: uri.collection,
-
rkey: uri.rkey,
-
}
-
-
if (op.action === 'create' || op.action === 'update') {
-
if (!op.cid) continue
-
const recordBytes = car.blocks.get(op.cid)
-
if (!recordBytes) continue
-
const record = cborToLexRecord(recordBytes)
-
evts.push({
-
...meta,
-
event: op.action as 'create' | 'update',
-
cid: op.cid,
-
record,
-
})
-
}
-
-
if (op.action === 'delete') {
-
evts.push({
-
...meta,
-
event: 'delete',
-
})
-
}
-
}
-
-
return evts
-
}
-
-
export const parseIdentity = (evt: Identity): IdentityEvt => {
-
return {
-
event: 'identity',
-
did: evt.did,
-
handle: evt.handle,
-
}
-
}
-
-
export const parseAccount = (evt: Account): AccountEvt | undefined => {
-
if (evt.status && !isValidStatus(evt.status)) return
-
return {
-
event: 'account',
-
did: evt.did,
-
active: evt.active,
-
status: evt.status as AccountStatus,
-
}
-
}
-
-
const isValidStatus = (str: string): str is AccountStatus => {
-
return ['takendown', 'suspended', 'deleted', 'deactivated'].includes(str)
-
}
-
-
type Event = CommitEvt | IdentityEvt | AccountEvt
-
-
type CommitMeta = {
-
uri: AtUri
-
author: string
-
collection: string
-
rkey: string
-
}
-
-
type CommitEvt = Create | Update | Delete
-
-
type Create = CommitMeta & {
-
event: 'create'
-
record: RepoRecord
-
cid: CID
-
}
-
-
type Update = CommitMeta & {
-
event: 'update'
-
}
-
-
type Delete = CommitMeta & {
-
event: 'delete'
-
}
-
-
type IdentityEvt = {
-
event: 'identity'
-
did: string
-
handle?: string
-
}
-
-
type AccountEvt = {
-
event: 'account'
-
did: string
-
active: boolean
-
status?: AccountStatus
-
}
-
-
type AccountStatus = 'takendown' | 'suspended' | 'deleted' | 'deactivated'
-38
src/firehose/ingester.ts
···
-
import type { Database } from '#/db'
-
import { Firehose } from '#/firehose/firehose'
-
import * as Status from '#/lexicon/types/com/example/status'
-
-
export class Ingester {
-
firehose: Firehose | undefined
-
constructor(public db: Database) {}
-
-
async start() {
-
const firehose = new Firehose({})
-
-
for await (const evt of firehose.run()) {
-
if (evt.event === 'create') {
-
const record = evt.record
-
if (
-
evt.collection === 'com.example.status' &&
-
Status.isRecord(record) &&
-
Status.validateRecord(record).success
-
) {
-
await this.db
-
.insertInto('status')
-
.values({
-
authorDid: evt.author,
-
status: record.status,
-
updatedAt: record.updatedAt,
-
indexedAt: new Date().toISOString(),
-
})
-
.onConflict((oc) => oc.doNothing())
-
.execute()
-
}
-
}
-
}
-
}
-
-
destroy() {
-
this.firehose?.destroy()
-
}
-
}
-355
src/firehose/lexicons.ts
···
-
import type { IncomingMessage } from 'node:http'
-
-
import { type LexiconDoc, Lexicons } from '@atproto/lexicon'
-
import type { ErrorFrame, HandlerAuth } from '@atproto/xrpc-server'
-
import type { CID } from 'multiformats/cid'
-
-
// @NOTE: this file is an ugly copy job of codegen output. I'd like to clean this whole thing up
-
-
export function isObj(v: unknown): v is Record<string, unknown> {
-
return typeof v === 'object' && v !== null
-
}
-
-
export function hasProp<K extends PropertyKey>(data: object, prop: K): data is Record<K, unknown> {
-
return prop in data
-
}
-
-
export interface QueryParams {
-
/** The last known event seq number to backfill from. */
-
cursor?: number
-
}
-
-
export type RepoEvent =
-
| Commit
-
| Identity
-
| Account
-
| Handle
-
| Migrate
-
| Tombstone
-
| Info
-
| { $type: string; [k: string]: unknown }
-
export type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'>
-
export type HandlerOutput = HandlerError | RepoEvent
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
-
auth: HA
-
params: QueryParams
-
req: IncomingMessage
-
signal: AbortSignal
-
}
-
export type Handler<HA extends HandlerAuth = never> = (ctx: HandlerReqCtx<HA>) => AsyncIterable<HandlerOutput>
-
-
/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */
-
export interface Commit {
-
/** The stream sequence number of this message. */
-
seq: number
-
/** DEPRECATED -- unused */
-
rebase: boolean
-
/** Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */
-
tooBig: boolean
-
/** The repo this event comes from. */
-
repo: string
-
/** Repo commit object CID. */
-
commit: CID
-
/** DEPRECATED -- unused. WARNING -- nullable and optional; stick with optional to ensure golang interoperability. */
-
prev?: CID | null
-
/** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */
-
rev: string
-
/** The rev of the last emitted commit from this repo (if any). */
-
since: string | null
-
/** CAR file containing relevant blocks, as a diff since the previous repo state. */
-
blocks: Uint8Array
-
ops: RepoOp[]
-
blobs: CID[]
-
/** Timestamp of when this message was originally broadcast. */
-
time: string
-
[k: string]: unknown
-
}
-
-
export function isCommit(v: unknown): v is Commit {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#commit'
-
}
-
-
/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */
-
export interface Identity {
-
seq: number
-
did: string
-
time: string
-
/** The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. */
-
handle?: string
-
[k: string]: unknown
-
}
-
-
export function isIdentity(v: unknown): v is Identity {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#identity'
-
}
-
-
/** Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active. */
-
export interface Account {
-
seq: number
-
did: string
-
time: string
-
/** Indicates that the account has a repository which can be fetched from the host that emitted this event. */
-
active: boolean
-
/** If active=false, this optional field indicates a reason for why the account is not active. */
-
status?: 'takendown' | 'suspended' | 'deleted' | 'deactivated' | (string & {})
-
[k: string]: unknown
-
}
-
-
export function isAccount(v: unknown): v is Account {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#account'
-
}
-
-
/** DEPRECATED -- Use #identity event instead */
-
export interface Handle {
-
seq: number
-
did: string
-
handle: string
-
time: string
-
[k: string]: unknown
-
}
-
-
export function isHandle(v: unknown): v is Handle {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#handle'
-
}
-
-
/** DEPRECATED -- Use #account event instead */
-
export interface Migrate {
-
seq: number
-
did: string
-
migrateTo: string | null
-
time: string
-
[k: string]: unknown
-
}
-
-
export function isMigrate(v: unknown): v is Migrate {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#migrate'
-
}
-
-
/** DEPRECATED -- Use #account event instead */
-
export interface Tombstone {
-
seq: number
-
did: string
-
time: string
-
[k: string]: unknown
-
}
-
-
export function isTombstone(v: unknown): v is Tombstone {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#tombstone'
-
}
-
-
export interface Info {
-
name: 'OutdatedCursor' | (string & {})
-
message?: string
-
[k: string]: unknown
-
}
-
-
export function isInfo(v: unknown): v is Info {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#info'
-
}
-
-
/** A repo operation, ie a mutation of a single record. */
-
export interface RepoOp {
-
action: 'create' | 'update' | 'delete' | (string & {})
-
path: string
-
/** For creates and updates, the new record CID. For deletions, null. */
-
cid: CID | null
-
[k: string]: unknown
-
}
-
-
export function isRepoOp(v: unknown): v is RepoOp {
-
return isObj(v) && hasProp(v, '$type') && v.$type === 'com.atproto.sync.subscribeRepos#repoOp'
-
}
-
-
export const ComAtprotoSyncSubscribeRepos: LexiconDoc = {
-
lexicon: 1,
-
id: 'com.atproto.sync.subscribeRepos',
-
defs: {
-
main: {
-
type: 'subscription',
-
description: 'Subscribe to repo updates',
-
parameters: {
-
type: 'params',
-
properties: {
-
cursor: {
-
type: 'integer',
-
description: 'The last known event to backfill from.',
-
},
-
},
-
},
-
message: {
-
schema: {
-
type: 'union',
-
refs: [
-
'lex:com.atproto.sync.subscribeRepos#commit',
-
'lex:com.atproto.sync.subscribeRepos#handle',
-
'lex:com.atproto.sync.subscribeRepos#migrate',
-
'lex:com.atproto.sync.subscribeRepos#tombstone',
-
'lex:com.atproto.sync.subscribeRepos#info',
-
],
-
},
-
},
-
errors: [
-
{
-
name: 'FutureCursor',
-
},
-
{
-
name: 'ConsumerTooSlow',
-
},
-
],
-
},
-
commit: {
-
type: 'object',
-
required: ['seq', 'rebase', 'tooBig', 'repo', 'commit', 'rev', 'since', 'blocks', 'ops', 'blobs', 'time'],
-
nullable: ['prev', 'since'],
-
properties: {
-
seq: {
-
type: 'integer',
-
},
-
rebase: {
-
type: 'boolean',
-
},
-
tooBig: {
-
type: 'boolean',
-
},
-
repo: {
-
type: 'string',
-
format: 'did',
-
},
-
commit: {
-
type: 'cid-link',
-
},
-
prev: {
-
type: 'cid-link',
-
},
-
rev: {
-
type: 'string',
-
description: 'The rev of the emitted commit',
-
},
-
since: {
-
type: 'string',
-
description: 'The rev of the last emitted commit from this repo',
-
},
-
blocks: {
-
type: 'bytes',
-
description: 'CAR file containing relevant blocks',
-
maxLength: 1000000,
-
},
-
ops: {
-
type: 'array',
-
items: {
-
type: 'ref',
-
ref: 'lex:com.atproto.sync.subscribeRepos#repoOp',
-
},
-
maxLength: 200,
-
},
-
blobs: {
-
type: 'array',
-
items: {
-
type: 'cid-link',
-
},
-
},
-
time: {
-
type: 'string',
-
format: 'datetime',
-
},
-
},
-
},
-
handle: {
-
type: 'object',
-
required: ['seq', 'did', 'handle', 'time'],
-
properties: {
-
seq: {
-
type: 'integer',
-
},
-
did: {
-
type: 'string',
-
format: 'did',
-
},
-
handle: {
-
type: 'string',
-
format: 'handle',
-
},
-
time: {
-
type: 'string',
-
format: 'datetime',
-
},
-
},
-
},
-
migrate: {
-
type: 'object',
-
required: ['seq', 'did', 'migrateTo', 'time'],
-
nullable: ['migrateTo'],
-
properties: {
-
seq: {
-
type: 'integer',
-
},
-
did: {
-
type: 'string',
-
format: 'did',
-
},
-
migrateTo: {
-
type: 'string',
-
},
-
time: {
-
type: 'string',
-
format: 'datetime',
-
},
-
},
-
},
-
tombstone: {
-
type: 'object',
-
required: ['seq', 'did', 'time'],
-
properties: {
-
seq: {
-
type: 'integer',
-
},
-
did: {
-
type: 'string',
-
format: 'did',
-
},
-
time: {
-
type: 'string',
-
format: 'datetime',
-
},
-
},
-
},
-
info: {
-
type: 'object',
-
required: ['name'],
-
properties: {
-
name: {
-
type: 'string',
-
knownValues: ['OutdatedCursor'],
-
},
-
message: {
-
type: 'string',
-
},
-
},
-
},
-
repoOp: {
-
type: 'object',
-
description:
-
"A repo operation, ie a write of a single record. For creates and updates, cid is the record's CID as of this operation. For deletes, it's null.",
-
required: ['action', 'path', 'cid'],
-
nullable: ['cid'],
-
properties: {
-
action: {
-
type: 'string',
-
knownValues: ['create', 'update', 'delete'],
-
},
-
path: {
-
type: 'string',
-
},
-
cid: {
-
type: 'cid-link',
-
},
-
},
-
},
-
},
-
}
-
-
const lexicons = new Lexicons([ComAtprotoSyncSubscribeRepos])
-
-
export const isValidRepoEvent = (evt: unknown) => {
-
return lexicons.assertValidXrpcMessage<RepoEvent>('com.atproto.sync.subscribeRepos', evt)
-
}
+37
src/id-resolver.ts
···
+
import { OAuthClient } from '@atproto/oauth-client-node'
+
+
export interface BidirectionalResolver {
+
resolveDidToHandle(did: string): Promise<string | undefined>
+
resolveDidsToHandles(
+
dids: string[],
+
): Promise<Record<string, string | undefined>>
+
}
+
+
export function createBidirectionalResolver({
+
identityResolver,
+
}: OAuthClient): BidirectionalResolver {
+
return {
+
async resolveDidToHandle(did: string): Promise<string | undefined> {
+
try {
+
const { handle } = await identityResolver.resolve(did)
+
if (handle) return handle
+
} catch {
+
// Ignore
+
}
+
},
+
+
async resolveDidsToHandles(
+
dids: string[],
+
): Promise<Record<string, string | undefined>> {
+
const uniqueDids = [...new Set(dids)]
+
+
return Object.fromEntries(
+
await Promise.all(
+
uniqueDids.map((did) =>
+
this.resolveDidToHandle(did).then((handle) => [did, handle]),
+
),
+
),
+
)
+
},
+
}
+
}
-34
src/ident/resolver.ts
···
-
import { IdResolver, MemoryCache } from '@atproto/identity'
-
-
const HOUR = 60e3 * 60
-
const DAY = HOUR * 24
-
-
export function createResolver() {
-
const resolver = new IdResolver({
-
didCache: new MemoryCache(HOUR, DAY),
-
})
-
-
return {
-
async resolveDidToHandle(did: string): Promise<string> {
-
const didDoc = await resolver.did.resolveAtprotoData(did)
-
const resolvedHandle = await resolver.handle.resolve(didDoc.handle)
-
if (resolvedHandle === did) {
-
return didDoc.handle
-
}
-
return did
-
},
-
-
async resolveDidsToHandles(
-
dids: string[]
-
): Promise<Record<string, string>> {
-
const didHandleMap: Record<string, string> = {}
-
const resolves = await Promise.all(
-
dids.map((did) => this.resolveDidToHandle(did).catch((_) => did))
-
)
-
for (let i = 0; i < dids.length; i++) {
-
didHandleMap[dids[i]] = resolves[i]
-
}
-
return didHandleMap
-
},
-
}
-
}
-4
src/ident/types.ts
···
-
export interface Resolver {
-
resolveDidToHandle(did: string): Promise<string>
-
resolveDidsToHandles(dids: string[]): Promise<Record<string, string>>
-
}
+30 -12
src/index.ts
···
-
import { Server } from '#/server'
+
import { once } from 'node:events'
-
const run = async () => {
-
const server = await Server.create()
+
import { createAppContext } from '#/context'
+
import { env } from '#/env'
+
import { startServer } from '#/lib/http'
+
import { run } from '#/lib/process'
+
import { createRouter } from '#/routes'
-
const onCloseSignal = async () => {
-
setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s
-
await server.close()
-
process.exit()
-
}
+
run(async (killSignal) => {
+
// Create the application context
+
const ctx = await createAppContext()
-
process.on('SIGINT', onCloseSignal)
-
process.on('SIGTERM', onCloseSignal)
-
}
+
// Create the HTTP router
+
const router = createRouter(ctx)
-
run()
+
// Start the HTTP server
+
const { terminate } = await startServer(router, { port: env.PORT })
+
+
const url = env.PUBLIC_URL || `http://localhost:${env.PORT}`
+
ctx.logger.info(`Server (${env.NODE_ENV}) running at ${url}`)
+
+
// Subscribe to events on the firehose
+
ctx.ingester.start()
+
+
// Wait for a termination signal
+
if (!killSignal.aborted) await once(killSignal, 'abort')
+
ctx.logger.info(`Signal received, shutting down...`)
+
+
// Gracefully shutdown the http server
+
await terminate()
+
+
// Gracefully shutdown the application context
+
await ctx.destroy()
+
})
+77
src/ingester.ts
···
+
import type { Database } from '#/db'
+
import * as Status from '#/lexicon/types/xyz/statusphere/status'
+
import { IdResolver, MemoryCache } from '@atproto/identity'
+
import { Event, Firehose } from '@atproto/sync'
+
import pino from 'pino'
+
import { env } from './env'
+
+
const HOUR = 60e3 * 60
+
const DAY = HOUR * 24
+
+
export function createIngester(db: Database) {
+
const logger = pino({ name: 'firehose', level: env.LOG_LEVEL })
+
return new Firehose({
+
filterCollections: ['xyz.statusphere.status'],
+
handleEvent: async (evt: Event) => {
+
// Watch for write events
+
if (evt.event === 'create' || evt.event === 'update') {
+
const now = new Date()
+
const record = evt.record
+
+
// If the write is a valid status update
+
if (
+
evt.collection === 'xyz.statusphere.status' &&
+
Status.isRecord(record) &&
+
Status.validateRecord(record).success
+
) {
+
logger.debug(
+
{ uri: evt.uri.toString(), status: record.status },
+
'ingesting status',
+
)
+
+
// Store the status in our SQLite
+
await db
+
.insertInto('status')
+
.values({
+
uri: evt.uri.toString(),
+
authorDid: evt.did,
+
status: record.status,
+
createdAt: record.createdAt,
+
indexedAt: now.toISOString(),
+
})
+
.onConflict((oc) =>
+
oc.column('uri').doUpdateSet({
+
status: record.status,
+
indexedAt: now.toISOString(),
+
}),
+
)
+
.execute()
+
}
+
} else if (
+
evt.event === 'delete' &&
+
evt.collection === 'xyz.statusphere.status'
+
) {
+
logger.debug(
+
{ uri: evt.uri.toString(), did: evt.did },
+
'deleting status',
+
)
+
+
// Remove the status from our SQLite
+
await db
+
.deleteFrom('status')
+
.where('uri', '=', evt.uri.toString())
+
.execute()
+
}
+
},
+
onError: (err: unknown) => {
+
logger.error({ err }, 'error on firehose ingestion')
+
},
+
excludeIdentity: true,
+
excludeAccount: true,
+
service: env.FIREHOSE_URL,
+
idResolver: new IdResolver({
+
plcUrl: env.PLC_URL,
+
didCache: new MemoryCache(HOUR, DAY),
+
}),
+
})
+
}
+63 -3
src/lexicon/index.ts
···
export class Server {
xrpc: XrpcServer
+
app: AppNS
+
xyz: XyzNS
com: ComNS
constructor(options?: XrpcOptions) {
this.xrpc = createXrpcServer(schemas, options)
+
this.app = new AppNS(this)
+
this.xyz = new XyzNS(this)
this.com = new ComNS(this)
}
}
+
export class AppNS {
+
_server: Server
+
bsky: AppBskyNS
+
+
constructor(server: Server) {
+
this._server = server
+
this.bsky = new AppBskyNS(server)
+
}
+
}
+
+
export class AppBskyNS {
+
_server: Server
+
actor: AppBskyActorNS
+
+
constructor(server: Server) {
+
this._server = server
+
this.actor = new AppBskyActorNS(server)
+
}
+
}
+
+
export class AppBskyActorNS {
+
_server: Server
+
+
constructor(server: Server) {
+
this._server = server
+
}
+
}
+
+
export class XyzNS {
+
_server: Server
+
statusphere: XyzStatusphereNS
+
+
constructor(server: Server) {
+
this._server = server
+
this.statusphere = new XyzStatusphereNS(server)
+
}
+
}
+
+
export class XyzStatusphereNS {
+
_server: Server
+
+
constructor(server: Server) {
+
this._server = server
+
}
+
}
+
export class ComNS {
_server: Server
-
example: ComExampleNS
+
atproto: ComAtprotoNS
constructor(server: Server) {
this._server = server
-
this.example = new ComExampleNS(server)
+
this.atproto = new ComAtprotoNS(server)
}
}
-
export class ComExampleNS {
+
export class ComAtprotoNS {
+
_server: Server
+
repo: ComAtprotoRepoNS
+
+
constructor(server: Server) {
+
this._server = server
+
this.repo = new ComAtprotoRepoNS(server)
+
}
+
}
+
+
export class ComAtprotoRepoNS {
_server: Server
constructor(server: Server) {
+265 -5
src/lexicon/lexicons.ts
···
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
export const schemaDict = {
-
ComExampleStatus: {
+
ComAtprotoLabelDefs: {
lexicon: 1,
-
id: 'com.example.status',
+
id: 'com.atproto.label.defs',
+
defs: {
+
label: {
+
type: 'object',
+
description:
+
'Metadata tag on an atproto resource (eg, repo or record).',
+
required: ['src', 'uri', 'val', 'cts'],
+
properties: {
+
ver: {
+
type: 'integer',
+
description: 'The AT Protocol version of the label object.',
+
},
+
src: {
+
type: 'string',
+
format: 'did',
+
description: 'DID of the actor who created this label.',
+
},
+
uri: {
+
type: 'string',
+
format: 'uri',
+
description:
+
'AT URI of the record, repository (account), or other resource that this label applies to.',
+
},
+
cid: {
+
type: 'string',
+
format: 'cid',
+
description:
+
"Optionally, CID specifying the specific version of 'uri' resource this label applies to.",
+
},
+
val: {
+
type: 'string',
+
maxLength: 128,
+
description:
+
'The short string name of the value or type of this label.',
+
},
+
neg: {
+
type: 'boolean',
+
description:
+
'If true, this is a negation label, overwriting a previous label.',
+
},
+
cts: {
+
type: 'string',
+
format: 'datetime',
+
description: 'Timestamp when this label was created.',
+
},
+
exp: {
+
type: 'string',
+
format: 'datetime',
+
description:
+
'Timestamp at which this label expires (no longer applies).',
+
},
+
sig: {
+
type: 'bytes',
+
description: 'Signature of dag-cbor encoded label.',
+
},
+
},
+
},
+
selfLabels: {
+
type: 'object',
+
description:
+
'Metadata tags on an atproto record, published by the author within the record.',
+
required: ['values'],
+
properties: {
+
values: {
+
type: 'array',
+
items: {
+
type: 'ref',
+
ref: 'lex:com.atproto.label.defs#selfLabel',
+
},
+
maxLength: 10,
+
},
+
},
+
},
+
selfLabel: {
+
type: 'object',
+
description:
+
'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',
+
required: ['val'],
+
properties: {
+
val: {
+
type: 'string',
+
maxLength: 128,
+
description:
+
'The short string name of the value or type of this label.',
+
},
+
},
+
},
+
labelValueDefinition: {
+
type: 'object',
+
description:
+
'Declares a label value and its expected interpretations and behaviors.',
+
required: ['identifier', 'severity', 'blurs', 'locales'],
+
properties: {
+
identifier: {
+
type: 'string',
+
description:
+
"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
+
maxLength: 100,
+
maxGraphemes: 100,
+
},
+
severity: {
+
type: 'string',
+
description:
+
"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
+
knownValues: ['inform', 'alert', 'none'],
+
},
+
blurs: {
+
type: 'string',
+
description:
+
"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
+
knownValues: ['content', 'media', 'none'],
+
},
+
defaultSetting: {
+
type: 'string',
+
description: 'The default setting for this label.',
+
knownValues: ['ignore', 'warn', 'hide'],
+
default: 'warn',
+
},
+
adultOnly: {
+
type: 'boolean',
+
description:
+
'Does the user need to have adult content enabled in order to configure this label?',
+
},
+
locales: {
+
type: 'array',
+
items: {
+
type: 'ref',
+
ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',
+
},
+
},
+
},
+
},
+
labelValueDefinitionStrings: {
+
type: 'object',
+
description:
+
'Strings which describe the label in the UI, localized into a specific language.',
+
required: ['lang', 'name', 'description'],
+
properties: {
+
lang: {
+
type: 'string',
+
description:
+
'The code of the language these strings are written in.',
+
format: 'language',
+
},
+
name: {
+
type: 'string',
+
description: 'A short human-readable name for the label.',
+
maxGraphemes: 64,
+
maxLength: 640,
+
},
+
description: {
+
type: 'string',
+
description:
+
'A longer description of what the label means and why it might be applied.',
+
maxGraphemes: 10000,
+
maxLength: 100000,
+
},
+
},
+
},
+
labelValue: {
+
type: 'string',
+
knownValues: [
+
'!hide',
+
'!no-promote',
+
'!warn',
+
'!no-unauthenticated',
+
'dmca-violation',
+
'doxxing',
+
'porn',
+
'sexual',
+
'nudity',
+
'nsfl',
+
'gore',
+
],
+
},
+
},
+
},
+
AppBskyActorProfile: {
+
lexicon: 1,
+
id: 'app.bsky.actor.profile',
defs: {
main: {
type: 'record',
+
description: 'A declaration of a Bluesky account profile.',
key: 'literal:self',
record: {
type: 'object',
-
required: ['status', 'updatedAt'],
+
properties: {
+
displayName: {
+
type: 'string',
+
maxGraphemes: 64,
+
maxLength: 640,
+
},
+
description: {
+
type: 'string',
+
description: 'Free-form profile description text.',
+
maxGraphemes: 256,
+
maxLength: 2560,
+
},
+
avatar: {
+
type: 'blob',
+
description:
+
"Small image to be displayed next to posts from account. AKA, 'profile picture'",
+
accept: ['image/png', 'image/jpeg'],
+
maxSize: 1000000,
+
},
+
banner: {
+
type: 'blob',
+
description:
+
'Larger horizontal image to display behind profile view.',
+
accept: ['image/png', 'image/jpeg'],
+
maxSize: 1000000,
+
},
+
labels: {
+
type: 'union',
+
description:
+
'Self-label values, specific to the Bluesky application, on the overall account.',
+
refs: ['lex:com.atproto.label.defs#selfLabels'],
+
},
+
joinedViaStarterPack: {
+
type: 'ref',
+
ref: 'lex:com.atproto.repo.strongRef',
+
},
+
createdAt: {
+
type: 'string',
+
format: 'datetime',
+
},
+
},
+
},
+
},
+
},
+
},
+
XyzStatusphereStatus: {
+
lexicon: 1,
+
id: 'xyz.statusphere.status',
+
defs: {
+
main: {
+
type: 'record',
+
key: 'tid',
+
record: {
+
type: 'object',
+
required: ['status', 'createdAt'],
properties: {
status: {
type: 'string',
···
maxGraphemes: 1,
maxLength: 32,
},
-
updatedAt: {
+
createdAt: {
type: 'string',
format: 'datetime',
},
···
},
},
},
+
ComAtprotoRepoStrongRef: {
+
lexicon: 1,
+
id: 'com.atproto.repo.strongRef',
+
description: 'A URI with a content-hash fingerprint.',
+
defs: {
+
main: {
+
type: 'object',
+
required: ['uri', 'cid'],
+
properties: {
+
uri: {
+
type: 'string',
+
format: 'at-uri',
+
},
+
cid: {
+
type: 'string',
+
format: 'cid',
+
},
+
},
+
},
+
},
+
},
}
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
export const lexicons: Lexicons = new Lexicons(schemas)
-
export const ids = { ComExampleStatus: 'com.example.status' }
+
export const ids = {
+
ComAtprotoLabelDefs: 'com.atproto.label.defs',
+
AppBskyActorProfile: 'app.bsky.actor.profile',
+
XyzStatusphereStatus: 'xyz.statusphere.status',
+
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
+
}
+38
src/lexicon/types/app/bsky/actor/profile.ts
···
+
/**
+
* GENERATED CODE - DO NOT MODIFY
+
*/
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
+
import { lexicons } from '../../../../lexicons'
+
import { isObj, hasProp } from '../../../../util'
+
import { CID } from 'multiformats/cid'
+
import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs'
+
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
+
+
export interface Record {
+
displayName?: string
+
/** Free-form profile description text. */
+
description?: string
+
/** Small image to be displayed next to posts from account. AKA, 'profile picture' */
+
avatar?: BlobRef
+
/** Larger horizontal image to display behind profile view. */
+
banner?: BlobRef
+
labels?:
+
| ComAtprotoLabelDefs.SelfLabels
+
| { $type: string; [k: string]: unknown }
+
joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main
+
createdAt?: string
+
[k: string]: unknown
+
}
+
+
export function isRecord(v: unknown): v is Record {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
(v.$type === 'app.bsky.actor.profile#main' ||
+
v.$type === 'app.bsky.actor.profile')
+
)
+
}
+
+
export function validateRecord(v: unknown): ValidationResult {
+
return lexicons.validate('app.bsky.actor.profile#main', v)
+
}
+151
src/lexicon/types/com/atproto/label/defs.ts
···
+
/**
+
* GENERATED CODE - DO NOT MODIFY
+
*/
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
+
import { lexicons } from '../../../../lexicons'
+
import { isObj, hasProp } from '../../../../util'
+
import { CID } from 'multiformats/cid'
+
+
/** Metadata tag on an atproto resource (eg, repo or record). */
+
export interface Label {
+
/** The AT Protocol version of the label object. */
+
ver?: number
+
/** DID of the actor who created this label. */
+
src: string
+
/** AT URI of the record, repository (account), or other resource that this label applies to. */
+
uri: string
+
/** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */
+
cid?: string
+
/** The short string name of the value or type of this label. */
+
val: string
+
/** If true, this is a negation label, overwriting a previous label. */
+
neg?: boolean
+
/** Timestamp when this label was created. */
+
cts: string
+
/** Timestamp at which this label expires (no longer applies). */
+
exp?: string
+
/** Signature of dag-cbor encoded label. */
+
sig?: Uint8Array
+
[k: string]: unknown
+
}
+
+
export function isLabel(v: unknown): v is Label {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
v.$type === 'com.atproto.label.defs#label'
+
)
+
}
+
+
export function validateLabel(v: unknown): ValidationResult {
+
return lexicons.validate('com.atproto.label.defs#label', v)
+
}
+
+
/** Metadata tags on an atproto record, published by the author within the record. */
+
export interface SelfLabels {
+
values: SelfLabel[]
+
[k: string]: unknown
+
}
+
+
export function isSelfLabels(v: unknown): v is SelfLabels {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
v.$type === 'com.atproto.label.defs#selfLabels'
+
)
+
}
+
+
export function validateSelfLabels(v: unknown): ValidationResult {
+
return lexicons.validate('com.atproto.label.defs#selfLabels', v)
+
}
+
+
/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */
+
export interface SelfLabel {
+
/** The short string name of the value or type of this label. */
+
val: string
+
[k: string]: unknown
+
}
+
+
export function isSelfLabel(v: unknown): v is SelfLabel {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
v.$type === 'com.atproto.label.defs#selfLabel'
+
)
+
}
+
+
export function validateSelfLabel(v: unknown): ValidationResult {
+
return lexicons.validate('com.atproto.label.defs#selfLabel', v)
+
}
+
+
/** Declares a label value and its expected interpretations and behaviors. */
+
export interface LabelValueDefinition {
+
/** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */
+
identifier: string
+
/** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */
+
severity: 'inform' | 'alert' | 'none' | (string & {})
+
/** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */
+
blurs: 'content' | 'media' | 'none' | (string & {})
+
/** The default setting for this label. */
+
defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})
+
/** Does the user need to have adult content enabled in order to configure this label? */
+
adultOnly?: boolean
+
locales: LabelValueDefinitionStrings[]
+
[k: string]: unknown
+
}
+
+
export function isLabelValueDefinition(v: unknown): v is LabelValueDefinition {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
v.$type === 'com.atproto.label.defs#labelValueDefinition'
+
)
+
}
+
+
export function validateLabelValueDefinition(v: unknown): ValidationResult {
+
return lexicons.validate('com.atproto.label.defs#labelValueDefinition', v)
+
}
+
+
/** Strings which describe the label in the UI, localized into a specific language. */
+
export interface LabelValueDefinitionStrings {
+
/** The code of the language these strings are written in. */
+
lang: string
+
/** A short human-readable name for the label. */
+
name: string
+
/** A longer description of what the label means and why it might be applied. */
+
description: string
+
[k: string]: unknown
+
}
+
+
export function isLabelValueDefinitionStrings(
+
v: unknown,
+
): v is LabelValueDefinitionStrings {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
v.$type === 'com.atproto.label.defs#labelValueDefinitionStrings'
+
)
+
}
+
+
export function validateLabelValueDefinitionStrings(
+
v: unknown,
+
): ValidationResult {
+
return lexicons.validate(
+
'com.atproto.label.defs#labelValueDefinitionStrings',
+
v,
+
)
+
}
+
+
export type LabelValue =
+
| '!hide'
+
| '!no-promote'
+
| '!warn'
+
| '!no-unauthenticated'
+
| 'dmca-violation'
+
| 'doxxing'
+
| 'porn'
+
| 'sexual'
+
| 'nudity'
+
| 'nsfl'
+
| 'gore'
+
| (string & {})
+26
src/lexicon/types/com/atproto/repo/strongRef.ts
···
+
/**
+
* GENERATED CODE - DO NOT MODIFY
+
*/
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
+
import { lexicons } from '../../../../lexicons'
+
import { isObj, hasProp } from '../../../../util'
+
import { CID } from 'multiformats/cid'
+
+
export interface Main {
+
uri: string
+
cid: string
+
[k: string]: unknown
+
}
+
+
export function isMain(v: unknown): v is Main {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
(v.$type === 'com.atproto.repo.strongRef#main' ||
+
v.$type === 'com.atproto.repo.strongRef')
+
)
+
}
+
+
export function validateMain(v: unknown): ValidationResult {
+
return lexicons.validate('com.atproto.repo.strongRef#main', v)
+
}
-25
src/lexicon/types/com/example/status.ts
···
-
/**
-
* GENERATED CODE - DO NOT MODIFY
-
*/
-
import { ValidationResult, BlobRef } from '@atproto/lexicon'
-
import { lexicons } from '../../../lexicons'
-
import { isObj, hasProp } from '../../../util'
-
import { CID } from 'multiformats/cid'
-
-
export interface Record {
-
status: string
-
updatedAt: string
-
[k: string]: unknown
-
}
-
-
export function isRecord(v: unknown): v is Record {
-
return (
-
isObj(v) &&
-
hasProp(v, '$type') &&
-
(v.$type === 'com.example.status#main' || v.$type === 'com.example.status')
-
)
-
}
-
-
export function validateRecord(v: unknown): ValidationResult {
-
return lexicons.validate('com.example.status#main', v)
-
}
+26
src/lexicon/types/xyz/statusphere/status.ts
···
+
/**
+
* GENERATED CODE - DO NOT MODIFY
+
*/
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
+
import { lexicons } from '../../../lexicons'
+
import { isObj, hasProp } from '../../../util'
+
import { CID } from 'multiformats/cid'
+
+
export interface Record {
+
status: string
+
createdAt: string
+
[k: string]: unknown
+
}
+
+
export function isRecord(v: unknown): v is Record {
+
return (
+
isObj(v) &&
+
hasProp(v, '$type') &&
+
(v.$type === 'xyz.statusphere.status#main' ||
+
v.$type === 'xyz.statusphere.status')
+
)
+
}
+
+
export function validateRecord(v: unknown): ValidationResult {
+
return lexicons.validate('xyz.statusphere.status#main', v)
+
}
+59
src/lib/http.ts
···
+
import { Request, Response } from 'express'
+
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 Middleware<
+
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse = ServerResponse,
+
> = (req: Req, res: Res, next: NextFunction) => void
+
+
export type Handler<
+
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse = ServerResponse,
+
> = (req: Req, res: Res) => unknown | Promise<unknown>
+
/**
+
* Wraps a request handler middleware to ensure that `next` is called if it
+
* throws or returns a promise that rejects.
+
*/
+
export function handler<
+
Req extends IncomingMessage = Request,
+
Res extends ServerResponse = Response,
+
>(fn: Handler<Req, Res>): Middleware<Req, Res> {
+
return async (req, res, next) => {
+
try {
+
await fn(req, res)
+
} catch (err) {
+
next(err)
+
}
+
}
+
}
+
+
/**
+
* Create an HTTP server with the provided request listener, ensuring that it
+
* can bind the listening port, and returns a termination function that allows
+
* graceful termination of HTTP connections.
+
*/
+
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 }
+
}
+17
src/lib/jwk.ts
···
+
import { Jwk, jwkValidator } from '@atproto/oauth-client-node'
+
import { makeValidator } from 'envalid'
+
import { z } from 'zod'
+
+
export type JsonWebKey = Jwk & { kid: string }
+
+
const jsonWebKeySchema = z.intersection(
+
jwkValidator,
+
z.object({ kid: z.string().nonempty() }),
+
) satisfies z.ZodType<JsonWebKey>
+
+
const jsonWebKeysSchema = z.array(jsonWebKeySchema).nonempty()
+
+
export const envalidJsonWebKeys = makeValidator((input) => {
+
const value = JSON.parse(input)
+
return jsonWebKeysSchema.parse(value)
+
})
+24
src/lib/process.ts
···
+
const SIGNALS = ['SIGINT', 'SIGTERM'] as const
+
+
/**
+
* Runs a function with an abort signal that will be triggered when the process
+
* receives a termination signal.
+
*/
+
export async function run<F extends (signal: AbortSignal) => Promise<void>>(
+
fn: F,
+
): Promise<void> {
+
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 {
+
await fn(killController.signal)
+
} finally {
+
abort()
+
}
+
}
+4
src/lib/util.ts
···
+
export function ifString<T>(value: T): (T & string) | undefined {
+
if (typeof value === 'string') return value
+
return undefined
+
}
+12
src/lib/view.ts
···
+
// @ts-ignore
+
import ssr from 'uhtml/ssr'
+
import type initSSR from 'uhtml/types/init-ssr'
+
import type { Hole } from 'uhtml/types/keyed'
+
+
export type { Hole }
+
+
export const { html }: ReturnType<typeof initSSR> = ssr()
+
+
export function page(hole: Hole) {
+
return `<!DOCTYPE html>\n${hole.toDOM().toString()}`
+
}
-12
src/middleware/errorHandler.ts
···
-
import type { ErrorRequestHandler, RequestHandler } from 'express'
-
-
const unexpectedRequest: RequestHandler = (_req, res) => {
-
res.sendStatus(404)
-
}
-
-
const addErrorToRequestLog: ErrorRequestHandler = (err, _req, res, next) => {
-
res.locals.err = err
-
next(err)
-
}
-
-
export default () => [unexpectedRequest, addErrorToRequestLog]
-100
src/middleware/requestLogger.ts
···
-
import { randomUUID } from 'node:crypto'
-
import type { IncomingMessage, ServerResponse } from 'node:http'
-
import type { Request, RequestHandler, Response } from 'express'
-
import type { LevelWithSilent } from 'pino'
-
import { type CustomAttributeKeys, type Options, pinoHttp } from 'pino-http'
-
-
import { env } from '#/env'
-
-
enum LogLevel {
-
Fatal = 'fatal',
-
Error = 'error',
-
Warn = 'warn',
-
Info = 'info',
-
Debug = 'debug',
-
Trace = 'trace',
-
Silent = 'silent',
-
}
-
-
type PinoCustomProps = {
-
request: Request
-
response: Response
-
error: Error
-
responseBody: unknown
-
}
-
-
const requestLogger = (options?: Options): RequestHandler[] => {
-
const pinoOptions: Options = {
-
enabled: env.isProduction,
-
customProps: customProps as unknown as Options['customProps'],
-
redact: [],
-
genReqId,
-
customLogLevel,
-
customSuccessMessage,
-
customReceivedMessage: (req) => `request received: ${req.method}`,
-
customErrorMessage: (_req, res) =>
-
`request errored with status code: ${res.statusCode}`,
-
customAttributeKeys,
-
...options,
-
}
-
return [responseBodyMiddleware, pinoHttp(pinoOptions)]
-
}
-
-
const customAttributeKeys: CustomAttributeKeys = {
-
req: 'request',
-
res: 'response',
-
err: 'error',
-
responseTime: 'timeTaken',
-
}
-
-
const customProps = (req: Request, res: Response): PinoCustomProps => ({
-
request: req,
-
response: res,
-
error: res.locals.err,
-
responseBody: res.locals.responseBody,
-
})
-
-
const responseBodyMiddleware: RequestHandler = (_req, res, next) => {
-
const isNotProduction = !env.isProduction
-
if (isNotProduction) {
-
const originalSend = res.send
-
res.send = (content) => {
-
res.locals.responseBody = content
-
res.send = originalSend
-
return originalSend.call(res, content)
-
}
-
}
-
next()
-
}
-
-
const customLogLevel = (
-
_req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>,
-
err?: Error
-
): LevelWithSilent => {
-
if (err || res.statusCode >= 500) return LogLevel.Error
-
if (res.statusCode >= 400) return LogLevel.Warn
-
if (res.statusCode >= 300) return LogLevel.Silent
-
return LogLevel.Info
-
}
-
-
const customSuccessMessage = (
-
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>
-
) => {
-
if (res.statusCode === 404) return 'Not found'
-
return `${req.method} completed`
-
}
-
-
const genReqId = (
-
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>
-
) => {
-
const existingID = req.id ?? req.headers['x-request-id']
-
if (existingID) return existingID
-
const id = randomUUID()
-
res.setHeader('X-Request-Id', id)
-
return id
-
}
-
-
export default requestLogger()
+15 -18
src/pages/home.ts
···
-
import { AtUri } from '@atproto/syntax'
-
import type { Status } from '#/db/schema'
-
import { html } from '../view'
+
import type { Status } from '#/db'
+
import { html } from '../lib/view'
import { shell } from './shell'
const TODAY = new Date().toDateString()
-
const STATUS_OPTIONS = [
+
export const STATUS_OPTIONS = [
'๐Ÿ‘',
'๐Ÿ‘Ž',
'๐Ÿ’™',
'๐Ÿฅน',
'๐Ÿ˜ง',
-
'๐Ÿ˜ค',
'๐Ÿ™ƒ',
'๐Ÿ˜‰',
'๐Ÿ˜Ž',
···
type Props = {
statuses: Status[]
-
didHandleMap: Record<string, string>
-
profile?: { displayName?: string; handle: string }
+
didHandleMap: Record<string, string | undefined>
+
profile?: { displayName?: string }
myStatus?: Status
}
···
${profile
? html`<form action="/logout" method="post" class="session-form">
<div>
-
Hi, <strong>${profile.displayName || profile.handle}</strong>.
-
what's your status today?
+
Hi, <strong>${profile.displayName || 'friend'}</strong>. What's
+
your status today?
</div>
<div>
<button type="submit">Log out</button>
···
</div>
</div>`}
</div>
-
<div class="status-options">
+
<form action="/status" method="post" class="status-options">
${STATUS_OPTIONS.map(
(status) =>
-
html`<div
+
html`<button
class=${myStatus?.status === status
? 'status-option selected'
: 'status-option'}
-
data-value="${status}"
-
data-authed=${profile ? '1' : '0'}
+
name="status"
+
value="${status}"
>
${status}
-
</div>`
+
</button>`,
)}
-
</div>
+
</form>
${statuses.map((status, i) => {
const handle = didHandleMap[status.authorDid] || status.authorDid
const date = ts(status)
···
`
})}
</div>
-
<script src="/public/home.js"></script>
</div>`
}
···
}
function ts(status: Status) {
+
const createdAt = new Date(status.createdAt)
const indexedAt = new Date(status.indexedAt)
-
const updatedAt = new Date(status.updatedAt)
-
if (updatedAt > indexedAt) return updatedAt.toDateString()
+
if (createdAt < indexedAt) return createdAt.toDateString()
return indexedAt.toDateString()
}
+15 -7
src/pages/login.ts
···
-
import { html } from '../view'
+
import { env } from '#/env'
+
import { html } from '../lib/view'
import { shell } from './shell'
type Props = { error?: string }
···
}
function content({ error }: Props) {
+
const signupService =
+
!env.PDS_URL || env.PDS_URL === 'https://bsky.social'
+
? 'Bluesky'
+
: new URL(env.PDS_URL).hostname
+
return html`<div id="root">
<div id="header">
<h1>Statusphere</h1>
···
<form action="/login" method="post" class="login-form">
<input
type="text"
-
name="handle"
+
name="input"
placeholder="Enter your handle (eg alice.bsky.social)"
required
/>
+
<button type="submit">Log in</button>
-
${error ? html`<p>Error: <i>${error}</i></p>` : undefined}
</form>
-
<div class="signup-cta">
-
Don't have an account on the Atmosphere?
-
<a href="https://bsky.app">Sign up for Bluesky</a> to create one now!
-
</div>
+
+
<a href="/signup" class="button signup-cta">
+
Login or Sign up with a ${signupService} account
+
</a>
+
+
${error ? html`<p>Error: <i>${error}</i></p>` : undefined}
</div>
</div>`
}
+231
src/pages/public/styles.css
···
+
body {
+
font-family: Arial, Helvetica, sans-serif;
+
+
--border-color: #ddd;
+
--gray-100: #fafafa;
+
--gray-500: #666;
+
--gray-700: #333;
+
--primary-100: #d2e7ff;
+
--primary-200: #b1d3fa;
+
--primary-400: #2e8fff;
+
--primary-500: #0078ff;
+
--primary-600: #0066db;
+
--error-500: #f00;
+
--error-100: #fee;
+
}
+
+
/*
+
Josh's Custom CSS Reset
+
https://www.joshwcomeau.com/css/custom-css-reset/
+
*/
+
*,
+
*::before,
+
*::after {
+
box-sizing: border-box;
+
}
+
* {
+
margin: 0;
+
}
+
body {
+
line-height: 1.5;
+
-webkit-font-smoothing: antialiased;
+
}
+
img,
+
picture,
+
video,
+
canvas,
+
svg {
+
display: block;
+
max-width: 100%;
+
}
+
input,
+
button,
+
textarea,
+
select {
+
font: inherit;
+
}
+
p,
+
h1,
+
h2,
+
h3,
+
h4,
+
h5,
+
h6 {
+
overflow-wrap: break-word;
+
}
+
#root,
+
#__next {
+
isolation: isolate;
+
}
+
+
/*
+
Common components
+
*/
+
button,
+
.button {
+
display: inline-block;
+
border: 0;
+
background-color: var(--primary-500);
+
border-radius: 50px;
+
color: #fff;
+
padding: 2px 10px;
+
cursor: pointer;
+
text-decoration: none;
+
}
+
button:hover,
+
.button:hover {
+
background: var(--primary-400);
+
}
+
+
/*
+
Custom components
+
*/
+
.error {
+
background-color: var(--error-100);
+
color: var(--error-500);
+
text-align: center;
+
padding: 1rem;
+
display: none;
+
}
+
.error.visible {
+
display: block;
+
}
+
+
#header {
+
background-color: #fff;
+
text-align: center;
+
padding: 0.5rem 0 1.5rem;
+
}
+
+
#header h1 {
+
font-size: 5rem;
+
}
+
+
.container {
+
display: flex;
+
flex-direction: column;
+
gap: 4px;
+
margin: 0 auto;
+
max-width: 600px;
+
padding: 20px;
+
}
+
+
.card {
+
/* border: 1px solid var(--border-color); */
+
border-radius: 6px;
+
padding: 10px 16px;
+
background-color: #fff;
+
}
+
.card > :first-child {
+
margin-top: 0;
+
}
+
.card > :last-child {
+
margin-bottom: 0;
+
}
+
+
.session-form {
+
display: flex;
+
flex-direction: row;
+
align-items: center;
+
justify-content: space-between;
+
}
+
+
.login-form {
+
display: flex;
+
flex-direction: row;
+
gap: 6px;
+
border: 1px solid var(--border-color);
+
border-radius: 6px;
+
padding: 10px 16px;
+
background-color: #fff;
+
}
+
+
.login-form input {
+
flex: 1;
+
border: 0;
+
}
+
+
.status-options {
+
display: flex;
+
flex-direction: row;
+
flex-wrap: wrap;
+
gap: 8px;
+
margin: 10px 0;
+
}
+
+
.status-option {
+
font-size: 2rem;
+
width: 3rem;
+
height: 3rem;
+
padding: 0;
+
background-color: #fff;
+
border: 1px solid var(--border-color);
+
border-radius: 3rem;
+
text-align: center;
+
box-shadow: 0 1px 4px #0001;
+
cursor: pointer;
+
}
+
+
.status-option:hover {
+
background-color: var(--primary-100);
+
box-shadow: 0 0 0 1px var(--primary-400);
+
}
+
+
.status-option.selected {
+
box-shadow: 0 0 0 1px var(--primary-500);
+
background-color: var(--primary-100);
+
}
+
+
.status-option.selected:hover {
+
background-color: var(--primary-200);
+
}
+
+
.status-line {
+
display: flex;
+
flex-direction: row;
+
align-items: center;
+
gap: 10px;
+
position: relative;
+
margin-top: 15px;
+
}
+
+
.status-line:not(.no-line)::before {
+
content: '';
+
position: absolute;
+
width: 2px;
+
background-color: var(--border-color);
+
left: 1.45rem;
+
bottom: calc(100% + 2px);
+
height: 15px;
+
}
+
+
.status-line .status {
+
font-size: 2rem;
+
background-color: #fff;
+
width: 3rem;
+
height: 3rem;
+
border-radius: 1.5rem;
+
text-align: center;
+
border: 1px solid var(--border-color);
+
}
+
+
.status-line .desc {
+
color: var(--gray-500);
+
}
+
+
.status-line .author {
+
color: var(--gray-700);
+
font-weight: 600;
+
text-decoration: none;
+
}
+
+
.status-line .author:hover {
+
text-decoration: underline;
+
}
+
+
.signup-cta {
+
text-align: center;
+
width: 100%;
+
display: block;
+
margin-top: 1rem;
+
}
+1 -1
src/pages/shell.ts
···
-
import { type Hole, html } from '../view'
+
import { type Hole, html } from '../lib/view'
export function shell({ title, content }: { title: string; content: Hole }) {
return html`<html>
-32
src/public/home.js
···
-
Array.from(document.querySelectorAll('.status-option'), (el) => {
-
el.addEventListener('click', async (ev) => {
-
setError('')
-
-
if (el.dataset.authed !== '1') {
-
window.location = '/login'
-
return
-
}
-
-
const res = await fetch('/status', {
-
method: 'POST',
-
headers: { 'content-type': 'application/json' },
-
body: JSON.stringify({ status: el.dataset.value }),
-
})
-
const body = await res.json()
-
if (body?.error) {
-
setError(body.error)
-
} else {
-
location.reload()
-
}
-
})
-
})
-
-
function setError(str) {
-
const errMsg = document.querySelector('.error')
-
if (str) {
-
errMsg.classList.add('visible')
-
errMsg.textContent = str
-
} else {
-
errMsg.classList.remove('visible')
-
}
-
}
-211
src/public/styles.css
···
-
body {
-
font-family: Arial, Helvetica, sans-serif;
-
-
--border-color: #ddd;
-
--gray-100: #fafafa;
-
--gray-500: #666;
-
--gray-700: #333;
-
--primary-100: #d2e7ff;
-
--primary-200: #b1d3fa;
-
--primary-400: #2e8fff;
-
--primary-500: #0078ff;
-
--primary-600: #0066db;
-
--error-500: #f00;
-
--error-100: #fee;
-
}
-
-
/*
-
Josh's Custom CSS Reset
-
https://www.joshwcomeau.com/css/custom-css-reset/
-
*/
-
*, *::before, *::after {
-
box-sizing: border-box;
-
}
-
* {
-
margin: 0;
-
}
-
body {
-
line-height: 1.5;
-
-webkit-font-smoothing: antialiased;
-
}
-
img, picture, video, canvas, svg {
-
display: block;
-
max-width: 100%;
-
}
-
input, button, textarea, select {
-
font: inherit;
-
}
-
p, h1, h2, h3, h4, h5, h6 {
-
overflow-wrap: break-word;
-
}
-
#root, #__next {
-
isolation: isolate;
-
}
-
-
/*
-
Common components
-
*/
-
button, .button {
-
display: inline-block;
-
border: 0;
-
background-color: var(--primary-500);
-
border-radius: 50px;
-
color: #fff;
-
padding: 2px 10px;
-
cursor: pointer;
-
text-decoration: none;
-
}
-
button:hover, .button:hover {
-
background: var(--primary-400);
-
}
-
-
/*
-
Custom components
-
*/
-
.error {
-
background-color: var(--error-100);
-
color: var(--error-500);
-
text-align: center;
-
padding: 1rem;
-
display: none;
-
}
-
.error.visible {
-
display: block;
-
}
-
-
#header {
-
background-color: #fff;
-
text-align: center;
-
padding: 0.5rem 0 1.5rem;
-
}
-
-
#header h1 {
-
font-size: 5rem;
-
}
-
-
.container {
-
display: flex;
-
flex-direction: column;
-
gap: 4px;
-
margin: 0 auto;
-
max-width: 600px;
-
padding: 20px;
-
}
-
-
.card {
-
/* border: 1px solid var(--border-color); */
-
border-radius: 6px;
-
padding: 10px 16px;
-
background-color: #fff;
-
}
-
.card > :first-child {
-
margin-top: 0;
-
}
-
.card > :last-child {
-
margin-bottom: 0;
-
}
-
-
.session-form {
-
display: flex;
-
flex-direction: row;
-
align-items: center;
-
justify-content: space-between;
-
}
-
-
.login-form {
-
display: flex;
-
flex-direction: row;
-
gap: 6px;
-
border: 1px solid var(--border-color);
-
border-radius: 6px;
-
padding: 10px 16px;
-
background-color: #fff;
-
}
-
-
.login-form input {
-
flex: 1;
-
border: 0;
-
}
-
-
.status-options {
-
display: flex;
-
flex-direction: row;
-
flex-wrap: wrap;
-
gap: 8px;
-
margin: 10px 0;
-
}
-
-
.status-option {
-
font-size: 2rem;
-
width: 3rem;
-
height: 3rem;
-
background-color: #fff;
-
border: 1px solid var(--border-color);
-
border-radius: 3rem;
-
text-align: center;
-
box-shadow: 0 1px 4px #0001;
-
cursor: pointer;
-
}
-
-
.status-option:hover {
-
background-color: var(--primary-100);
-
box-shadow: 0 0 0 1px var(--primary-400);
-
}
-
-
.status-option.selected {
-
box-shadow: 0 0 0 1px var(--primary-500);
-
background-color: var(--primary-100);
-
}
-
-
.status-option.selected:hover {
-
background-color: var(--primary-200);
-
}
-
-
.status-line {
-
display: flex;
-
flex-direction: row;
-
align-items: center;
-
gap: 10px;
-
position: relative;
-
margin-top: 15px;
-
}
-
-
.status-line:not(.no-line)::before {
-
content: '';
-
position: absolute;
-
width: 2px;
-
background-color: var(--border-color);
-
left: 1.45rem;
-
bottom: calc(100% + 2px);
-
height: 15px;
-
}
-
-
.status-line .status {
-
font-size: 2rem;
-
background-color: #fff;
-
width: 3rem;
-
height: 3rem;
-
border-radius: 1.5rem;
-
text-align: center;
-
border: 1px solid var(--border-color);
-
}
-
-
.status-line .desc {
-
color: var(--gray-500);
-
}
-
-
.status-line .author {
-
color: var(--gray-700);
-
font-weight: 600;
-
text-decoration: none;
-
}
-
-
.status-line .author:hover {
-
text-decoration: underline;
-
}
-
-
.signup-cta {
-
text-align: center;
-
text-wrap: balance;
-
margin-top: 1rem;
-
}
-172
src/routes/index.ts
···
-
import path from 'node:path'
-
import { OAuthResolverError } from '@atproto/oauth-client-node'
-
import { isValidHandle } from '@atproto/syntax'
-
import express from 'express'
-
import { createSession, destroySession, getSessionAgent } from '#/auth/session'
-
import type { AppContext } from '#/config'
-
import { home } from '#/pages/home'
-
import { login } from '#/pages/login'
-
import { page } from '#/view'
-
import { handler } from './util'
-
import * as Status from '#/lexicon/types/com/example/status'
-
-
export const createRouter = (ctx: AppContext) => {
-
const router = express.Router()
-
-
router.use('/public', express.static(path.join(__dirname, '..', 'public')))
-
-
router.get(
-
'/client-metadata.json',
-
handler((_req, res) => {
-
return res.json(ctx.oauthClient.clientMetadata)
-
})
-
)
-
-
router.get(
-
'/oauth/callback',
-
handler(async (req, res) => {
-
const params = new URLSearchParams(req.originalUrl.split('?')[1])
-
try {
-
const { agent } = await ctx.oauthClient.callback(params)
-
await createSession(req, res, agent.accountDid)
-
} catch (err) {
-
ctx.logger.error({ err }, 'oauth callback failed')
-
return res.redirect('/?error')
-
}
-
return res.redirect('/')
-
})
-
)
-
-
router.get(
-
'/login',
-
handler(async (_req, res) => {
-
return res.type('html').send(page(login({})))
-
})
-
)
-
-
router.post(
-
'/login',
-
handler(async (req, res) => {
-
const handle = req.body?.handle
-
if (typeof handle !== 'string' || !isValidHandle(handle)) {
-
return res.type('html').send(page(login({ error: 'invalid handle' })))
-
}
-
try {
-
const url = await ctx.oauthClient.authorize(handle)
-
return res.redirect(url.toString())
-
} catch (err) {
-
ctx.logger.error({ err }, 'oauth authorize failed')
-
return res.type('html').send(
-
page(
-
login({
-
error:
-
err instanceof OAuthResolverError
-
? err.message
-
: "couldn't initiate login",
-
})
-
)
-
)
-
}
-
})
-
)
-
-
router.post(
-
'/logout',
-
handler(async (req, res) => {
-
await destroySession(req, res)
-
return res.redirect('/')
-
})
-
)
-
-
router.get(
-
'/',
-
handler(async (req, res) => {
-
const agent = await getSessionAgent(req, res, ctx)
-
const statuses = await ctx.db
-
.selectFrom('status')
-
.selectAll()
-
.orderBy('indexedAt', 'desc')
-
.limit(10)
-
.execute()
-
const myStatus = agent
-
? await ctx.db
-
.selectFrom('status')
-
.selectAll()
-
.where('authorDid', '=', agent.accountDid)
-
.executeTakeFirst()
-
: undefined
-
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
-
statuses.map((s) => s.authorDid)
-
)
-
if (!agent) {
-
return res.type('html').send(page(home({ statuses, didHandleMap })))
-
}
-
const { data: profile } = await agent.getProfile({
-
actor: agent.accountDid,
-
})
-
return res
-
.type('html')
-
.send(page(home({ statuses, didHandleMap, profile, myStatus })))
-
})
-
)
-
-
router.post(
-
'/status',
-
handler(async (req, res) => {
-
const agent = await getSessionAgent(req, res, ctx)
-
if (!agent) {
-
return res.status(401).json({ error: 'Session required' })
-
}
-
-
const record = {
-
$type: 'com.example.status',
-
status: req.body?.status,
-
updatedAt: new Date().toISOString(),
-
}
-
if (!Status.validateRecord(record).success) {
-
return res.status(400).json({ error: 'Invalid status' })
-
}
-
-
try {
-
await agent.com.atproto.repo.putRecord({
-
repo: agent.accountDid,
-
collection: 'com.example.status',
-
rkey: 'self',
-
record,
-
validate: false,
-
})
-
} catch (err) {
-
ctx.logger.warn({ err }, 'failed to write record')
-
return res.status(500).json({ error: 'Failed to write record' })
-
}
-
-
try {
-
await ctx.db
-
.insertInto('status')
-
.values({
-
authorDid: agent.accountDid,
-
status: record.status,
-
updatedAt: record.updatedAt,
-
indexedAt: new Date().toISOString(),
-
})
-
.onConflict((oc) =>
-
oc.column('authorDid').doUpdateSet({
-
status: record.status,
-
updatedAt: record.updatedAt,
-
indexedAt: new Date().toISOString(),
-
})
-
)
-
.execute()
-
} catch (err) {
-
ctx.logger.warn(
-
{ err },
-
'failed to update computed view; ignoring as it should be caught by the firehose'
-
)
-
}
-
-
res.status(200).json({})
-
})
-
)
-
-
return router
-
}
-10
src/routes/util.ts
···
-
import type express from 'express'
-
-
export 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)
-
}
-
}
+353
src/routes.ts
···
+
import { Agent } from '@atproto/api'
+
import { TID } from '@atproto/common'
+
import { OAuthResolverError } from '@atproto/oauth-client-node'
+
import express, { Request, Response } from 'express'
+
import { getIronSession } from 'iron-session'
+
import type {
+
IncomingMessage,
+
RequestListener,
+
ServerResponse,
+
} from 'node:http'
+
import path from 'node:path'
+
+
import type { AppContext } from '#/context'
+
import { env } from '#/env'
+
import * as Profile from '#/lexicon/types/app/bsky/actor/profile'
+
import * as Status from '#/lexicon/types/xyz/statusphere/status'
+
import { handler } from '#/lib/http'
+
import { ifString } from '#/lib/util'
+
import { page } from '#/lib/view'
+
import { home } from '#/pages/home'
+
import { login } from '#/pages/login'
+
+
// Max age, in seconds, for static routes and assets
+
const MAX_AGE = env.NODE_ENV === 'production' ? 60 : 0
+
+
type Session = { did?: string }
+
+
// Helper function to get the Atproto Agent for the active session
+
async function getSessionAgent(
+
req: IncomingMessage,
+
res: ServerResponse,
+
ctx: AppContext,
+
) {
+
res.setHeader('Vary', 'Cookie')
+
+
const session = await getIronSession<Session>(req, res, {
+
cookieName: 'sid',
+
password: env.COOKIE_SECRET,
+
})
+
if (!session.did) return null
+
+
// This page is dynamic and should not be cached publicly
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, private`)
+
+
try {
+
const oauthSession = await ctx.oauthClient.restore(session.did)
+
return oauthSession ? new Agent(oauthSession) : null
+
} catch (err) {
+
ctx.logger.warn({ err }, 'oauth restore failed')
+
await session.destroy()
+
return null
+
}
+
}
+
+
export const createRouter = (ctx: AppContext): RequestListener => {
+
const router = express()
+
+
// Static assets
+
router.use(
+
'/public',
+
express.static(path.join(__dirname, 'pages', 'public'), {
+
maxAge: MAX_AGE * 1000,
+
}),
+
)
+
+
// OAuth metadata
+
router.get(
+
'/oauth-client-metadata.json',
+
handler((req, res) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
+
res.json(ctx.oauthClient.clientMetadata)
+
}),
+
)
+
+
// Public keys
+
router.get(
+
'/.well-known/jwks.json',
+
handler((req, res) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
+
res.json(ctx.oauthClient.jwks)
+
}),
+
)
+
+
// OAuth callback to complete session creation
+
router.get(
+
'/oauth/callback',
+
handler(async (req, res) => {
+
res.setHeader('cache-control', 'no-store')
+
+
const params = new URLSearchParams(req.originalUrl.split('?')[1])
+
try {
+
// Load the session cookie
+
const session = await getIronSession<Session>(req, res, {
+
cookieName: 'sid',
+
password: env.COOKIE_SECRET,
+
})
+
+
// If the user is already signed in, destroy the old credentials
+
if (session.did) {
+
try {
+
const oauthSession = await ctx.oauthClient.restore(session.did)
+
if (oauthSession) oauthSession.signOut()
+
} catch (err) {
+
ctx.logger.warn({ err }, 'oauth restore failed')
+
}
+
}
+
+
// Complete the OAuth flow
+
const oauth = await ctx.oauthClient.callback(params)
+
+
// Update the session cookie
+
session.did = oauth.session.did
+
+
await session.save()
+
} catch (err) {
+
ctx.logger.error({ err }, 'oauth callback failed')
+
}
+
+
return res.redirect('/')
+
}),
+
)
+
+
// Login page
+
router.get(
+
'/login',
+
handler(async (req, res) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
+
res.type('html').send(page(login({})))
+
}),
+
)
+
+
// Login handler
+
router.post(
+
'/login',
+
express.urlencoded(),
+
handler(async (req, res) => {
+
// Never store this route
+
res.setHeader('cache-control', 'no-store')
+
+
// Initiate the OAuth flow
+
try {
+
// Validate input: can be a handle, a DID or a service URL (PDS).
+
const input = ifString(req.body.input)
+
if (!input) {
+
throw new Error('Invalid input')
+
}
+
+
// Initiate the OAuth flow
+
const url = await ctx.oauthClient.authorize(input, {
+
scope: 'atproto transition:generic',
+
})
+
+
res.redirect(url.toString())
+
} catch (err) {
+
ctx.logger.error({ err }, 'oauth authorize failed')
+
+
const error = err instanceof Error ? err.message : 'unexpected error'
+
+
return res.type('html').send(page(login({ error })))
+
}
+
}),
+
)
+
+
// Signup
+
router.get(
+
'/signup',
+
handler(async (req, res) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
+
+
try {
+
const service = env.PDS_URL ?? 'https://bsky.social'
+
const url = await ctx.oauthClient.authorize(service, {
+
scope: 'atproto transition:generic',
+
})
+
res.redirect(url.toString())
+
} catch (err) {
+
ctx.logger.error({ err }, 'oauth authorize failed')
+
res.type('html').send(
+
page(
+
login({
+
error:
+
err instanceof OAuthResolverError
+
? err.message
+
: "couldn't initiate login",
+
}),
+
),
+
)
+
}
+
}),
+
)
+
+
// Logout handler
+
router.post(
+
'/logout',
+
handler(async (req, res) => {
+
// Never store this route
+
res.setHeader('cache-control', 'no-store')
+
+
const session = await getIronSession<Session>(req, res, {
+
cookieName: 'sid',
+
password: env.COOKIE_SECRET,
+
})
+
+
// Revoke credentials on the server
+
if (session.did) {
+
try {
+
const oauthSession = await ctx.oauthClient.restore(session.did)
+
if (oauthSession) await oauthSession.signOut()
+
} catch (err) {
+
ctx.logger.warn({ err }, 'Failed to revoke credentials')
+
}
+
}
+
+
session.destroy()
+
+
return res.redirect('/')
+
}),
+
)
+
+
// Homepage
+
router.get(
+
'/',
+
handler(async (req, res) => {
+
// If the user is signed in, get an agent which communicates with their server
+
const agent = await getSessionAgent(req, res, ctx)
+
+
// Fetch data stored in our SQLite
+
const statuses = await ctx.db
+
.selectFrom('status')
+
.selectAll()
+
.orderBy('indexedAt', 'desc')
+
.limit(10)
+
.execute()
+
const myStatus = agent
+
? await ctx.db
+
.selectFrom('status')
+
.selectAll()
+
.where('authorDid', '=', agent.assertDid)
+
.orderBy('indexedAt', 'desc')
+
.executeTakeFirst()
+
: undefined
+
+
// Map user DIDs to their domain-name handles
+
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
+
statuses.map((s) => s.authorDid),
+
)
+
+
if (!agent) {
+
// Serve the logged-out view
+
return 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 profileRecord = profileResponse?.data
+
+
const profile =
+
profileRecord &&
+
Profile.isRecord(profileRecord.value) &&
+
Profile.validateRecord(profileRecord.value).success
+
? profileRecord.value
+
: {}
+
+
// Serve the logged-in view
+
res
+
.type('html')
+
.send(page(home({ statuses, didHandleMap, profile, myStatus })))
+
}),
+
)
+
+
// "Set status" handler
+
router.post(
+
'/status',
+
express.urlencoded(),
+
handler(async (req, res) => {
+
// 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
+
.status(401)
+
.type('html')
+
.send('<h1>Error: Session required</h1>')
+
}
+
+
// Construct their status record
+
const record = {
+
$type: 'xyz.statusphere.status',
+
status: req.body?.status,
+
createdAt: new Date().toISOString(),
+
}
+
+
// Make sure the record generated from the input is valid
+
if (!Status.validateRecord(record).success) {
+
return res
+
.status(400)
+
.type('html')
+
.send('<h1>Error: Invalid status</h1>')
+
}
+
+
let uri
+
try {
+
// Write the status record to the user's repository
+
const res = await agent.com.atproto.repo.putRecord({
+
repo: agent.assertDid,
+
collection: 'xyz.statusphere.status',
+
rkey: TID.nextStr(),
+
record,
+
validate: false,
+
})
+
uri = res.data.uri
+
} catch (err) {
+
ctx.logger.warn({ err }, 'failed to write record')
+
return res
+
.status(500)
+
.type('html')
+
.send('<h1>Error: Failed to write record</h1>')
+
}
+
+
try {
+
// Optimistically update our SQLite
+
// This isn't strictly necessary because the write event will be
+
// handled in #/firehose/ingestor.ts, but it ensures that future reads
+
// will be up-to-date after this method finishes.
+
await ctx.db
+
.insertInto('status')
+
.values({
+
uri,
+
authorDid: agent.assertDid,
+
status: record.status,
+
createdAt: record.createdAt,
+
indexedAt: new Date().toISOString(),
+
})
+
.execute()
+
} catch (err) {
+
ctx.logger.warn(
+
{ err },
+
'failed to update computed view; ignoring as it should be caught by the firehose',
+
)
+
}
+
+
return res.redirect('/')
+
}),
+
)
+
+
return router
+
}
-80
src/server.ts
···
-
import events from 'node:events'
-
import type http from 'node:http'
-
import express, { type Express } from 'express'
-
import { pino } from 'pino'
-
-
import { createDb, migrateToLatest } from '#/db'
-
import { env } from '#/env'
-
import { Ingester } from '#/firehose/ingester'
-
import errorHandler from '#/middleware/errorHandler'
-
import requestLogger from '#/middleware/requestLogger'
-
import { createRouter } from '#/routes'
-
import { createClient } from '#/auth/client'
-
import { createResolver } from '#/ident/resolver'
-
import type { AppContext } from '#/config'
-
-
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' })
-
const db = createDb(DB_PATH)
-
await migrateToLatest(db)
-
const ingester = new Ingester(db)
-
const oauthClient = await createClient(db)
-
const resolver = createResolver()
-
ingester.start()
-
const ctx = {
-
db,
-
ingester,
-
logger,
-
oauthClient,
-
resolver,
-
}
-
-
const app: Express = express()
-
-
// Set the application to trust the reverse proxy
-
app.set('trust proxy', true)
-
-
// TODO: middleware for sqlite server
-
// TODO: middleware for OAuth
-
-
// Middlewares
-
app.use(express.json())
-
app.use(express.urlencoded({ extended: true }))
-
-
// Request logging
-
app.use(requestLogger)
-
-
// Routes
-
const router = createRouter(ctx)
-
app.use(router)
-
-
// Error handlers
-
app.use(errorHandler())
-
-
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)
-
}
-
-
async close() {
-
this.ctx.logger.info('sigint received, shutting down')
-
this.ctx.ingester.destroy()
-
return new Promise<void>((resolve) => {
-
this.server.close(() => {
-
this.ctx.logger.info('server closed')
-
resolve()
-
})
-
})
-
}
-
}
-12
src/view.ts
···
-
// @ts-ignore
-
import ssr from 'uhtml/ssr'
-
import type initSSR from 'uhtml/types/init-ssr'
-
import type { Hole } from 'uhtml/types/keyed'
-
-
export type { Hole }
-
-
export const { html }: ReturnType<typeof initSSR> = ssr()
-
-
export function page(hole: Hole) {
-
return `<!DOCTYPE html>\n${hole.toDOM().toString()}`
-
}