Scratch space for learning atproto app development

Compare changes

Choose any two refs to compare.

+12 -7
.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
+
+
# 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.
-644
TUTORIAL.md
···
-
# Tutorial
-
-
In this guide, we're going to build a simple multi-user app that publishes your current "status" as an emoji.
-
-
![A screenshot of our example application](./docs/app-screenshot.png)
-
-
At various points we will cover how to:
-
-
- 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
-
-
We're going to keep this light so you can quickly wrap your head around ATProto. There will be links with more information about each step.
-
-
## Where are we going?
-
-
Data in the Atmosphere is stored on users' personal repos. It's almost like each user has their own website. Our goal is to aggregate data from the users into our SQLite DB.
-
-
Think of our app like a Google. If Google's job was to say which emoji each website had under `/status.json`, then it would show something like:
-
-
- `nytimes.com` is feeling ๐Ÿ“ฐ according to `https://nytimes.com/status.json`
-
- `bsky.app` is feeling ๐Ÿฆ‹ according to `https://bsky.app/status.json`
-
- `reddit.com` is feeling ๐Ÿค“ according to `https://reddit.com/status.json`
-
-
The Atmosphere works the same way, except we're going to check `at://` instead of `https://`. Each user has a data repo under an `at://` URL. We'll crawl all the `at://`s in the Atmosphere for all the "status.json" records and aggregate them into our SQLite database.
-
-
> `at://` is the URL scheme of the AT Protocol. Under the hood it uses common tech like HTTP and DNS, but it adds all of the features we'll be using in this tutorial.
-
-
## Step 1. Starting with our ExpressJS app
-
-
Start by cloning the repo and installing packages.
-
-
```bash
-
git clone TODO
-
cd TODO
-
npm i
-
npm run dev # you can leave this running and it will auto-reload
-
```
-
-
Our repo is a regular Web app. We're rendering our HTML server-side like it's 1999. We also have a SQLite database that we're managing with [Kysley](https://kysely.dev/).
-
-
Our starting stack:
-
-
- Typescript
-
- NodeJS web server ([express](https://expressjs.com/))
-
- SQLite database ([Kysley](https://kysely.dev/))
-
- Server-side rendering ([uhtml](https://www.npmjs.com/package/uhtml))
-
-
With each step we'll explain how our Web app taps into the Atmosphere. Refer to the codebase for more detailed code — again, this tutorial is going to keep it light and quick to digest.
-
-
## Step 2. Signing in with OAuth
-
-
When somebody logs into our app, they'll give us read & write access to their personal `at://` repo. We'll use that to write the `status.json` record.
-
-
We're going to accomplish this using OAuth ([spec](https://github.com/bluesky-social/proposals/tree/main/0004-oauth)). Most of the OAuth flows are going to be handled for us using the [@atproto/oauth-client-node](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node) library. This is the arrangement we're aiming toward:
-
-
![A diagram of the OAuth elements](./docs/diagram-oauth.png)
-
-
When the user logs in, the OAuth client will create a new session with their repo server and give us read/write access along with basic user info.
-
-
![A screenshot of the login UI](./docs/app-login.png)
-
-
Our login page just asks the user for their "handle," which is the domain name associated with their account. For [Bluesky](https://bsky.app) users, these tend to look like `alice.bsky.social`, but they can be any kind of domain (eg `alice.com`).
-
-
```html
-
<!-- src/pages/login.ts -->
-
<form action="/login" method="post" class="login-form">
-
<input
-
type="text"
-
name="handle"
-
placeholder="Enter your handle (eg alice.bsky.social)"
-
required
-
/>
-
<button type="submit">Log in</button>
-
</form>
-
```
-
-
When they submit the form, we tell our OAuth client to initiate the authorization flow and then redirect the user to their server to complete the process.
-
-
```typescript
-
/** src/routes.ts **/
-
// Login handler
-
router.post(
-
'/login',
-
handler(async (req, res) => {
-
// Initiate the OAuth flow
-
const url = await oauthClient.authorize(handle)
-
return res.redirect(url.toString())
-
})
-
)
-
```
-
-
This is the same kind of SSO flow that Google or GitHub uses. The user will be asked for their password, then asked to confirm the session with your application.
-
-
When that finishes, they'll be sent back to `/oauth/callback` on our Web app. The OAuth client stores the access tokens for the server, and then we attach their account's [DID](https://atproto.com/specs/did) to their cookie-session.
-
-
```typescript
-
/** src/routes.ts **/
-
// OAuth callback to complete session creation
-
router.get(
-
'/oauth/callback',
-
handler(async (req, res) => {
-
// Store the credentials
-
const { agent } = await oauthClient.callback(params)
-
-
// Attach the account DID to our user via a cookie
-
const session = await getIronSession(req, res)
-
session.did = agent.accountDid
-
await session.save()
-
-
// Send them back to the app
-
return res.redirect('/')
-
})
-
)
-
```
-
-
With that, we're in business! We now have a session with the user's `at://` repo server and can use that to access their data.
-
-
## Step 3. Fetching the user's profile
-
-
Why don't we learn something about our user? In [Bluesky](https://bsky.app), users publish a "profile" record which looks like this:
-
-
```typescript
-
interface ProfileRecord {
-
displayName?: string // a human friendly name
-
description?: string // a short bio
-
avatar?: BlobRef // small profile picture
-
banner?: BlobRef // banner image to put on profiles
-
createdAt?: string // declared time this profile data was added
-
// ...
-
}
-
```
-
-
You can examine this record directly using [atproto-browser.vercel.app](https://atproto-browser.vercel.app). For instance, [this is the profile record for @bsky.app](https://atproto-browser.vercel.app/at?u=at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.actor.profile/self).
-
-
We're going to use the [Agent](https://github.com/bluesky-social/atproto/tree/main/packages/api) associated with the user's OAuth session to fetch this record.
-
-
```typescript
-
await agent.getRecord({
-
repo: agent.accountDid, // The user
-
collection: 'app.bsky.actor.profile', // The collection
-
rkey: 'self', // The record key
-
})
-
```
-
-
When asking for a record, we provide three pieces of information.
-
-
- **repo** The [DID](https://atproto.com/specs/did) which identifies the user,
-
- **collection** The collection name, and
-
- **rkey** The record key
-
-
We'll explain the collection name shortly. Record keys are strings with [some restrictions](https://atproto.com/specs/record-key#record-key-syntax) and a couple of common patterns. The `"self"` pattern is used when a collection is expected to only contain one record which describes the user.
-
-
Let's update our homepage to fetch this profile record:
-
-
```typescript
-
/** src/routes.ts **/
-
// 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)
-
-
if (!agent) {
-
// Serve the logged-out view
-
return res.type('html').send(page(home()))
-
}
-
-
// Fetch additional information about the logged-in user
-
const { data: profileRecord } = await agent.getRecord({
-
repo: agent.accountDid, // our user's repo
-
collection: 'app.bsky.actor.profile', // the bluesky profile record type
-
rkey: 'self', // the record's key
-
})
-
-
// Serve the logged-in view
-
return res
-
.type('html')
-
.send(page(home({ profile: profileRecord.value || {} })))
-
})
-
)
-
```
-
-
With that data, we can give a nice personalized welcome banner for our user:
-
-
![A screenshot of the banner image](./docs/app-banner.png)
-
-
```html
-
<!-- pages/home.ts -->
-
<div class="card">
-
${profile
-
? html`<form action="/logout" method="post" class="session-form">
-
<div>
-
Hi, <strong>${profile.displayName || 'friend'}</strong>.
-
What's your status today?
-
</div>
-
<div>
-
<button type="submit">Log out</button>
-
</div>
-
</form>`
-
: html`<div class="session-form">
-
<div><a href="/login">Log in</a> to set your status!</div>
-
<div>
-
<a href="/login" class="button">Log in</a>
-
</div>
-
</div>`}
-
</div>
-
```
-
-
## Step 4. Reading & writing records
-
-
You can think of the user repositories as collections of JSON records:
-
-
![A diagram of a repository](./docs/diagram-repo.png)
-
-
Let's look again at how we read the "profile" record:
-
-
```typescript
-
await agent.getRecord({
-
repo: agent.accountDid, // The user
-
collection: 'app.bsky.actor.profile', // The collection
-
rkey: 'self', // The record key
-
})
-
```
-
-
We write records using a similar API. Since our goal is to write "status" records, let's look at how that will happen:
-
-
```typescript
-
// Generate a time-based key for our record
-
const rkey = TID.nextStr()
-
-
// Write the
-
await agent.putRecord({
-
repo: agent.accountDid, // The user
-
collection: 'com.example.status', // The collection
-
rkey, // The record key
-
record: { // The record value
-
status: "๐Ÿ‘",
-
createdAt: new Date().toISOString()
-
}
-
})
-
```
-
-
Our `POST /status` route is going to use this API to publish the user's status to their repo.
-
-
```typescript
-
/** src/routes.ts **/
-
// "Set status" handler
-
router.post(
-
'/status',
-
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: 'com.example.status',
-
status: req.body?.status,
-
createdAt: new Date().toISOString(),
-
}
-
-
try {
-
// Write the status record to the user's repository
-
await agent.putRecord({
-
repo: agent.accountDid,
-
collection: 'com.example.status',
-
rkey: TID.nextStr(),
-
record,
-
})
-
} catch (err) {
-
logger.warn({ err }, 'failed to write record')
-
return res.status(500).type('html').send('<h1>Error: Failed to write record</h1>')
-
}
-
-
res.status(200).json({})
-
})
-
)
-
```
-
-
Now in our homepage we can list out the status buttons:
-
-
```html
-
<!-- src/pages/home.ts -->
-
<form action="/status" method="post" class="status-options">
-
${STATUS_OPTIONS.map(status => html`
-
<button class="status-option" name="status" value="${status}">
-
${status}
-
</button>
-
`)}
-
</form>
-
```
-
-
And here we are!
-
-
![A screenshot of the app's status options](./docs/app-status-options.png)
-
-
## Step 5. Creating a custom "status" schema
-
-
Repo collections are typed, meaning that they have a defined schema. The `app.bsky.actor.profile` type definition [can be found here](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/profile.json).
-
-
Anybody can create a new schema using the [Lexicon](https://atproto.com/specs/lexicon) language, which is very similar to [JSON-Schema](http://json-schema.org/). The schemas use [reverse-DNS IDs](https://atproto.com/specs/nsid) which indicate ownership, but for this demo app we're going to use `com.example` which is safe for non-production software.
-
-
> ### Why create a schema?
-
>
-
> Schemas help other applications understand the data your app is creating. By publishing your schemas, you make it easier for other application authors to publish data in a format your app will recognize and handle.
-
-
Let's create our schema in the `/lexicons` folder of our codebase. You can [read more about how to define schemas here](https://atproto.com/guides/lexicon).
-
-
```json
-
/** lexicons/status.json **/
-
{
-
"lexicon": 1,
-
"id": "com.example.status",
-
"defs": {
-
"main": {
-
"type": "record",
-
"key": "tid",
-
"record": {
-
"type": "object",
-
"required": ["status", "createdAt"],
-
"properties": {
-
"status": {
-
"type": "string",
-
"minLength": 1,
-
"maxGraphemes": 1,
-
"maxLength": 32
-
},
-
"createdAt": {
-
"type": "string",
-
"format": "datetime"
-
}
-
}
-
}
-
}
-
}
-
}
-
```
-
-
Now let's run some code-generation using our schema:
-
-
```bash
-
./node_modules/.bin/lex gen-server ./src/lexicon ./lexicons/*
-
```
-
-
This will produce Typescript interfaces as well as runtime validation functions that we can use in our app. Here's what that generated code looks like:
-
-
```typescript
-
/** src/lexicon/types/com/example/status.ts **/
-
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 === 'com.example.status#main' || v.$type === 'com.example.status')
-
)
-
}
-
-
export function validateRecord(v: unknown): ValidationResult {
-
return lexicons.validate('com.example.status#main', v)
-
}
-
```
-
-
Let's use that code to improve the `POST /status` route:
-
-
```typescript
-
/** src/routes.ts **/
-
import * as Status from '#/lexicon/types/com/example/status'
-
// ...
-
// "Set status" handler
-
router.post(
-
'/status',
-
handler(async (req, res) => {
-
// ...
-
-
// Construct & validate their status record
-
const record = {
-
$type: 'com.example.status',
-
status: req.body?.status,
-
createdAt: new Date().toISOString(),
-
}
-
if (!Status.validateRecord(record).success) {
-
return res.status(400).json({ error: 'Invalid status' })
-
}
-
-
// ...
-
})
-
)
-
```
-
-
## Step 6. Listening to the firehose
-
-
So far, we have:
-
-
- Logged in via OAuth
-
- Created a custom schema
-
- Read & written records for the logged in user
-
-
Now we want to fetch the status records from other users.
-
-
Remember how we referred to our app as being like a Google, crawling around the repos to get their records? One advantage we have in the AT Protocol is that each repo publishes an event log of their updates.
-
-
![A diagram of the event stream](./docs/diagram-event-stream.png)
-
-
Using a [Relay service](https://docs.bsky.app/docs/advanced-guides/federation-architecture#relay) we can listen to an aggregated firehose of these events across all users in the network. In our case what we're looking for are valid `com.example.status` records.
-
-
-
```typescript
-
/** src/firehose.ts **/
-
import * as Status from '#/lexicon/types/com/example/status'
-
// ...
-
const firehose = new Firehose({})
-
-
for await (const evt of firehose.run()) {
-
// Watch for write events
-
if (evt.event === 'create' || evt.event === 'update') {
-
const record = evt.record
-
-
// If the write is a valid status update
-
if (
-
evt.collection === 'com.example.status' &&
-
Status.isRecord(record) &&
-
Status.validateRecord(record).success
-
) {
-
// Store the status
-
// TODO
-
}
-
}
-
}
-
```
-
-
Let's create a SQLite table to store these statuses:
-
-
```typescript
-
/** src/db.ts **/
-
// Create our statuses table
-
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()
-
```
-
-
Now we can write these statuses into our database as they arrive from the firehose:
-
-
```typescript
-
/** src/firehose.ts **/
-
// If the write is a valid status update
-
if (
-
evt.collection === 'com.example.status' &&
-
Status.isRecord(record) &&
-
Status.validateRecord(record).success
-
) {
-
// Store the status in our SQLite
-
await db
-
.insertInto('status')
-
.values({
-
uri: evt.uri.toString(),
-
authorDid: evt.author,
-
status: record.status,
-
createdAt: record.createdAt,
-
indexedAt: new Date().toISOString(),
-
})
-
.onConflict((oc) =>
-
oc.column('uri').doUpdateSet({
-
status: record.status,
-
indexedAt: new Date().toISOString(),
-
})
-
)
-
.execute()
-
}
-
```
-
-
You can almost think of information flowing in a loop:
-
-
![A diagram of the flow of information](./docs/diagram-info-flow.png)
-
-
Applications write to the repo. The write events are then emitted on the firehose where they're caught by the apps and ingested into their databases.
-
-
Why sync from the event log like this? Because there are other apps in the network that will write the records we're interested in. By subscribing to the event log, we ensure that we catch all the data we're interested in &mdash; including data published by other apps!
-
-
## Step 7. Listing the latest statuses
-
-
Now that we have statuses populating our SQLite, we can produce a timeline of status updates by users. We also use a [DID](https://atproto.com/specs/did)-to-handle resolver so we can show a nice username with the statuses:
-
-
```typescript
-
/** src/routes.ts **/
-
// Homepage
-
router.get(
-
'/',
-
handler(async (req, res) => {
-
// ...
-
-
// Fetch data stored in our SQLite
-
const statuses = await db
-
.selectFrom('status')
-
.selectAll()
-
.orderBy('indexedAt', 'desc')
-
.limit(10)
-
.execute()
-
-
// Map user DIDs to their domain-name handles
-
const didHandleMap = await resolver.resolveDidsToHandles(
-
statuses.map((s) => s.authorDid)
-
)
-
-
// ...
-
})
-
)
-
```
-
-
Our HTML can now list these status records:
-
-
```html
-
<!-- src/pages/home.ts -->
-
${statuses.map((status, i) => {
-
const handle = didHandleMap[status.authorDid] || status.authorDid
-
return html`
-
<div class="status-line">
-
<div>
-
<div class="status">${status.status}</div>
-
</div>
-
<div class="desc">
-
<a class="author" href="https://bsky.app/profile/${handle}">@${handle}</a>
-
was feeling ${status.status} on ${status.indexedAt}.
-
</div>
-
</div>
-
`
-
})}
-
```
-
-
![A screenshot of the app status timeline](./docs/app-status-history.png)
-
-
## Step 8. Optimistic updates
-
-
As a final optimization, let's introduce "optimistic updates."
-
-
Remember the information flow loop with the repo write and the event log?
-
-
![A diagram of the flow of information](./docs/diagram-info-flow.png)
-
-
Since we're updating our users' repos locally, we can short-circuit that flow to our own database:
-
-
![A diagram illustrating optimistic updates](./docs/diagram-optimistic-update.png)
-
-
This is an important optimization to make, because it ensures that the user sees their own changes while using your app. When the event eventually arrives from the firehose, we just discard it since we already have it saved locally.
-
-
To do this, we just update `POST /status` to include an additional write to our SQLite DB:
-
-
```typescript
-
/** src/routes.ts **/
-
// "Set status" handler
-
router.post(
-
'/status',
-
handler(async (req, res) => {
-
// ...
-
-
let uri
-
try {
-
// Write the status record to the user's repository
-
const res = await agent.putRecord({
-
repo: agent.accountDid,
-
collection: 'com.example.status',
-
rkey: TID.nextStr(),
-
record,
-
})
-
uri = res.uri
-
} catch (err) {
-
logger.warn({ err }, 'failed to write record')
-
return res.status(500).json({ error: 'Failed to write record' })
-
}
-
-
try {
-
// Optimistically update our SQLite <-- HERE!
-
await db
-
.insertInto('status')
-
.values({
-
uri,
-
authorDid: agent.accountDid,
-
status: record.status,
-
createdAt: record.createdAt,
-
indexedAt: new Date().toISOString(),
-
})
-
.execute()
-
} catch (err) {
-
logger.warn(
-
{ err },
-
'failed to update computed view; ignoring as it should be caught by the firehose'
-
)
-
}
-
-
res.status(200).json({})
-
})
-
)
-
```
-
-
You'll notice this code looks almost exactly like what we're doing in `firehose.ts`.
-
-
## Thinking in AT Proto
-
-
In this tutorial we've covered the key steps to building an atproto app. Data is published in its canonical form on users' `at://` repos and then aggregated into apps' databases to produce views of the network.
-
-
When building your app, think in these four key steps:
-
-
- Design the [Lexicon](#) schemas for the records you'll publish into the Atmosphere.
-
- Create a database for aggregating the records into useful views.
-
- Build your application to write the records on your users' repos.
-
- Listen to the firehose to aggregate data across the network.
-
-
Remember this flow of information throughout:
-
-
![A diagram of the flow of information](./docs/diagram-info-flow.png)
-
-
This is how every app in the Atmosphere works, including the [Bluesky social app](https://bsky.app).
-
-
## Next steps
-
-
If you want to practice what you've learned, here are some additional challenges you could try:
-
-
- Sync the profile records of all users so that you can show their display names instead of their handles.
-
- Count the number of each status used and display the total counts.
-
- Fetch the authed user's `app.bsky.graph.follow` follows and show statuses from them.
-
- Create a different kind of schema, like a way to post links to websites and rate them 1 through 4 stars.
-
-
You can find more information here:
-
-
|Resources|-|
-
|-|-|
-
|[๏ผ  ATProto docs](https://atproto.com)|Learn more about the AT Protocol.|
-
|[๐Ÿฆ‹ Bluesky API docs](https://docs.bsky.app/)|See how Bluesky works as an ATProto app.|
-
|[๐Ÿ“ฆ ATProto monorepo](https://github.com/bluesky-social/atproto)|See the source code first-hand.|
-
|[๐Ÿ’ฌ ATProto discussions board](https://github.com/bluesky-social/atproto/discussions)|Ask any questions you have!|
+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()
docs/app-banner.png

This is a binary file and will not be displayed.

docs/app-login.png

This is a binary file and will not be displayed.

docs/app-screenshot.png

This is a binary file and will not be displayed.

docs/app-status-history.png

This is a binary file and will not be displayed.

docs/app-status-options.png

This is a binary file and will not be displayed.

docs/diagram-event-stream.png

This is a binary file and will not be displayed.

docs/diagram-info-flow.png

This is a binary file and will not be displayed.

docs/diagram-oauth.png

This is a binary file and will not be displayed.

docs/diagram-optimistic-update.png

This is a binary file and will not be displayed.

docs/diagram-repo.png

This is a binary file and will not be displayed.

+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"
+
]
+
}
+
}
+
}
+1 -1
lexicons/status.json
···
{
"lexicon": 1,
-
"id": "com.example.status",
+
"id": "xyz.statusphere.status",
"defs": {
"main": {
"type": "record",
+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" }
+
}
+
}
+
}
+
}
+1278 -625
package-lock.json
···
"version": "0.0.1",
"license": "MIT",
"dependencies": {
-
"@atproto/common": "^0.4.1",
-
"@atproto/identity": "^0.4.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",
-
"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",
···
"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": {
···
"engines": {
"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/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",
···
"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": ">=8.6.0"
},
+
"node_modules/fast-printf": {
+
"version": "1.6.10",
+
"resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz",
+
"integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==",
+
"license": "BSD-3-Clause",
+
"engines": {
+
"node": ">=10.0"
+
}
+
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
···
"node": ">= 0.8"
},
-
"node_modules/follow-redirects": {
-
"version": "1.15.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=="
+
"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.8"
},
+
"node_modules/http-terminator": {
+
"version": "3.2.0",
+
"resolved": "https://registry.npmjs.org/http-terminator/-/http-terminator-3.2.0.tgz",
+
"integrity": "sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==",
+
"license": "BSD-3-Clause",
+
"dependencies": {
+
"delay": "^5.0.0",
+
"p-wait-for": "^3.2.0",
+
"roarr": "^7.0.4",
+
"type-fest": "^2.3.3"
+
},
+
"engines": {
+
"node": ">=14"
+
}
+
},
+
"node_modules/human-signals": {
+
"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",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
···
"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",
···
"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",
···
"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",
···
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"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",
···
"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",
···
"node": "*"
},
+
"node_modules/type-fest": {
+
"version": "2.19.0",
+
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+
"license": "(MIT OR CC0-1.0)",
+
"engines": {
+
"node": ">=12.20"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
···
"integrity": "sha512-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 -8
package.json
···
"clean": "rimraf dist coverage"
},
"dependencies": {
-
"@atproto/common": "^0.4.1",
-
"@atproto/identity": "^0.4.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",
-
"uhtml": "^4.5.9"
+
"uhtml": "^4.5.9",
+
"zod": "^3.25.67"
},
"devDependencies": {
"@atproto/lex-cli": "^0.4.1",
+58 -19
src/auth/client.ts
···
-
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 '#/lib/env'
+
import { env } from '#/env'
import { SessionStore, StateStore } from './storage'
-
export const createClient = async (db: Database) => {
-
const publicUrl = env.PUBLIC_URL
-
const url = publicUrl || `http://127.0.0.1:${env.PORT}`
+
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,
})
}
+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()
+
},
+
}
+
}
+25
src/env.ts
···
+
import dotenv from 'dotenv'
+
import { cleanEnv, port, str, testOnly, url } from 'envalid'
+
import { envalidJsonWebKeys as keys } from '#/lib/jwk'
+
+
dotenv.config()
+
+
export const env = cleanEnv(process.env, {
+
NODE_ENV: str({
+
devDefault: testOnly('test'),
+
choices: ['development', 'production', 'test'],
+
}),
+
PORT: port({ devDefault: testOnly(3000) }),
+
PUBLIC_URL: url({ default: undefined }),
+
DB_PATH: str({ devDefault: ':memory:' }),
+
COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }),
+
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 }),
+
})
-194
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'
-
record: RepoRecord
-
cid: CID
-
}
-
-
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'
-54
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()) {
-
// Watch for write events
-
if (evt.event === 'create' || evt.event === 'update') {
-
const record = evt.record
-
-
// If the write is a valid status update
-
if (
-
evt.collection === 'com.example.status' &&
-
Status.isRecord(record) &&
-
Status.validateRecord(record).success
-
) {
-
// Store the status in our SQLite
-
await this.db
-
.insertInto('status')
-
.values({
-
uri: evt.uri.toString(),
-
authorDid: evt.author,
-
status: record.status,
-
createdAt: record.createdAt,
-
indexedAt: new Date().toISOString(),
-
})
-
.onConflict((oc) =>
-
oc.column('uri').doUpdateSet({
-
status: record.status,
-
indexedAt: new Date().toISOString(),
-
})
-
)
-
.execute()
-
}
-
} else if (
-
evt.event === 'delete' &&
-
evt.collection === 'com.example.status'
-
) {
-
// Remove the status from our SQLite
-
await this.db.deleteFrom('status').where({ uri: evt.uri.toString() })
-
}
-
}
-
}
-
-
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)
-
}
-39
src/firehose/resolver.ts
···
-
import { IdResolver, MemoryCache } from '@atproto/identity'
-
-
const HOUR = 60e3 * 60
-
const DAY = HOUR * 24
-
-
export interface Resolver {
-
resolveDidToHandle(did: string): Promise<string>
-
resolveDidsToHandles(dids: string[]): Promise<Record<string, string>>
-
}
-
-
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
-
},
-
}
-
}
+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]),
+
),
+
),
+
)
+
},
+
}
+
}
+24 -88
src/index.ts
···
-
import events from 'node:events'
-
import type http from 'node:http'
-
import express, { type Express } from 'express'
-
import { pino } from 'pino'
-
import type { OAuthClient } from '@atproto/oauth-client-node'
+
import { once } from 'node:events'
-
import { createDb, migrateToLatest } from '#/db'
-
import { env } from '#/lib/env'
-
import { Ingester } from '#/firehose/ingester'
+
import { createAppContext } from '#/context'
+
import { env } from '#/env'
+
import { startServer } from '#/lib/http'
+
import { run } from '#/lib/process'
import { createRouter } from '#/routes'
-
import { createClient } from '#/auth/client'
-
import { createResolver, Resolver } from '#/firehose/resolver'
-
import type { Database } from '#/db'
-
// Application state passed to the router and elsewhere
-
export type AppContext = {
-
db: Database
-
ingester: Ingester
-
logger: pino.Logger
-
oauthClient: OAuthClient
-
resolver: Resolver
-
}
-
-
export class Server {
-
constructor(
-
public app: express.Application,
-
public server: http.Server,
-
public ctx: AppContext
-
) {}
-
-
static async create() {
-
const { NODE_ENV, HOST, PORT, DB_PATH } = env
-
const logger = pino({ name: 'server start' })
-
-
// Set up the SQLite database
-
const db = createDb(DB_PATH)
-
await migrateToLatest(db)
-
-
// Create the atproto utilities
-
const oauthClient = await createClient(db)
-
const ingester = new Ingester(db)
-
const resolver = createResolver()
-
const ctx = {
-
db,
-
ingester,
-
logger,
-
oauthClient,
-
resolver,
-
}
-
-
// Subscribe to events on the firehose
-
ingester.start()
-
-
// Create our server
-
const app: Express = express()
-
app.set('trust proxy', true)
-
-
// Routes & middlewares
-
const router = createRouter(ctx)
-
app.use(express.json())
-
app.use(express.urlencoded({ extended: true }))
-
app.use(router)
-
app.use((_req, res) => res.sendStatus(404))
+
run(async (killSignal) => {
+
// Create the application context
+
const ctx = await createAppContext()
-
// Bind our server to the port
-
const server = app.listen(env.PORT)
-
await events.once(server, 'listening')
-
logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`)
+
// Create the HTTP router
+
const router = createRouter(ctx)
-
return new Server(app, server, ctx)
-
}
+
// Start the HTTP server
+
const { terminate } = await startServer(router, { port: env.PORT })
-
async close() {
-
this.ctx.logger.info('sigint received, shutting down')
-
this.ctx.ingester.destroy()
-
return new Promise<void>((resolve) => {
-
this.server.close(() => {
-
this.ctx.logger.info('server closed')
-
resolve()
-
})
-
})
-
}
-
}
+
const url = env.PUBLIC_URL || `http://localhost:${env.PORT}`
+
ctx.logger.info(`Server (${env.NODE_ENV}) running at ${url}`)
-
const run = async () => {
-
const server = await Server.create()
+
// Subscribe to events on the firehose
+
ctx.ingester.start()
-
const onCloseSignal = async () => {
-
setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s
-
await server.close()
-
process.exit()
-
}
+
// Wait for a termination signal
+
if (!killSignal.aborted) await once(killSignal, 'abort')
+
ctx.logger.info(`Signal received, shutting down...`)
-
process.on('SIGINT', onCloseSignal)
-
process.on('SIGTERM', onCloseSignal)
-
}
+
// Gracefully shutdown the http server
+
await terminate()
-
run()
+
// 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),
+
}),
+
})
+
}
+33 -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 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) {
+206 -4
src/lexicon/lexicons.ts
···
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
export const schemaDict = {
+
ComAtprotoLabelDefs: {
+
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: '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',
···
},
},
},
-
ComExampleStatus: {
+
XyzStatusphereStatus: {
lexicon: 1,
-
id: 'com.example.status',
+
id: 'xyz.statusphere.status',
defs: {
main: {
type: 'record',
-
key: 'literal:self',
+
key: 'tid',
record: {
type: 'object',
required: ['status', 'createdAt'],
···
},
},
},
+
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 = {
+
ComAtprotoLabelDefs: 'com.atproto.label.defs',
AppBskyActorProfile: 'app.bsky.actor.profile',
-
ComExampleStatus: 'com.example.status',
+
XyzStatusphereStatus: 'xyz.statusphere.status',
+
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
}
+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
-
createdAt: 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)
+
}
-18
src/lib/env.ts
···
-
import dotenv from 'dotenv'
-
import { cleanEnv, host, num, port, str, testOnly } from 'envalid'
-
-
dotenv.config()
-
-
export const env = cleanEnv(process.env, {
-
NODE_ENV: str({
-
devDefault: testOnly('test'),
-
choices: ['development', 'production', 'test'],
-
}),
-
HOST: host({ devDefault: testOnly('localhost') }),
-
PORT: port({ devDefault: testOnly(3000) }),
-
PUBLIC_URL: str({}),
-
DB_PATH: str({ devDefault: ':memory:' }),
-
COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }),
-
COMMON_RATE_LIMIT_MAX_REQUESTS: num({ devDefault: testOnly(1000) }),
-
COMMON_RATE_LIMIT_WINDOW_MS: num({ devDefault: testOnly(1000) }),
-
})
+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
+
}
+5 -6
src/pages/home.ts
···
const TODAY = new Date().toDateString()
-
const STATUS_OPTIONS = [
+
export const STATUS_OPTIONS = [
'๐Ÿ‘',
'๐Ÿ‘Ž',
'๐Ÿ’™',
'๐Ÿฅน',
'๐Ÿ˜ง',
-
'๐Ÿ˜ค',
'๐Ÿ™ƒ',
'๐Ÿ˜‰',
'๐Ÿ˜Ž',
···
type Props = {
statuses: Status[]
-
didHandleMap: Record<string, string>
+
didHandleMap: Record<string, string | undefined>
profile?: { displayName?: string }
myStatus?: Status
}
···
value="${status}"
>
${status}
-
</button>`
+
</button>`,
)}
</form>
${statuses.map((status, i) => {
···
}
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()
}
+14 -6
src/pages/login.ts
···
+
import { env } from '#/env'
import { html } from '../lib/view'
import { shell } from './shell'
···
}
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>`
}
+28 -9
src/pages/public/styles.css
···
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
-
*, *::before, *::after {
+
*,
+
*::before,
+
*::after {
box-sizing: border-box;
}
* {
···
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
-
img, picture, video, canvas, svg {
+
img,
+
picture,
+
video,
+
canvas,
+
svg {
display: block;
max-width: 100%;
}
-
input, button, textarea, select {
+
input,
+
button,
+
textarea,
+
select {
font: inherit;
}
-
p, h1, h2, h3, h4, h5, h6 {
+
p,
+
h1,
+
h2,
+
h3,
+
h4,
+
h5,
+
h6 {
overflow-wrap: break-word;
}
-
#root, #__next {
+
#root,
+
#__next {
isolation: isolate;
}
/*
Common components
*/
-
button, .button {
+
button,
+
.button {
display: inline-block;
border: 0;
background-color: var(--primary-500);
···
cursor: pointer;
text-decoration: none;
}
-
button:hover, .button:hover {
+
button:hover,
+
.button:hover {
background: var(--primary-400);
}
···
.signup-cta {
text-align: center;
-
text-wrap: balance;
+
width: 100%;
+
display: block;
margin-top: 1rem;
-
}
+
}
+164 -84
src/routes.ts
···
-
import assert from 'node:assert'
-
import path from 'node:path'
-
import type { IncomingMessage, ServerResponse } from 'node:http'
-
import { OAuthResolverError } from '@atproto/oauth-client-node'
-
import { isValidHandle } from '@atproto/syntax'
+
import { Agent } from '@atproto/api'
import { TID } from '@atproto/common'
-
import express from 'express'
+
import { OAuthResolverError } from '@atproto/oauth-client-node'
+
import express, { Request, Response } from 'express'
import { getIronSession } from 'iron-session'
-
import type { AppContext } from '#/index'
+
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'
-
import { env } from '#/lib/env'
-
import { page } from '#/lib/view'
-
import * as Status from '#/lexicon/types/com/example/status'
-
import * as Profile from '#/lexicon/types/app/bsky/actor/profile'
-
type Session = { did: string }
+
// Max age, in seconds, for static routes and assets
+
const MAX_AGE = env.NODE_ENV === 'production' ? 60 : 0
-
// Helper function for defining routes
-
const handler =
-
(fn: express.Handler) =>
-
async (
-
req: express.Request,
-
res: express.Response,
-
next: express.NextFunction
-
) => {
-
try {
-
await fn(req, res, next)
-
} catch (err) {
-
next(err)
-
}
-
}
+
type Session = { did?: string }
// Helper function to get the Atproto Agent for the active session
async function getSessionAgent(
req: IncomingMessage,
-
res: ServerResponse<IncomingMessage>,
-
ctx: AppContext
+
res: ServerResponse,
+
ctx: AppContext,
) {
+
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 {
-
return await ctx.oauthClient.restore(session.did)
+
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()
···
}
}
-
export const createRouter = (ctx: AppContext) => {
-
const router = express.Router()
+
export const createRouter = (ctx: AppContext): RequestListener => {
+
const router = express()
// Static assets
-
router.use('/public', express.static(path.join(__dirname, 'pages', 'public')))
+
router.use(
+
'/public',
+
express.static(path.join(__dirname, 'pages', 'public'), {
+
maxAge: MAX_AGE * 1000,
+
}),
+
)
// OAuth metadata
router.get(
-
'/client-metadata.json',
-
handler((_req, res) => {
-
return res.json(ctx.oauthClient.clientMetadata)
-
})
+
'/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 {
-
const { agent } = await ctx.oauthClient.callback(params)
+
// Load the session cookie
const session = await getIronSession<Session>(req, res, {
cookieName: 'sid',
password: env.COOKIE_SECRET,
})
-
assert(!session.did, 'session already exists')
-
session.did = agent.accountDid
+
+
// 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('/?error')
}
+
return res.redirect('/')
-
})
+
}),
)
// Login page
router.get(
'/login',
-
handler(async (_req, res) => {
-
return res.type('html').send(page(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) => {
-
// Validate
-
const handle = req.body?.handle
-
if (typeof handle !== 'string' || !isValidHandle(handle)) {
-
return res.type('html').send(page(login({ error: 'invalid handle' })))
-
}
+
// Never store this route
+
res.setHeader('cache-control', 'no-store')
// Initiate the OAuth flow
try {
-
const url = await ctx.oauthClient.authorize(handle)
-
return res.redirect(url.toString())
+
// 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')
-
return res.type('html').send(
+
+
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,
})
-
await session.destroy()
+
+
// 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
···
? await ctx.db
.selectFrom('status')
.selectAll()
-
.where('authorDid', '=', agent.accountDid)
+
.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)
+
statuses.map((s) => s.authorDid),
)
if (!agent) {
···
}
// Fetch additional information about the logged-in user
-
const { data: profileRecord } = await agent.com.atproto.repo.getRecord({
-
repo: agent.accountDid,
-
collection: 'app.bsky.actor.profile',
-
rkey: 'self',
-
})
+
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
-
return res.type('html').send(
-
page(
-
home({
-
statuses,
-
didHandleMap,
-
profile,
-
myStatus,
-
})
-
)
-
)
-
})
+
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)
···
.send('<h1>Error: Session required</h1>')
}
-
// Construct & validate their status record
-
const rkey = TID.nextStr()
+
// Construct their status record
const record = {
-
$type: 'com.example.status',
+
$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)
···
try {
// Write the status record to the user's repository
const res = await agent.com.atproto.repo.putRecord({
-
repo: agent.accountDid,
-
collection: 'com.example.status',
-
rkey,
+
repo: agent.assertDid,
+
collection: 'xyz.statusphere.status',
+
rkey: TID.nextStr(),
record,
validate: false,
})
···
.insertInto('status')
.values({
uri,
-
authorDid: agent.accountDid,
+
authorDid: agent.assertDid,
status: record.status,
createdAt: record.createdAt,
indexedAt: new Date().toISOString(),
···
} catch (err) {
ctx.logger.warn(
{ err },
-
'failed to update computed view; ignoring as it should be caught by the firehose'
+
'failed to update computed view; ignoring as it should be caught by the firehose',
)
}
return res.redirect('/')
-
})
+
}),
)
return router