···11-# Environment Configuration
22-NODE_ENV="development" # Options: 'development', 'production'
33-PORT="8080" # The port your server will listen on
44-HOST="localhost" # Hostname for the server
55-PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id.
66-DB_PATH=":memory:" # The SQLite database path. Leave as ":memory:" to use a temporary in-memory database.
77-88-# Secrets
99-# Must set this in production. May be generated with `openssl rand -base64 33`
1010-# COOKIE_SECRET=""
···11+hey buddy :)
22+33+if you're going to undertake multi-file or otherwise complex edits, please write a summary of what you're looking to achieve, so that I can either approve or provide suggestions
44+55+and most importantly, have fun!
66+77+your friend,
88+mozzius
+53-11
README.md
···11-# AT Protocol "Statusphere" Example App
11+# Statusphere React
2233-An example application covering:
33+A monorepo for the Statusphere application, which includes a React client and a Node.js backend.
44+55+This is a React refactoring of the [example application](https://atproto.com/guides/applications) covering:
4657- Signin via OAuth
68- Fetch information about users (profiles)
79- Listen to the network firehose for new data
810- Publish data on the user's account using a custom schema
9111010-See https://atproto.com/guides/applications for a guide through the codebase.
1212+## Structure
11131212-## Getting Started
1414+- `packages/appview` - Express.js backend that serves API endpoints
1515+- `packages/client` - React frontend using Vite
13161414-```sh
1515-git clone https://github.com/bluesky-social/statusphere-example-app.git
1616-cd statusphere-example-app
1717-cp .env.template .env
1818-npm install
1919-npm run dev
2020-# Navigate to http://localhost:8080
1717+## Development
1818+1919+```bash
2020+# Install dependencies
2121+pnpm install
2222+2323+# Option 1: Local development (login won't work due to OAuth requirements)
2424+pnpm dev
2525+2626+# Option 2: Development with OAuth login support (recommended)
2727+pnpm dev:oauth
2128```
2929+3030+### OAuth Development
3131+3232+Due to OAuth requirements, HTTPS is needed for development. We've made this easy:
3333+3434+- `pnpm dev:oauth` - Sets up everything automatically:
3535+ 1. Starts ngrok to create an HTTPS tunnel
3636+ 2. Configures environment variables with the ngrok URL
3737+ 3. Starts both the API server and client app
3838+ 4. Handles proper shutdown of all processes
3939+4040+This all-in-one command makes OAuth development seamless.
4141+4242+### Additional Commands
4343+4444+```bash
4545+# Build both packages
4646+pnpm build
4747+4848+# Run typecheck on both packages
4949+pnpm typecheck
5050+5151+# Format all code
5252+pnpm format
5353+```
5454+5555+## Requirements
5656+5757+- Node.js 18+
5858+- pnpm 9+
5959+- ngrok (for OAuth development)
6060+6161+## License
6262+6363+MIT
···11+{
22+ "lexicon": 1,
33+ "id": "com.atproto.label.defs",
44+ "defs": {
55+ "label": {
66+ "type": "object",
77+ "description": "Metadata tag on an atproto resource (eg, repo or record).",
88+ "required": ["src", "uri", "val", "cts"],
99+ "properties": {
1010+ "ver": {
1111+ "type": "integer",
1212+ "description": "The AT Protocol version of the label object."
1313+ },
1414+ "src": {
1515+ "type": "string",
1616+ "format": "did",
1717+ "description": "DID of the actor who created this label."
1818+ },
1919+ "uri": {
2020+ "type": "string",
2121+ "format": "uri",
2222+ "description": "AT URI of the record, repository (account), or other resource that this label applies to."
2323+ },
2424+ "cid": {
2525+ "type": "string",
2626+ "format": "cid",
2727+ "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
2828+ },
2929+ "val": {
3030+ "type": "string",
3131+ "maxLength": 128,
3232+ "description": "The short string name of the value or type of this label."
3333+ },
3434+ "neg": {
3535+ "type": "boolean",
3636+ "description": "If true, this is a negation label, overwriting a previous label."
3737+ },
3838+ "cts": {
3939+ "type": "string",
4040+ "format": "datetime",
4141+ "description": "Timestamp when this label was created."
4242+ },
4343+ "exp": {
4444+ "type": "string",
4545+ "format": "datetime",
4646+ "description": "Timestamp at which this label expires (no longer applies)."
4747+ },
4848+ "sig": {
4949+ "type": "bytes",
5050+ "description": "Signature of dag-cbor encoded label."
5151+ }
5252+ }
5353+ },
5454+ "selfLabels": {
5555+ "type": "object",
5656+ "description": "Metadata tags on an atproto record, published by the author within the record.",
5757+ "required": ["values"],
5858+ "properties": {
5959+ "values": {
6060+ "type": "array",
6161+ "items": { "type": "ref", "ref": "#selfLabel" },
6262+ "maxLength": 10
6363+ }
6464+ }
6565+ },
6666+ "selfLabel": {
6767+ "type": "object",
6868+ "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
6969+ "required": ["val"],
7070+ "properties": {
7171+ "val": {
7272+ "type": "string",
7373+ "maxLength": 128,
7474+ "description": "The short string name of the value or type of this label."
7575+ }
7676+ }
7777+ },
7878+ "labelValueDefinition": {
7979+ "type": "object",
8080+ "description": "Declares a label value and its expected interpretations and behaviors.",
8181+ "required": ["identifier", "severity", "blurs", "locales"],
8282+ "properties": {
8383+ "identifier": {
8484+ "type": "string",
8585+ "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
8686+ "maxLength": 100,
8787+ "maxGraphemes": 100
8888+ },
8989+ "severity": {
9090+ "type": "string",
9191+ "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
9292+ "knownValues": ["inform", "alert", "none"]
9393+ },
9494+ "blurs": {
9595+ "type": "string",
9696+ "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.",
9797+ "knownValues": ["content", "media", "none"]
9898+ },
9999+ "defaultSetting": {
100100+ "type": "string",
101101+ "description": "The default setting for this label.",
102102+ "knownValues": ["ignore", "warn", "hide"],
103103+ "default": "warn"
104104+ },
105105+ "adultOnly": {
106106+ "type": "boolean",
107107+ "description": "Does the user need to have adult content enabled in order to configure this label?"
108108+ },
109109+ "locales": {
110110+ "type": "array",
111111+ "items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
112112+ }
113113+ }
114114+ },
115115+ "labelValueDefinitionStrings": {
116116+ "type": "object",
117117+ "description": "Strings which describe the label in the UI, localized into a specific language.",
118118+ "required": ["lang", "name", "description"],
119119+ "properties": {
120120+ "lang": {
121121+ "type": "string",
122122+ "description": "The code of the language these strings are written in.",
123123+ "format": "language"
124124+ },
125125+ "name": {
126126+ "type": "string",
127127+ "description": "A short human-readable name for the label.",
128128+ "maxGraphemes": 64,
129129+ "maxLength": 640
130130+ },
131131+ "description": {
132132+ "type": "string",
133133+ "description": "A longer description of what the label means and why it might be applied.",
134134+ "maxGraphemes": 10000,
135135+ "maxLength": 100000
136136+ }
137137+ }
138138+ },
139139+ "labelValue": {
140140+ "type": "string",
141141+ "knownValues": [
142142+ "!hide",
143143+ "!no-promote",
144144+ "!warn",
145145+ "!no-unauthenticated",
146146+ "dmca-violation",
147147+ "doxxing",
148148+ "porn",
149149+ "sexual",
150150+ "nudity",
151151+ "nsfl",
152152+ "gore"
153153+ ]
154154+ }
155155+ }
156156+}
···11+{
22+ "lexicon": 1,
33+ "id": "com.atproto.repo.uploadBlob",
44+ "defs": {
55+ "main": {
66+ "type": "procedure",
77+ "description": "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.",
88+ "input": {
99+ "encoding": "*/*"
1010+ },
1111+ "output": {
1212+ "encoding": "application/json",
1313+ "schema": {
1414+ "type": "object",
1515+ "required": ["blob"],
1616+ "properties": {
1717+ "blob": { "type": "blob" }
1818+ }
1919+ }
2020+ }
2121+ }
2222+ }
2323+}
-156
lexicons/defs.json
···11-{
22- "lexicon": 1,
33- "id": "com.atproto.label.defs",
44- "defs": {
55- "label": {
66- "type": "object",
77- "description": "Metadata tag on an atproto resource (eg, repo or record).",
88- "required": ["src", "uri", "val", "cts"],
99- "properties": {
1010- "ver": {
1111- "type": "integer",
1212- "description": "The AT Protocol version of the label object."
1313- },
1414- "src": {
1515- "type": "string",
1616- "format": "did",
1717- "description": "DID of the actor who created this label."
1818- },
1919- "uri": {
2020- "type": "string",
2121- "format": "uri",
2222- "description": "AT URI of the record, repository (account), or other resource that this label applies to."
2323- },
2424- "cid": {
2525- "type": "string",
2626- "format": "cid",
2727- "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
2828- },
2929- "val": {
3030- "type": "string",
3131- "maxLength": 128,
3232- "description": "The short string name of the value or type of this label."
3333- },
3434- "neg": {
3535- "type": "boolean",
3636- "description": "If true, this is a negation label, overwriting a previous label."
3737- },
3838- "cts": {
3939- "type": "string",
4040- "format": "datetime",
4141- "description": "Timestamp when this label was created."
4242- },
4343- "exp": {
4444- "type": "string",
4545- "format": "datetime",
4646- "description": "Timestamp at which this label expires (no longer applies)."
4747- },
4848- "sig": {
4949- "type": "bytes",
5050- "description": "Signature of dag-cbor encoded label."
5151- }
5252- }
5353- },
5454- "selfLabels": {
5555- "type": "object",
5656- "description": "Metadata tags on an atproto record, published by the author within the record.",
5757- "required": ["values"],
5858- "properties": {
5959- "values": {
6060- "type": "array",
6161- "items": { "type": "ref", "ref": "#selfLabel" },
6262- "maxLength": 10
6363- }
6464- }
6565- },
6666- "selfLabel": {
6767- "type": "object",
6868- "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
6969- "required": ["val"],
7070- "properties": {
7171- "val": {
7272- "type": "string",
7373- "maxLength": 128,
7474- "description": "The short string name of the value or type of this label."
7575- }
7676- }
7777- },
7878- "labelValueDefinition": {
7979- "type": "object",
8080- "description": "Declares a label value and its expected interpretations and behaviors.",
8181- "required": ["identifier", "severity", "blurs", "locales"],
8282- "properties": {
8383- "identifier": {
8484- "type": "string",
8585- "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
8686- "maxLength": 100,
8787- "maxGraphemes": 100
8888- },
8989- "severity": {
9090- "type": "string",
9191- "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
9292- "knownValues": ["inform", "alert", "none"]
9393- },
9494- "blurs": {
9595- "type": "string",
9696- "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.",
9797- "knownValues": ["content", "media", "none"]
9898- },
9999- "defaultSetting": {
100100- "type": "string",
101101- "description": "The default setting for this label.",
102102- "knownValues": ["ignore", "warn", "hide"],
103103- "default": "warn"
104104- },
105105- "adultOnly": {
106106- "type": "boolean",
107107- "description": "Does the user need to have adult content enabled in order to configure this label?"
108108- },
109109- "locales": {
110110- "type": "array",
111111- "items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
112112- }
113113- }
114114- },
115115- "labelValueDefinitionStrings": {
116116- "type": "object",
117117- "description": "Strings which describe the label in the UI, localized into a specific language.",
118118- "required": ["lang", "name", "description"],
119119- "properties": {
120120- "lang": {
121121- "type": "string",
122122- "description": "The code of the language these strings are written in.",
123123- "format": "language"
124124- },
125125- "name": {
126126- "type": "string",
127127- "description": "A short human-readable name for the label.",
128128- "maxGraphemes": 64,
129129- "maxLength": 640
130130- },
131131- "description": {
132132- "type": "string",
133133- "description": "A longer description of what the label means and why it might be applied.",
134134- "maxGraphemes": 10000,
135135- "maxLength": 100000
136136- }
137137- }
138138- },
139139- "labelValue": {
140140- "type": "string",
141141- "knownValues": [
142142- "!hide",
143143- "!no-promote",
144144- "!warn",
145145- "!no-unauthenticated",
146146- "dmca-violation",
147147- "doxxing",
148148- "porn",
149149- "sexual",
150150- "nudity",
151151- "nsfl",
152152- "gore"
153153- ]
154154- }
155155- }
156156- }
···11+# Statusphere AppView
22+33+This is the backend API for the Statusphere application. It provides REST endpoints for the React frontend to consume.
44+55+## Development
66+77+```bash
88+# Install dependencies
99+pnpm install
1010+1111+# Start development server
1212+pnpm dev
1313+1414+# Build for production
1515+pnpm build
1616+1717+# Start production server
1818+pnpm start
1919+```
2020+2121+## Environment Variables
2222+2323+Create a `.env` file in the root of this package with the following variables:
2424+2525+```
2626+NODE_ENV=development
2727+HOST=localhost
2828+PORT=3001
2929+DB_PATH=./data.sqlite
3030+COOKIE_SECRET=your_secret_here_at_least_32_characters_long
3131+ATPROTO_SERVER=https://bsky.social
3232+PUBLIC_URL=http://localhost:3001
3333+NGROK_URL=your_ngrok_url_here
3434+```
3535+3636+## Using ngrok for OAuth Development
3737+3838+Due to OAuth requirements, we need to use HTTPS for development. The easiest way to do this is with ngrok:
3939+4040+1. Install ngrok: https://ngrok.com/download
4141+2. Run ngrok to create a tunnel to your local server:
4242+ ```bash
4343+ ngrok http 3001
4444+ ```
4545+3. Copy the HTTPS URL provided by ngrok (e.g., `https://abcd-123-45-678-90.ngrok.io`)
4646+4. Add it to your `.env` file:
4747+ ```
4848+ NGROK_URL=https://abcd-123-45-678-90.ngrok.io
4949+ ```
5050+5. Also update the API URL in the client package:
5151+ ```
5252+ # In packages/client/src/services/api.ts
5353+ const API_URL = 'https://abcd-123-45-678-90.ngrok.io';
5454+ ```
5555+5656+## API Endpoints
5757+5858+- `GET /client-metadata.json` - OAuth client metadata
5959+- `GET /oauth/callback` - OAuth callback endpoint
6060+- `POST /login` - Login with handle
6161+- `POST /logout` - Logout current user
6262+- `GET /user` - Get current user info
6363+- `GET /statuses` - Get recent statuses
6464+- `POST /status` - Create a new status
···11+# Statusphere Client
22+33+This is the React frontend for the Statusphere application.
44+55+## Development
66+77+```bash
88+# Install dependencies
99+pnpm install
1010+1111+# Start development server
1212+pnpm dev
1313+1414+# Build for production
1515+pnpm build
1616+1717+# Preview production build
1818+pnpm preview
1919+```
2020+2121+## Features
2222+2323+- Display statuses from all users
2424+- Create new statuses
2525+- Login with your Bluesky handle
2626+- View your profile info
2727+- Responsive design
2828+2929+## Architecture
3030+3131+- React 18 with TypeScript
3232+- React Router for navigation
3333+- Context API for state management
3434+- Vite for development and building
3535+- CSS for styling
···11+/**
22+ * GENERATED CODE - DO NOT MODIFY
33+ */
44+import { BlobRef, ValidationResult } from '@atproto/lexicon'
55+import { HeadersMap, XRPCError } from '@atproto/xrpc'
66+import { CID } from 'multiformats/cid'
77+88+import { validate as _validate } from '../../../../lexicons'
99+import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util'
1010+import type * as ComAtprotoRepoDefs from './defs.js'
1111+1212+const is$typed = _is$typed,
1313+ validate = _validate
1414+const id = 'com.atproto.repo.putRecord'
1515+1616+export interface QueryParams {}
1717+1818+export interface InputSchema {
1919+ /** The handle or DID of the repo (aka, current account). */
2020+ repo: string
2121+ /** The NSID of the record collection. */
2222+ collection: string
2323+ /** The Record Key. */
2424+ rkey: string
2525+ /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */
2626+ validate?: boolean
2727+ /** The record to write. */
2828+ record: { [_ in string]: unknown }
2929+ /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */
3030+ swapRecord?: string | null
3131+ /** Compare and swap with the previous commit by CID. */
3232+ swapCommit?: string
3333+}
3434+3535+export interface OutputSchema {
3636+ uri: string
3737+ cid: string
3838+ commit?: ComAtprotoRepoDefs.CommitMeta
3939+ validationStatus?: 'valid' | 'unknown' | (string & {})
4040+}
4141+4242+export interface CallOptions {
4343+ signal?: AbortSignal
4444+ headers?: HeadersMap
4545+ qp?: QueryParams
4646+ encoding?: 'application/json'
4747+}
4848+4949+export interface Response {
5050+ success: boolean
5151+ headers: HeadersMap
5252+ data: OutputSchema
5353+}
5454+5555+export class InvalidSwapError extends XRPCError {
5656+ constructor(src: XRPCError) {
5757+ super(src.status, src.error, src.message, src.headers, { cause: src })
5858+ }
5959+}
6060+6161+export function toKnownErr(e: any) {
6262+ if (e instanceof XRPCError) {
6363+ if (e.error === 'InvalidSwap') return new InvalidSwapError(e)
6464+ }
6565+6666+ return e
6767+}
···44 NodeSavedState,
55 NodeSavedStateStore,
66} from '@atproto/oauth-client-node'
77+78import type { Database } from '#/db'
89910export class StateStore implements NodeSavedStateStore {
···11-import events from 'node:events'
22-import type http from 'node:http'
33-import express, { type Express } from 'express'
44-import { pino } from 'pino'
55-import type { OAuthClient } from '@atproto/oauth-client-node'
66-import { Firehose } from '@atproto/sync'
77-88-import { createDb, migrateToLatest } from '#/db'
99-import { env } from '#/lib/env'
1010-import { createIngester } from '#/ingester'
1111-import { createRouter } from '#/routes'
1212-import { createClient } from '#/auth/client'
1313-import {
1414- createBidirectionalResolver,
1515- createIdResolver,
1616- BidirectionalResolver,
1717-} from '#/id-resolver'
1818-import type { Database } from '#/db'
1919-import { IdResolver, MemoryCache } from '@atproto/identity'
2020-2121-// Application state passed to the router and elsewhere
2222-export type AppContext = {
2323- db: Database
2424- ingester: Firehose
2525- logger: pino.Logger
2626- oauthClient: OAuthClient
2727- resolver: BidirectionalResolver
2828-}
2929-3030-export class Server {
3131- constructor(
3232- public app: express.Application,
3333- public server: http.Server,
3434- public ctx: AppContext,
3535- ) {}
3636-3737- static async create() {
3838- const { NODE_ENV, HOST, PORT, DB_PATH } = env
3939- const logger = pino({ name: 'server start' })
4040-4141- // Set up the SQLite database
4242- const db = createDb(DB_PATH)
4343- await migrateToLatest(db)
4444-4545- // Create the atproto utilities
4646- const oauthClient = await createClient(db)
4747- const baseIdResolver = createIdResolver()
4848- const ingester = createIngester(db, baseIdResolver)
4949- const resolver = createBidirectionalResolver(baseIdResolver)
5050- const ctx = {
5151- db,
5252- ingester,
5353- logger,
5454- oauthClient,
5555- resolver,
5656- }
5757-5858- // Subscribe to events on the firehose
5959- ingester.start()
6060-6161- // Create our server
6262- const app: Express = express()
6363- app.set('trust proxy', true)
6464-6565- // Routes & middlewares
6666- const router = createRouter(ctx)
6767- app.use(express.json())
6868- app.use(express.urlencoded({ extended: true }))
6969- app.use(router)
7070- app.use('*', (_req, res) => {
7171- res.sendStatus(404)
7272- })
7373-7474- // Bind our server to the port
7575- const server = app.listen(env.PORT)
7676- await events.once(server, 'listening')
7777- logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`)
7878-7979- return new Server(app, server, ctx)
8080- }
8181-8282- async close() {
8383- this.ctx.logger.info('sigint received, shutting down')
8484- await this.ctx.ingester.destroy()
8585- return new Promise<void>((resolve) => {
8686- this.server.close(() => {
8787- this.ctx.logger.info('server closed')
8888- resolve()
8989- })
9090- })
9191- }
9292-}
9393-9494-const run = async () => {
9595- const server = await Server.create()
9696-9797- const onCloseSignal = async () => {
9898- setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s
9999- await server.close()
100100- process.exit()
101101- }
102102-103103- process.on('SIGINT', onCloseSignal)
104104- process.on('SIGTERM', onCloseSignal)
105105-}
106106-107107-run()
+5-4
src/ingester.ts
packages/appview/src/ingester.ts
···11-import pino from 'pino'
21import { IdResolver } from '@atproto/identity'
32import { Firehose, type Event } from '@atproto/sync'
33+import { XyzStatusphereStatus } from '@statusphere/lexicon'
44+import pino from 'pino'
55+46import type { Database } from '#/db'
55-import * as Status from '#/lexicon/types/xyz/statusphere/status'
6778export function createIngester(db: Database, idResolver: IdResolver) {
89 const logger = pino({ name: 'firehose ingestion' })
···1718 // If the write is a valid status update
1819 if (
1920 evt.collection === 'xyz.statusphere.status' &&
2020- Status.isRecord(record)
2121+ XyzStatusphereStatus.isRecord(record)
2122 ) {
2222- const validatedRecord = Status.validateRecord(record)
2323+ const validatedRecord = XyzStatusphereStatus.validateRecord(record)
2324 if (!validatedRecord.success) return
2425 // Store the status in our SQLite
2526 await db
-129
src/lexicon/index.ts
···11-/**
22- * GENERATED CODE - DO NOT MODIFY
33- */
44-import {
55- createServer as createXrpcServer,
66- Server as XrpcServer,
77- Options as XrpcOptions,
88- AuthVerifier,
99- StreamAuthVerifier,
1010-} from '@atproto/xrpc-server'
1111-import { schemas } from './lexicons.js'
1212-1313-export function createServer(options?: XrpcOptions): Server {
1414- return new Server(options)
1515-}
1616-1717-export class Server {
1818- xrpc: XrpcServer
1919- app: AppNS
2020- xyz: XyzNS
2121- com: ComNS
2222-2323- constructor(options?: XrpcOptions) {
2424- this.xrpc = createXrpcServer(schemas, options)
2525- this.app = new AppNS(this)
2626- this.xyz = new XyzNS(this)
2727- this.com = new ComNS(this)
2828- }
2929-}
3030-3131-export class AppNS {
3232- _server: Server
3333- bsky: AppBskyNS
3434-3535- constructor(server: Server) {
3636- this._server = server
3737- this.bsky = new AppBskyNS(server)
3838- }
3939-}
4040-4141-export class AppBskyNS {
4242- _server: Server
4343- actor: AppBskyActorNS
4444-4545- constructor(server: Server) {
4646- this._server = server
4747- this.actor = new AppBskyActorNS(server)
4848- }
4949-}
5050-5151-export class AppBskyActorNS {
5252- _server: Server
5353-5454- constructor(server: Server) {
5555- this._server = server
5656- }
5757-}
5858-5959-export class XyzNS {
6060- _server: Server
6161- statusphere: XyzStatusphereNS
6262-6363- constructor(server: Server) {
6464- this._server = server
6565- this.statusphere = new XyzStatusphereNS(server)
6666- }
6767-}
6868-6969-export class XyzStatusphereNS {
7070- _server: Server
7171-7272- constructor(server: Server) {
7373- this._server = server
7474- }
7575-}
7676-7777-export class ComNS {
7878- _server: Server
7979- atproto: ComAtprotoNS
8080-8181- constructor(server: Server) {
8282- this._server = server
8383- this.atproto = new ComAtprotoNS(server)
8484- }
8585-}
8686-8787-export class ComAtprotoNS {
8888- _server: Server
8989- repo: ComAtprotoRepoNS
9090-9191- constructor(server: Server) {
9292- this._server = server
9393- this.repo = new ComAtprotoRepoNS(server)
9494- }
9595-}
9696-9797-export class ComAtprotoRepoNS {
9898- _server: Server
9999-100100- constructor(server: Server) {
101101- this._server = server
102102- }
103103-}
104104-105105-type SharedRateLimitOpts<T> = {
106106- name: string
107107- calcKey?: (ctx: T) => string | null
108108- calcPoints?: (ctx: T) => number
109109-}
110110-type RouteRateLimitOpts<T> = {
111111- durationMs: number
112112- points: number
113113- calcKey?: (ctx: T) => string | null
114114- calcPoints?: (ctx: T) => number
115115-}
116116-type HandlerOpts = { blobLimit?: number }
117117-type HandlerRateLimitOpts<T> = SharedRateLimitOpts<T> | RouteRateLimitOpts<T>
118118-type ConfigOf<Auth, Handler, ReqCtx> =
119119- | Handler
120120- | {
121121- auth?: Auth
122122- opts?: HandlerOpts
123123- rateLimit?: HandlerRateLimitOpts<ReqCtx> | HandlerRateLimitOpts<ReqCtx>[]
124124- handler: Handler
125125- }
126126-type ExtractAuth<AV extends AuthVerifier | StreamAuthVerifier> = Extract<
127127- Awaited<ReturnType<AV>>,
128128- { credentials: unknown }
129129->
-332
src/lexicon/lexicons.ts
···11-/**
22- * GENERATED CODE - DO NOT MODIFY
33- */
44-import {
55- LexiconDoc,
66- Lexicons,
77- ValidationError,
88- ValidationResult,
99-} from '@atproto/lexicon'
1010-import { $Typed, is$typed, maybe$typed } from './util.js'
1111-1212-export const schemaDict = {
1313- ComAtprotoLabelDefs: {
1414- lexicon: 1,
1515- id: 'com.atproto.label.defs',
1616- defs: {
1717- label: {
1818- type: 'object',
1919- description:
2020- 'Metadata tag on an atproto resource (eg, repo or record).',
2121- required: ['src', 'uri', 'val', 'cts'],
2222- properties: {
2323- ver: {
2424- type: 'integer',
2525- description: 'The AT Protocol version of the label object.',
2626- },
2727- src: {
2828- type: 'string',
2929- format: 'did',
3030- description: 'DID of the actor who created this label.',
3131- },
3232- uri: {
3333- type: 'string',
3434- format: 'uri',
3535- description:
3636- 'AT URI of the record, repository (account), or other resource that this label applies to.',
3737- },
3838- cid: {
3939- type: 'string',
4040- format: 'cid',
4141- description:
4242- "Optionally, CID specifying the specific version of 'uri' resource this label applies to.",
4343- },
4444- val: {
4545- type: 'string',
4646- maxLength: 128,
4747- description:
4848- 'The short string name of the value or type of this label.',
4949- },
5050- neg: {
5151- type: 'boolean',
5252- description:
5353- 'If true, this is a negation label, overwriting a previous label.',
5454- },
5555- cts: {
5656- type: 'string',
5757- format: 'datetime',
5858- description: 'Timestamp when this label was created.',
5959- },
6060- exp: {
6161- type: 'string',
6262- format: 'datetime',
6363- description:
6464- 'Timestamp at which this label expires (no longer applies).',
6565- },
6666- sig: {
6767- type: 'bytes',
6868- description: 'Signature of dag-cbor encoded label.',
6969- },
7070- },
7171- },
7272- selfLabels: {
7373- type: 'object',
7474- description:
7575- 'Metadata tags on an atproto record, published by the author within the record.',
7676- required: ['values'],
7777- properties: {
7878- values: {
7979- type: 'array',
8080- items: {
8181- type: 'ref',
8282- ref: 'lex:com.atproto.label.defs#selfLabel',
8383- },
8484- maxLength: 10,
8585- },
8686- },
8787- },
8888- selfLabel: {
8989- type: 'object',
9090- description:
9191- 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',
9292- required: ['val'],
9393- properties: {
9494- val: {
9595- type: 'string',
9696- maxLength: 128,
9797- description:
9898- 'The short string name of the value or type of this label.',
9999- },
100100- },
101101- },
102102- labelValueDefinition: {
103103- type: 'object',
104104- description:
105105- 'Declares a label value and its expected interpretations and behaviors.',
106106- required: ['identifier', 'severity', 'blurs', 'locales'],
107107- properties: {
108108- identifier: {
109109- type: 'string',
110110- description:
111111- "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
112112- maxLength: 100,
113113- maxGraphemes: 100,
114114- },
115115- severity: {
116116- type: 'string',
117117- description:
118118- "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
119119- knownValues: ['inform', 'alert', 'none'],
120120- },
121121- blurs: {
122122- type: 'string',
123123- description:
124124- "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.",
125125- knownValues: ['content', 'media', 'none'],
126126- },
127127- defaultSetting: {
128128- type: 'string',
129129- description: 'The default setting for this label.',
130130- knownValues: ['ignore', 'warn', 'hide'],
131131- default: 'warn',
132132- },
133133- adultOnly: {
134134- type: 'boolean',
135135- description:
136136- 'Does the user need to have adult content enabled in order to configure this label?',
137137- },
138138- locales: {
139139- type: 'array',
140140- items: {
141141- type: 'ref',
142142- ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',
143143- },
144144- },
145145- },
146146- },
147147- labelValueDefinitionStrings: {
148148- type: 'object',
149149- description:
150150- 'Strings which describe the label in the UI, localized into a specific language.',
151151- required: ['lang', 'name', 'description'],
152152- properties: {
153153- lang: {
154154- type: 'string',
155155- description:
156156- 'The code of the language these strings are written in.',
157157- format: 'language',
158158- },
159159- name: {
160160- type: 'string',
161161- description: 'A short human-readable name for the label.',
162162- maxGraphemes: 64,
163163- maxLength: 640,
164164- },
165165- description: {
166166- type: 'string',
167167- description:
168168- 'A longer description of what the label means and why it might be applied.',
169169- maxGraphemes: 10000,
170170- maxLength: 100000,
171171- },
172172- },
173173- },
174174- labelValue: {
175175- type: 'string',
176176- knownValues: [
177177- '!hide',
178178- '!no-promote',
179179- '!warn',
180180- '!no-unauthenticated',
181181- 'dmca-violation',
182182- 'doxxing',
183183- 'porn',
184184- 'sexual',
185185- 'nudity',
186186- 'nsfl',
187187- 'gore',
188188- ],
189189- },
190190- },
191191- },
192192- AppBskyActorProfile: {
193193- lexicon: 1,
194194- id: 'app.bsky.actor.profile',
195195- defs: {
196196- main: {
197197- type: 'record',
198198- description: 'A declaration of a Bluesky account profile.',
199199- key: 'literal:self',
200200- record: {
201201- type: 'object',
202202- properties: {
203203- displayName: {
204204- type: 'string',
205205- maxGraphemes: 64,
206206- maxLength: 640,
207207- },
208208- description: {
209209- type: 'string',
210210- description: 'Free-form profile description text.',
211211- maxGraphemes: 256,
212212- maxLength: 2560,
213213- },
214214- avatar: {
215215- type: 'blob',
216216- description:
217217- "Small image to be displayed next to posts from account. AKA, 'profile picture'",
218218- accept: ['image/png', 'image/jpeg'],
219219- maxSize: 1000000,
220220- },
221221- banner: {
222222- type: 'blob',
223223- description:
224224- 'Larger horizontal image to display behind profile view.',
225225- accept: ['image/png', 'image/jpeg'],
226226- maxSize: 1000000,
227227- },
228228- labels: {
229229- type: 'union',
230230- description:
231231- 'Self-label values, specific to the Bluesky application, on the overall account.',
232232- refs: ['lex:com.atproto.label.defs#selfLabels'],
233233- },
234234- joinedViaStarterPack: {
235235- type: 'ref',
236236- ref: 'lex:com.atproto.repo.strongRef',
237237- },
238238- createdAt: {
239239- type: 'string',
240240- format: 'datetime',
241241- },
242242- },
243243- },
244244- },
245245- },
246246- },
247247- XyzStatusphereStatus: {
248248- lexicon: 1,
249249- id: 'xyz.statusphere.status',
250250- defs: {
251251- main: {
252252- type: 'record',
253253- key: 'tid',
254254- record: {
255255- type: 'object',
256256- required: ['status', 'createdAt'],
257257- properties: {
258258- status: {
259259- type: 'string',
260260- minLength: 1,
261261- maxGraphemes: 1,
262262- maxLength: 32,
263263- },
264264- createdAt: {
265265- type: 'string',
266266- format: 'datetime',
267267- },
268268- },
269269- },
270270- },
271271- },
272272- },
273273- ComAtprotoRepoStrongRef: {
274274- lexicon: 1,
275275- id: 'com.atproto.repo.strongRef',
276276- description: 'A URI with a content-hash fingerprint.',
277277- defs: {
278278- main: {
279279- type: 'object',
280280- required: ['uri', 'cid'],
281281- properties: {
282282- uri: {
283283- type: 'string',
284284- format: 'at-uri',
285285- },
286286- cid: {
287287- type: 'string',
288288- format: 'cid',
289289- },
290290- },
291291- },
292292- },
293293- },
294294-} as const satisfies Record<string, LexiconDoc>
295295-296296-export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]
297297-export const lexicons: Lexicons = new Lexicons(schemas)
298298-299299-export function validate<T extends { $type: string }>(
300300- v: unknown,
301301- id: string,
302302- hash: string,
303303- requiredType: true,
304304-): ValidationResult<T>
305305-export function validate<T extends { $type?: string }>(
306306- v: unknown,
307307- id: string,
308308- hash: string,
309309- requiredType?: false,
310310-): ValidationResult<T>
311311-export function validate(
312312- v: unknown,
313313- id: string,
314314- hash: string,
315315- requiredType?: boolean,
316316-): ValidationResult {
317317- return (requiredType ? is$typed : maybe$typed)(v, id, hash)
318318- ? lexicons.validate(`${id}#${hash}`, v)
319319- : {
320320- success: false,
321321- error: new ValidationError(
322322- `Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`,
323323- ),
324324- }
325325-}
326326-327327-export const ids = {
328328- ComAtprotoLabelDefs: 'com.atproto.label.defs',
329329- AppBskyActorProfile: 'app.bsky.actor.profile',
330330- XyzStatusphereStatus: 'xyz.statusphere.status',
331331- ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
332332-} as const
···11-// @ts-ignore
22-import ssr from 'uhtml/ssr'
33-import type initSSR from 'uhtml/types/init-ssr'
44-import type { Hole } from 'uhtml/types/keyed'
55-66-export type { Hole }
77-88-export const { html }: ReturnType<typeof initSSR> = ssr()
99-1010-export function page(hole: Hole) {
1111- return `<!DOCTYPE html>\n${hole.toDOM().toString()}`
1212-}