the statusphere demo reworked into a vite/react app in a monorepo

Compare changes

Choose any two refs to compare.

+27
.tangled/workflows/deploy.yml
··· 1 + # Set the following secrets in your repo's pipeline settings: 2 + # RAILWAY_TOKEN 3 + # RAILWAY_SERVICE_ID 4 + 5 + when: 6 + - event: ["push"] 7 + branch: ["main"] 8 + 9 + engine: "nixery" 10 + 11 + dependencies: 12 + nixpkgs: 13 + - rustup 14 + - gcc 15 + 16 + steps: 17 + - name: Install Rust toolchain 18 + command: rustup default stable 19 + 20 + - name: Install Railway CLI 21 + command: cargo install railwayapp --locked 22 + 23 + - name: Link `railway` executable 24 + command: ln -s /tangled/home/.cargo/bin/railway /bin/railway 25 + 26 + - name: Deploy to Railway 27 + command: railway up --ci --service=$RAILWAY_SERVICE_ID
+1 -1
README.md
··· 68 68 69 69 The backend server will: 70 70 71 - - Serve the API at `/api/*` endpoints 71 + - Serve the API at `/xrpc/*` and `/oauth/*` endpoints 72 72 - Serve the frontend static files from the client's build directory 73 73 - Handle client-side routing by serving index.html for all non-API routes 74 74
+37
lexicons/xyz/statusphere/getStatuses.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.getStatuses", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a list of the most recent statuses on the network.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 100, 15 + "default": 50 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["statuses"], 24 + "properties": { 25 + "statuses": { 26 + "type": "array", 27 + "items": { 28 + "type": "ref", 29 + "ref": "xyz.statusphere.defs#statusView" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+31
lexicons/xyz/statusphere/getUser.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.getUser", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the current user's profile and status.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": {} 11 + }, 12 + "output": { 13 + "encoding": "application/json", 14 + "schema": { 15 + "type": "object", 16 + "required": ["profile"], 17 + "properties": { 18 + "profile": { 19 + "type": "ref", 20 + "ref": "app.bsky.actor.defs#profileView" 21 + }, 22 + "status": { 23 + "type": "ref", 24 + "ref": "xyz.statusphere.defs#statusView" 25 + } 26 + } 27 + } 28 + } 29 + } 30 + } 31 + }
+38
lexicons/xyz/statusphere/sendStatus.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.sendStatus", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Send a status into the ATmosphere.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["status"], 13 + "properties": { 14 + "status": { 15 + "type": "string", 16 + "minLength": 1, 17 + "maxGraphemes": 1, 18 + "maxLength": 32 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": ["status"], 28 + "properties": { 29 + "status": { 30 + "type": "ref", 31 + "ref": "xyz.statusphere.defs#statusView" 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+1 -1
package.json
··· 10 10 "dev:lexicon": "pnpm --filter @statusphere/lexicon dev", 11 11 "dev:appview": "pnpm --filter @statusphere/appview dev", 12 12 "dev:client": "pnpm --filter @statusphere/client dev", 13 - "lexgen": "pnpm --filter @statusphere/lexicon lexgen", 13 + "lexgen": "pnpm -r lexgen", 14 14 "build": "pnpm build:lexicon && pnpm build:client && pnpm build:appview", 15 15 "build:lexicon": "pnpm --filter @statusphere/lexicon build", 16 16 "build:appview": "pnpm --filter @statusphere/appview build",
+1 -1
packages/appview/README.md
··· 55 55 56 56 ## API Endpoints 57 57 58 - - `GET /client-metadata.json` - OAuth client metadata 58 + - `GET /oauth-client-metadata.json` - OAuth client metadata 59 59 - `GET /oauth/callback` - OAuth callback endpoint 60 60 - `POST /login` - Login with handle 61 61 - `POST /logout` - Logout current user
+6 -1
packages/appview/package.json
··· 10 10 "dev": "tsx watch --clear-screen=false src/index.ts | pino-pretty", 11 11 "build": "tsup", 12 12 "start": "node dist/index.js", 13 + "lexgen": "lex gen-server ./src/lexicons ../../lexicons/xyz/statusphere/* ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* --yes && pnpm format", 13 14 "clean": "rimraf dist coverage", 14 15 "format": "prettier --write src", 15 16 "typecheck": "tsc --noEmit" ··· 25 26 "@atproto/xrpc-server": "^0.7.11", 26 27 "@statusphere/lexicon": "workspace:*", 27 28 "better-sqlite3": "^11.8.1", 29 + "compression": "^1.8.0", 28 30 "cors": "^2.8.5", 29 31 "dotenv": "^16.4.7", 30 32 "envalid": "^8.0.0", ··· 32 34 "iron-session": "^8.0.4", 33 35 "kysely": "^0.27.5", 34 36 "multiformats": "^13.3.2", 35 - "pino": "^9.6.0" 37 + "pino": "^9.6.0", 38 + "ws": "^8.18.1" 36 39 }, 37 40 "devDependencies": { 41 + "@atproto/lex-cli": "^0.6.1", 38 42 "@types/better-sqlite3": "^7.6.12", 43 + "@types/compression": "^1.7.5", 39 44 "@types/cors": "^2.8.17", 40 45 "@types/express": "^5.0.0", 41 46 "@types/node": "^22.13.8",
+13
packages/appview/src/api/health.ts
··· 1 + import { Router } from 'express' 2 + 3 + import { AppContext } from '#/context' 4 + 5 + export const createRouter = (ctx: AppContext) => { 6 + const router = Router() 7 + 8 + router.get('/health', async function (req, res) { 9 + res.status(200).send('OK') 10 + }) 11 + 12 + return router 13 + }
+15
packages/appview/src/api/index.ts
··· 1 + import { AppContext } from '#/context' 2 + import { Server } from '#/lexicons' 3 + import getStatuses from './lexicons/getStatuses' 4 + import getUser from './lexicons/getUser' 5 + import sendStatus from './lexicons/sendStatus' 6 + 7 + export * as health from './health' 8 + export * as oauth from './oauth' 9 + 10 + export default function (server: Server, ctx: AppContext) { 11 + getStatuses(server, ctx) 12 + sendStatus(server, ctx) 13 + getUser(server, ctx) 14 + return server 15 + }
+26
packages/appview/src/api/lexicons/getStatuses.ts
··· 1 + import { AppContext } from '#/context' 2 + import { Server } from '#/lexicons' 3 + import { statusToStatusView } from '#/lib/hydrate' 4 + 5 + export default function (server: Server, ctx: AppContext) { 6 + server.xyz.statusphere.getStatuses({ 7 + handler: async ({ params }) => { 8 + // Fetch data stored in our SQLite 9 + const statuses = await ctx.db 10 + .selectFrom('status') 11 + .selectAll() 12 + .orderBy('indexedAt', 'desc') 13 + .limit(params.limit) 14 + .execute() 15 + 16 + return { 17 + encoding: 'application/json', 18 + body: { 19 + statuses: await Promise.all( 20 + statuses.map((status) => statusToStatusView(status, ctx)), 21 + ), 22 + }, 23 + } 24 + }, 25 + }) 26 + }
+61
packages/appview/src/api/lexicons/getUser.ts
··· 1 + import { AuthRequiredError } from '@atproto/xrpc-server' 2 + import { AppBskyActorProfile } from '@statusphere/lexicon' 3 + 4 + import { AppContext } from '#/context' 5 + import { Server } from '#/lexicons' 6 + import { bskyProfileToProfileView, statusToStatusView } from '#/lib/hydrate' 7 + import { getSessionAgent } from '#/session' 8 + 9 + export default function (server: Server, ctx: AppContext) { 10 + server.xyz.statusphere.getUser({ 11 + handler: async ({ req, res }) => { 12 + const agent = await getSessionAgent(req, res, ctx) 13 + if (!agent) { 14 + throw new AuthRequiredError('Authentication required') 15 + } 16 + 17 + const did = agent.assertDid 18 + 19 + const profileResponse = await agent.com.atproto.repo 20 + .getRecord({ 21 + repo: did, 22 + collection: 'app.bsky.actor.profile', 23 + rkey: 'self', 24 + }) 25 + .catch(() => undefined) 26 + 27 + const profileRecord = profileResponse?.data 28 + let profile: AppBskyActorProfile.Record = {} as AppBskyActorProfile.Record 29 + 30 + if (profileRecord && AppBskyActorProfile.isRecord(profileRecord.value)) { 31 + const validated = AppBskyActorProfile.validateRecord( 32 + profileRecord.value, 33 + ) 34 + if (validated.success) { 35 + profile = profileRecord.value 36 + } else { 37 + ctx.logger.error( 38 + { err: validated.error }, 39 + 'Failed to validate user profile', 40 + ) 41 + } 42 + } 43 + 44 + // Fetch user status 45 + const status = await ctx.db 46 + .selectFrom('status') 47 + .selectAll() 48 + .where('authorDid', '=', did) 49 + .orderBy('indexedAt', 'desc') 50 + .executeTakeFirst() 51 + 52 + return { 53 + encoding: 'application/json', 54 + body: { 55 + profile: await bskyProfileToProfileView(did, profile, ctx), 56 + status: status ? await statusToStatusView(status, ctx) : undefined, 57 + }, 58 + } 59 + }, 60 + }) 61 + }
+79
packages/appview/src/api/lexicons/sendStatus.ts
··· 1 + import { TID } from '@atproto/common' 2 + import { 3 + AuthRequiredError, 4 + InvalidRequestError, 5 + UpstreamFailureError, 6 + } from '@atproto/xrpc-server' 7 + import { XyzStatusphereStatus } from '@statusphere/lexicon' 8 + 9 + import { AppContext } from '#/context' 10 + import { Server } from '#/lexicons' 11 + import { statusToStatusView } from '#/lib/hydrate' 12 + import { getSessionAgent } from '#/session' 13 + 14 + export default function (server: Server, ctx: AppContext) { 15 + server.xyz.statusphere.sendStatus({ 16 + handler: async ({ input, req, res }) => { 17 + const agent = await getSessionAgent(req, res, ctx) 18 + if (!agent) { 19 + throw new AuthRequiredError('Authentication required') 20 + } 21 + 22 + // Construct & validate their status record 23 + const rkey = TID.nextStr() 24 + const record = { 25 + $type: 'xyz.statusphere.status', 26 + status: input.body.status, 27 + createdAt: new Date().toISOString(), 28 + } 29 + 30 + const validation = XyzStatusphereStatus.validateRecord(record) 31 + if (!validation.success) { 32 + throw new InvalidRequestError('Invalid status') 33 + } 34 + 35 + let uri 36 + try { 37 + // Write the status record to the user's repository 38 + const response = await agent.com.atproto.repo.putRecord({ 39 + repo: agent.assertDid, 40 + collection: 'xyz.statusphere.status', 41 + rkey, 42 + record: validation.value, 43 + validate: false, 44 + }) 45 + uri = response.data.uri 46 + } catch (err) { 47 + throw new UpstreamFailureError('Failed to write record') 48 + } 49 + 50 + const optimisticStatus = { 51 + uri, 52 + authorDid: agent.assertDid, 53 + status: record.status, 54 + createdAt: record.createdAt, 55 + indexedAt: new Date().toISOString(), 56 + } 57 + 58 + try { 59 + // Optimistically update our SQLite 60 + // This isn't strictly necessary because the write event will be 61 + // handled in #/firehose/ingestor.ts, but it ensures that future reads 62 + // will be up-to-date after this method finishes. 63 + await ctx.db.insertInto('status').values(optimisticStatus).execute() 64 + } catch (err) { 65 + ctx.logger.warn( 66 + { err }, 67 + 'failed to update computed view; ignoring as it should be caught by the firehose', 68 + ) 69 + } 70 + 71 + return { 72 + encoding: 'application/json', 73 + body: { 74 + status: await statusToStatusView(optimisticStatus, ctx), 75 + }, 76 + } 77 + }, 78 + }) 79 + }
+96
packages/appview/src/api/oauth.ts
··· 1 + import { OAuthResolverError } from '@atproto/oauth-client-node' 2 + import { isValidHandle } from '@atproto/syntax' 3 + import express from 'express' 4 + 5 + import { AppContext } from '#/context' 6 + import { getSession } from '#/session' 7 + 8 + export const createRouter = (ctx: AppContext) => { 9 + const router = express.Router() 10 + 11 + // OAuth metadata 12 + router.get('/oauth-client-metadata.json', (_req, res) => { 13 + res.json(ctx.oauthClient.clientMetadata) 14 + }) 15 + 16 + // OAuth callback to complete session creation 17 + router.get('/oauth/callback', async (req, res) => { 18 + // Get the query parameters from the URL 19 + const params = new URLSearchParams(req.originalUrl.split('?')[1]) 20 + 21 + try { 22 + const { session } = await ctx.oauthClient.callback(params) 23 + 24 + // Use the common session options 25 + const clientSession = await getSession(req, res) 26 + 27 + // Set the DID on the session 28 + clientSession.did = session.did 29 + await clientSession.save() 30 + 31 + // Get the origin and determine appropriate redirect 32 + const host = req.get('host') || '' 33 + const protocol = req.protocol || 'http' 34 + const baseUrl = `${protocol}://${host}` 35 + 36 + ctx.logger.info( 37 + `OAuth callback successful, redirecting to ${baseUrl}/oauth-callback`, 38 + ) 39 + 40 + // Redirect to the frontend oauth-callback page 41 + res.redirect('/oauth-callback') 42 + } catch (err) { 43 + ctx.logger.error({ err }, 'oauth callback failed') 44 + 45 + // Handle error redirect - stay on same domain 46 + res.redirect('/oauth-callback?error=auth') 47 + } 48 + }) 49 + 50 + // Login handler 51 + router.post('/oauth/initiate', async (req, res) => { 52 + // Validate 53 + const handle = req.body?.handle 54 + if ( 55 + typeof handle !== 'string' || 56 + !(isValidHandle(handle) || isValidUrl(handle)) 57 + ) { 58 + res.status(400).json({ error: 'Invalid handle' }) 59 + return 60 + } 61 + 62 + // Initiate the OAuth flow 63 + try { 64 + const url = await ctx.oauthClient.authorize(handle, { 65 + scope: 'atproto transition:generic', 66 + }) 67 + res.json({ redirectUrl: url.toString() }) 68 + } catch (err) { 69 + ctx.logger.error({ err }, 'oauth authorize failed') 70 + const errorMsg = 71 + err instanceof OAuthResolverError 72 + ? err.message 73 + : "Couldn't initiate login" 74 + res.status(500).json({ error: errorMsg }) 75 + } 76 + }) 77 + 78 + // Logout handler 79 + router.post('/oauth/logout', async (req, res) => { 80 + const session = await getSession(req, res) 81 + session.destroy() 82 + res.json({ success: true }) 83 + }) 84 + 85 + return router 86 + } 87 + 88 + function isValidUrl(url: string): boolean { 89 + try { 90 + const urlp = new URL(url) 91 + // http or https 92 + return urlp.protocol === 'http:' || urlp.protocol === 'https:' 93 + } catch (error) { 94 + return false 95 + } 96 + }
+3 -3
packages/appview/src/auth/client.ts
··· 17 17 clientMetadata: { 18 18 client_name: 'Statusphere React App', 19 19 client_id: publicUrl 20 - ? `${url}/api/client-metadata.json` 21 - : `http://localhost?redirect_uri=${enc(`${url}/api/oauth/callback`)}&scope=${enc('atproto transition:generic')}`, 20 + ? `${url}/oauth-client-metadata.json` 21 + : `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc('atproto transition:generic')}`, 22 22 client_uri: url, 23 - redirect_uris: [`${url}/api/oauth/callback`], 23 + redirect_uris: [`${url}/oauth/callback`], 24 24 scope: 'atproto transition:generic', 25 25 grant_types: ['authorization_code', 'refresh_token'], 26 26 response_types: ['code'],
+16
packages/appview/src/context.ts
··· 1 + import { OAuthClient } from '@atproto/oauth-client-node' 2 + import { Firehose } from '@atproto/sync' 3 + import pino from 'pino' 4 + 5 + import { Database } from '#/db' 6 + import { BidirectionalResolver } from '#/id-resolver' 7 + import { Jetstream } from '#/ingestors' 8 + 9 + // Application state passed to the router and elsewhere 10 + export type AppContext = { 11 + db: Database 12 + ingester: Firehose | Jetstream<any> 13 + logger: pino.Logger 14 + oauthClient: OAuthClient 15 + resolver: BidirectionalResolver 16 + }
+15
packages/appview/src/db.ts
··· 53 53 }, 54 54 } 55 55 56 + migrations['003'] = { 57 + async up(db: Kysely<unknown>) {}, 58 + async down(_db: Kysely<unknown>) {}, 59 + } 60 + 56 61 migrations['002'] = { 57 62 async up(db: Kysely<unknown>) { 58 63 await db.schema 59 64 .createTable('cursor') 60 65 .addColumn('id', 'integer', (col) => col.primaryKey()) 61 66 .addColumn('seq', 'integer', (col) => col.notNull()) 67 + .execute() 68 + 69 + // Insert initial cursor values: 70 + // id=1 is for firehose, id=2 is for jetstream 71 + await db 72 + .insertInto('cursor' as never) 73 + .values([ 74 + { id: 1, seq: 0 }, 75 + { id: 2, seq: 0 }, 76 + ]) 62 77 .execute() 63 78 }, 64 79 async down(db: Kysely<unknown>) {
+14
packages/appview/src/error.ts
··· 1 + import { XRPCError } from '@atproto/xrpc-server' 2 + import { ErrorRequestHandler } from 'express' 3 + 4 + import { AppContext } from '#/context' 5 + 6 + export const createHandler: (ctx: AppContext) => ErrorRequestHandler = 7 + (ctx) => (err, _req, res, next) => { 8 + ctx.logger.error('unexpected internal server error', err) 9 + if (res.headersSent) { 10 + return next(err) 11 + } 12 + const serverError = XRPCError.fromError(err) 13 + res.status(serverError.type).json(serverError.payload) 14 + }
+39 -78
packages/appview/src/index.ts
··· 2 2 import fs from 'node:fs' 3 3 import type http from 'node:http' 4 4 import path from 'node:path' 5 - import type { OAuthClient } from '@atproto/oauth-client-node' 6 - import { Firehose } from '@atproto/sync' 5 + import { DAY, SECOND } from '@atproto/common' 6 + import compression from 'compression' 7 7 import cors from 'cors' 8 - import express, { type Express } from 'express' 8 + import express from 'express' 9 9 import { pino } from 'pino' 10 10 11 + import API, { health, oauth } from '#/api' 11 12 import { createClient } from '#/auth/client' 13 + import { AppContext } from '#/context' 12 14 import { createDb, migrateToLatest } from '#/db' 13 - import type { Database } from '#/db' 14 - import { 15 - BidirectionalResolver, 16 - createBidirectionalResolver, 17 - createIdResolver, 18 - } from '#/id-resolver' 19 - import { createIngester } from '#/ingester' 15 + import * as error from '#/error' 16 + import { createBidirectionalResolver, createIdResolver } from '#/id-resolver' 17 + import { createFirehoseIngester, createJetstreamIngester } from '#/ingestors' 18 + import { createServer } from '#/lexicons' 20 19 import { env } from '#/lib/env' 21 - import { createRouter } from '#/routes' 22 - 23 - // Application state passed to the router and elsewhere 24 - export type AppContext = { 25 - db: Database 26 - ingester: Firehose 27 - logger: pino.Logger 28 - oauthClient: OAuthClient 29 - resolver: BidirectionalResolver 30 - } 31 20 32 21 export class Server { 33 22 constructor( ··· 47 36 // Create the atproto utilities 48 37 const oauthClient = await createClient(db) 49 38 const baseIdResolver = createIdResolver() 50 - const ingester = await createIngester(db, baseIdResolver) 39 + const ingester = await createJetstreamIngester(db) 40 + // Alternative: const ingester = await createFirehoseIngester(db, baseIdResolver) 51 41 const resolver = createBidirectionalResolver(baseIdResolver) 52 42 const ctx = { 53 43 db, ··· 59 49 60 50 // Subscribe to events on the firehose 61 51 ingester.start() 52 + 53 + const app = express() 54 + app.use(cors({ maxAge: DAY / SECOND })) 55 + app.use(compression()) 56 + app.use(express.json()) 57 + app.use(express.urlencoded({ extended: true })) 62 58 63 59 // Create our server 64 - const app: Express = express() 65 - app.set('trust proxy', true) 60 + let server = createServer({ 61 + validateResponse: env.isDevelopment, 62 + payload: { 63 + jsonLimit: 100 * 1024, // 100kb 64 + textLimit: 100 * 1024, // 100kb 65 + // no blobs 66 + blobLimit: 0, 67 + }, 68 + }) 66 69 67 - // CORS configuration based on environment 68 - if (env.NODE_ENV === 'development') { 69 - // In development, allow multiple origins including ngrok 70 - app.use( 71 - cors({ 72 - origin: function (origin, callback) { 73 - // Allow requests with no origin (like mobile apps, curl) 74 - if (!origin) return callback(null, true) 70 + server = API(server, ctx) 75 71 76 - // List of allowed origins 77 - const allowedOrigins = [ 78 - 'http://localhost:3000', // Standard React port 79 - 'http://127.0.0.1:3000', // Alternative React address 80 - ] 81 - 82 - // Check if the request origin is in our allowed list or is an ngrok domain 83 - if (allowedOrigins.indexOf(origin) !== -1) { 84 - callback(null, true) 85 - } else { 86 - console.warn(`โš ๏ธ CORS blocked origin: ${origin}`) 87 - callback(null, false) 88 - } 89 - }, 90 - credentials: true, 91 - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 92 - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], 93 - }), 94 - ) 95 - } else { 96 - // In production, CORS is not needed if frontend and API are on same domain 97 - // But we'll still enable it for flexibility with minimal configuration 98 - app.use( 99 - cors({ 100 - origin: true, // Use req.origin, which means same-origin requests will always be allowed 101 - credentials: true, 102 - }), 103 - ) 104 - } 105 - 106 - // Routes & middlewares 107 - const router = createRouter(ctx) 108 - app.use(express.json()) 109 - app.use(express.urlencoded({ extended: true })) 110 - 111 - app.use('/api', router) 72 + app.use(health.createRouter(ctx)) 73 + app.use(oauth.createRouter(ctx)) 74 + app.use(server.xrpc.router) 75 + app.use(error.createHandler(ctx)) 112 76 113 77 // Serve static files from the frontend build - prod only 114 78 if (env.isProduction) { ··· 123 87 124 88 // Serve static files 125 89 app.use(express.static(frontendPath)) 126 - 127 - // Heathcheck 128 - app.get('/health', (req, res) => { 129 - res.status(200).json({ status: 'ok' }) 130 - }) 131 90 132 91 // For any other requests, send the index.html file 133 92 app.get('*', (req, res) => { 134 93 // Only handle non-API paths 135 - if (!req.path.startsWith('/api/')) { 94 + if (!req.path.startsWith('/xrpc/')) { 136 95 res.sendFile(path.join(frontendPath, 'index.html')) 137 96 } else { 138 97 res.status(404).json({ error: 'API endpoint not found' }) ··· 144 103 res.sendStatus(404) 145 104 }) 146 105 } 106 + } else { 107 + app.set('trust proxy', true) 147 108 } 148 109 149 110 // Use the port from env (should be 3001 for the API server) 150 - const server = app.listen(env.PORT) 151 - await events.once(server, 'listening') 111 + const httpServer = app.listen(env.PORT) 112 + await events.once(httpServer, 'listening') 152 113 logger.info( 153 114 `API Server (${NODE_ENV}) running on port http://${HOST}:${env.PORT}`, 154 115 ) 155 116 156 - return new Server(app, server, ctx) 117 + return new Server(app, httpServer, ctx) 157 118 } 158 119 159 120 async close() { 160 121 this.ctx.logger.info('sigint received, shutting down') 161 122 await this.ctx.ingester.destroy() 162 - return new Promise<void>((resolve) => { 123 + await new Promise<void>((resolve) => { 163 124 this.server.close(() => { 164 125 this.ctx.logger.info('server closed') 165 126 resolve() ··· 174 135 const onCloseSignal = async () => { 175 136 setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s 176 137 await server.close() 177 - process.exit() 138 + process.exit(0) 178 139 } 179 140 180 141 process.on('SIGINT', onCloseSignal)
-90
packages/appview/src/ingester.ts
··· 1 - import { IdResolver } from '@atproto/identity' 2 - import { Firehose, MemoryRunner, type Event } from '@atproto/sync' 3 - import { XyzStatusphereStatus } from '@statusphere/lexicon' 4 - import pino from 'pino' 5 - 6 - import type { Database } from '#/db' 7 - 8 - export async function createIngester(db: Database, idResolver: IdResolver) { 9 - const logger = pino({ name: 'firehose ingestion' }) 10 - 11 - const cursor = await db 12 - .selectFrom('cursor') 13 - .where('id', '=', 1) 14 - .select('seq') 15 - .executeTakeFirst() 16 - 17 - logger.info(`start cursor: ${cursor?.seq}`) 18 - 19 - // For throttling cursor writes 20 - let lastCursorWrite = 0 21 - 22 - const runner = new MemoryRunner({ 23 - startCursor: cursor?.seq || undefined, 24 - setCursor: async (seq) => { 25 - const now = Date.now() 26 - 27 - if (now - lastCursorWrite >= 10000) { 28 - lastCursorWrite = now 29 - await db 30 - .updateTable('cursor') 31 - .set({ seq }) 32 - .where('id', '=', 1) 33 - .execute() 34 - } 35 - }, 36 - }) 37 - 38 - return new Firehose({ 39 - idResolver, 40 - runner, 41 - handleEvent: async (evt: Event) => { 42 - // Watch for write events 43 - if (evt.event === 'create' || evt.event === 'update') { 44 - const now = new Date() 45 - const record = evt.record 46 - 47 - // If the write is a valid status update 48 - if ( 49 - evt.collection === 'xyz.statusphere.status' && 50 - XyzStatusphereStatus.isRecord(record) 51 - ) { 52 - const validatedRecord = XyzStatusphereStatus.validateRecord(record) 53 - if (!validatedRecord.success) return 54 - // Store the status in our SQLite 55 - await db 56 - .insertInto('status') 57 - .values({ 58 - uri: evt.uri.toString(), 59 - authorDid: evt.did, 60 - status: validatedRecord.value.status, 61 - createdAt: validatedRecord.value.createdAt, 62 - indexedAt: now.toISOString(), 63 - }) 64 - .onConflict((oc) => 65 - oc.column('uri').doUpdateSet({ 66 - status: validatedRecord.value.status, 67 - indexedAt: now.toISOString(), 68 - }), 69 - ) 70 - .execute() 71 - } 72 - } else if ( 73 - evt.event === 'delete' && 74 - evt.collection === 'xyz.statusphere.status' 75 - ) { 76 - // Remove the status from our SQLite 77 - await db 78 - .deleteFrom('status') 79 - .where('uri', '=', evt.uri.toString()) 80 - .execute() 81 - } 82 - }, 83 - onError: (err: Error) => { 84 - logger.error({ err }, 'error on firehose ingestion') 85 - }, 86 - filterCollections: ['xyz.statusphere.status'], 87 - excludeIdentity: true, 88 - excludeAccount: true, 89 - }) 90 - }
+93
packages/appview/src/ingestors/firehose.ts
··· 1 + import { IdResolver } from '@atproto/identity' 2 + import { Firehose, MemoryRunner, type Event } from '@atproto/sync' 3 + import { XyzStatusphereStatus } from '@statusphere/lexicon' 4 + import pino from 'pino' 5 + 6 + import type { Database } from '#/db' 7 + 8 + export async function createFirehoseIngester( 9 + db: Database, 10 + idResolver: IdResolver, 11 + ) { 12 + const logger = pino({ name: 'firehose ingestion' }) 13 + 14 + const cursor = await db 15 + .selectFrom('cursor') 16 + .where('id', '=', 1) 17 + .select('seq') 18 + .executeTakeFirst() 19 + 20 + logger.info(`start cursor: ${cursor?.seq}`) 21 + 22 + // For throttling cursor writes 23 + let lastCursorWrite = 0 24 + 25 + const runner = new MemoryRunner({ 26 + startCursor: cursor?.seq || undefined, 27 + setCursor: async (seq) => { 28 + const now = Date.now() 29 + 30 + if (now - lastCursorWrite >= 10000) { 31 + lastCursorWrite = now 32 + await db 33 + .updateTable('cursor') 34 + .set({ seq }) 35 + .where('id', '=', 1) 36 + .execute() 37 + } 38 + }, 39 + }) 40 + 41 + return new Firehose({ 42 + idResolver, 43 + runner, 44 + handleEvent: async (evt: Event) => { 45 + // Watch for write events 46 + if (evt.event === 'create' || evt.event === 'update') { 47 + const now = new Date() 48 + const record = evt.record 49 + 50 + // If the write is a valid status update 51 + if ( 52 + evt.collection === 'xyz.statusphere.status' && 53 + XyzStatusphereStatus.isRecord(record) 54 + ) { 55 + const validatedRecord = XyzStatusphereStatus.validateRecord(record) 56 + if (!validatedRecord.success) return 57 + // Store the status in our SQLite 58 + await db 59 + .insertInto('status') 60 + .values({ 61 + uri: evt.uri.toString(), 62 + authorDid: evt.did, 63 + status: validatedRecord.value.status, 64 + createdAt: validatedRecord.value.createdAt, 65 + indexedAt: now.toISOString(), 66 + }) 67 + .onConflict((oc) => 68 + oc.column('uri').doUpdateSet({ 69 + status: validatedRecord.value.status, 70 + indexedAt: now.toISOString(), 71 + }), 72 + ) 73 + .execute() 74 + } 75 + } else if ( 76 + evt.event === 'delete' && 77 + evt.collection === 'xyz.statusphere.status' 78 + ) { 79 + // Remove the status from our SQLite 80 + await db 81 + .deleteFrom('status') 82 + .where('uri', '=', evt.uri.toString()) 83 + .execute() 84 + } 85 + }, 86 + onError: (err: Error) => { 87 + logger.error({ err }, 'error on firehose ingestion') 88 + }, 89 + filterCollections: ['xyz.statusphere.status'], 90 + excludeIdentity: true, 91 + excludeAccount: true, 92 + }) 93 + }
+2
packages/appview/src/ingestors/index.ts
··· 1 + export * from './jetstream' 2 + export * from './firehose'
+223
packages/appview/src/ingestors/jetstream.ts
··· 1 + import { XyzStatusphereStatus } from '@statusphere/lexicon' 2 + import pino from 'pino' 3 + import WebSocket from 'ws' 4 + 5 + import type { Database } from '#/db' 6 + import { env } from '#/lib/env' 7 + 8 + export async function createJetstreamIngester(db: Database) { 9 + const logger = pino({ name: 'jetstream ingestion' }) 10 + 11 + const cursor = await db 12 + .selectFrom('cursor') 13 + .where('id', '=', 2) 14 + .select('seq') 15 + .executeTakeFirst() 16 + 17 + logger.info(`start cursor: ${cursor?.seq}`) 18 + 19 + // For throttling cursor writes 20 + let lastCursorWrite = 0 21 + 22 + return new Jetstream<XyzStatusphereStatus.Record>({ 23 + instanceUrl: env.JETSTREAM_INSTANCE, 24 + logger, 25 + cursor: cursor?.seq || undefined, 26 + setCursor: async (seq) => { 27 + const now = Date.now() 28 + 29 + if (now - lastCursorWrite >= 30000) { 30 + lastCursorWrite = now 31 + logger.info(`writing cursor: ${seq}`) 32 + await db 33 + .updateTable('cursor') 34 + .set({ seq }) 35 + .where('id', '=', 2) 36 + .execute() 37 + } 38 + }, 39 + handleEvent: async (evt) => { 40 + // ignore account and identity events 41 + if ( 42 + evt.kind !== 'commit' || 43 + evt.commit.collection !== 'xyz.statusphere.status' 44 + ) 45 + return 46 + 47 + const now = new Date() 48 + const uri = `at://${evt.did}/${evt.commit.collection}/${evt.commit.rkey}` 49 + 50 + if ( 51 + (evt.commit.operation === 'create' || 52 + evt.commit.operation === 'update') && 53 + XyzStatusphereStatus.isRecord(evt.commit.record) 54 + ) { 55 + const validatedRecord = XyzStatusphereStatus.validateRecord( 56 + evt.commit.record, 57 + ) 58 + if (!validatedRecord.success) return 59 + 60 + await db 61 + .insertInto('status') 62 + .values({ 63 + uri, 64 + authorDid: evt.did, 65 + status: validatedRecord.value.status, 66 + createdAt: validatedRecord.value.createdAt, 67 + indexedAt: now.toISOString(), 68 + }) 69 + .onConflict((oc) => 70 + oc.column('uri').doUpdateSet({ 71 + status: validatedRecord.value.status, 72 + indexedAt: now.toISOString(), 73 + }), 74 + ) 75 + .execute() 76 + } else if (evt.commit.operation === 'delete') { 77 + await db.deleteFrom('status').where('uri', '=', uri).execute() 78 + } 79 + }, 80 + onError: (err) => { 81 + logger.error({ err }, 'error during jetstream ingestion') 82 + }, 83 + wantedCollections: ['xyz.statusphere.status'], 84 + }) 85 + } 86 + 87 + export class Jetstream<T> { 88 + private instanceUrl: string 89 + private logger: pino.Logger 90 + private handleEvent: (evt: JetstreamEvent<T>) => Promise<void> 91 + private onError: (err: unknown) => void 92 + private setCursor?: (seq: number) => Promise<void> 93 + private cursor?: number 94 + private ws?: WebSocket 95 + private isStarted = false 96 + private isDestroyed = false 97 + private wantedCollections: string[] 98 + 99 + constructor({ 100 + instanceUrl, 101 + logger, 102 + cursor, 103 + setCursor, 104 + handleEvent, 105 + onError, 106 + wantedCollections, 107 + }: { 108 + instanceUrl: string 109 + logger: pino.Logger 110 + cursor?: number 111 + setCursor?: (seq: number) => Promise<void> 112 + handleEvent: (evt: any) => Promise<void> 113 + onError: (err: any) => void 114 + wantedCollections: string[] 115 + }) { 116 + this.instanceUrl = instanceUrl 117 + this.logger = logger 118 + this.cursor = cursor 119 + this.setCursor = setCursor 120 + this.handleEvent = handleEvent 121 + this.onError = onError 122 + this.wantedCollections = wantedCollections 123 + } 124 + 125 + constructUrlWithQuery = (): string => { 126 + const params = new URLSearchParams() 127 + params.append('wantedCollections', this.wantedCollections.join(',')) 128 + if (this.cursor !== undefined) { 129 + params.append('cursor', this.cursor.toString()) 130 + } 131 + return `${this.instanceUrl}/subscribe?${params.toString()}` 132 + } 133 + 134 + start() { 135 + if (this.isStarted) return 136 + this.isStarted = true 137 + this.isDestroyed = false 138 + this.ws = new WebSocket(this.constructUrlWithQuery()) 139 + 140 + this.ws.on('open', () => { 141 + this.logger.info('Jetstream connection opened.') 142 + }) 143 + 144 + this.ws.on('message', async (data) => { 145 + try { 146 + const event: JetstreamEvent<T> = JSON.parse(data.toString()) 147 + 148 + // Update cursor if provided 149 + if (event.time_us !== undefined && this.setCursor) { 150 + await this.setCursor(event.time_us) 151 + } 152 + 153 + await this.handleEvent(event) 154 + } catch (err) { 155 + this.onError(err) 156 + } 157 + }) 158 + 159 + this.ws.on('error', (err) => { 160 + this.onError(err) 161 + }) 162 + 163 + this.ws.on('close', (code, reason) => { 164 + if (!this.isDestroyed) { 165 + this.logger.error(`Jetstream closed. Code: ${code}, Reason: ${reason}`) 166 + } 167 + this.isStarted = false 168 + }) 169 + } 170 + 171 + destroy() { 172 + if (this.ws) { 173 + this.isDestroyed = true 174 + this.ws.close() 175 + this.isStarted = false 176 + this.logger.info('jetstream destroyed gracefully') 177 + } 178 + } 179 + } 180 + 181 + type JetstreamEvent<T> = { 182 + did: string 183 + time_us: number 184 + } & (CommitEvent<T> | AccountEvent | IdentityEvent) 185 + 186 + type CommitEvent<T> = { 187 + kind: 'commit' 188 + commit: 189 + | { 190 + operation: 'create' | 'update' 191 + record: T 192 + rev: string 193 + collection: string 194 + rkey: string 195 + cid: string 196 + } 197 + | { 198 + operation: 'delete' 199 + rev: string 200 + collection: string 201 + rkey: string 202 + } 203 + } 204 + 205 + type IdentityEvent = { 206 + kind: 'identity' 207 + identity: { 208 + did: string 209 + handle: string 210 + seq: number 211 + time: string 212 + } 213 + } 214 + 215 + type AccountEvent = { 216 + kind: 'account' 217 + account: { 218 + active: boolean 219 + did: string 220 + seq: number 221 + time: string 222 + } 223 + }
+286
packages/appview/src/lexicons/index.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { 5 + AuthVerifier, 6 + createServer as createXrpcServer, 7 + StreamAuthVerifier, 8 + Options as XrpcOptions, 9 + Server as XrpcServer, 10 + } from '@atproto/xrpc-server' 11 + 12 + import { schemas } from './lexicons.js' 13 + import * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js' 14 + import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js' 15 + import * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js' 16 + import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo.js' 17 + import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord.js' 18 + import * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo.js' 19 + import * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs.js' 20 + import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords.js' 21 + import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord.js' 22 + import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js' 23 + import * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js' 24 + import * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js' 25 + import * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js' 26 + 27 + export function createServer(options?: XrpcOptions): Server { 28 + return new Server(options) 29 + } 30 + 31 + export class Server { 32 + xrpc: XrpcServer 33 + xyz: XyzNS 34 + com: ComNS 35 + app: AppNS 36 + 37 + constructor(options?: XrpcOptions) { 38 + this.xrpc = createXrpcServer(schemas, options) 39 + this.xyz = new XyzNS(this) 40 + this.com = new ComNS(this) 41 + this.app = new AppNS(this) 42 + } 43 + } 44 + 45 + export class XyzNS { 46 + _server: Server 47 + statusphere: XyzStatusphereNS 48 + 49 + constructor(server: Server) { 50 + this._server = server 51 + this.statusphere = new XyzStatusphereNS(server) 52 + } 53 + } 54 + 55 + export class XyzStatusphereNS { 56 + _server: Server 57 + 58 + constructor(server: Server) { 59 + this._server = server 60 + } 61 + 62 + getStatuses<AV extends AuthVerifier>( 63 + cfg: ConfigOf< 64 + AV, 65 + XyzStatusphereGetStatuses.Handler<ExtractAuth<AV>>, 66 + XyzStatusphereGetStatuses.HandlerReqCtx<ExtractAuth<AV>> 67 + >, 68 + ) { 69 + const nsid = 'xyz.statusphere.getStatuses' // @ts-ignore 70 + return this._server.xrpc.method(nsid, cfg) 71 + } 72 + 73 + getUser<AV extends AuthVerifier>( 74 + cfg: ConfigOf< 75 + AV, 76 + XyzStatusphereGetUser.Handler<ExtractAuth<AV>>, 77 + XyzStatusphereGetUser.HandlerReqCtx<ExtractAuth<AV>> 78 + >, 79 + ) { 80 + const nsid = 'xyz.statusphere.getUser' // @ts-ignore 81 + return this._server.xrpc.method(nsid, cfg) 82 + } 83 + 84 + sendStatus<AV extends AuthVerifier>( 85 + cfg: ConfigOf< 86 + AV, 87 + XyzStatusphereSendStatus.Handler<ExtractAuth<AV>>, 88 + XyzStatusphereSendStatus.HandlerReqCtx<ExtractAuth<AV>> 89 + >, 90 + ) { 91 + const nsid = 'xyz.statusphere.sendStatus' // @ts-ignore 92 + return this._server.xrpc.method(nsid, cfg) 93 + } 94 + } 95 + 96 + export class ComNS { 97 + _server: Server 98 + atproto: ComAtprotoNS 99 + 100 + constructor(server: Server) { 101 + this._server = server 102 + this.atproto = new ComAtprotoNS(server) 103 + } 104 + } 105 + 106 + export class ComAtprotoNS { 107 + _server: Server 108 + repo: ComAtprotoRepoNS 109 + 110 + constructor(server: Server) { 111 + this._server = server 112 + this.repo = new ComAtprotoRepoNS(server) 113 + } 114 + } 115 + 116 + export class ComAtprotoRepoNS { 117 + _server: Server 118 + 119 + constructor(server: Server) { 120 + this._server = server 121 + } 122 + 123 + applyWrites<AV extends AuthVerifier>( 124 + cfg: ConfigOf< 125 + AV, 126 + ComAtprotoRepoApplyWrites.Handler<ExtractAuth<AV>>, 127 + ComAtprotoRepoApplyWrites.HandlerReqCtx<ExtractAuth<AV>> 128 + >, 129 + ) { 130 + const nsid = 'com.atproto.repo.applyWrites' // @ts-ignore 131 + return this._server.xrpc.method(nsid, cfg) 132 + } 133 + 134 + createRecord<AV extends AuthVerifier>( 135 + cfg: ConfigOf< 136 + AV, 137 + ComAtprotoRepoCreateRecord.Handler<ExtractAuth<AV>>, 138 + ComAtprotoRepoCreateRecord.HandlerReqCtx<ExtractAuth<AV>> 139 + >, 140 + ) { 141 + const nsid = 'com.atproto.repo.createRecord' // @ts-ignore 142 + return this._server.xrpc.method(nsid, cfg) 143 + } 144 + 145 + deleteRecord<AV extends AuthVerifier>( 146 + cfg: ConfigOf< 147 + AV, 148 + ComAtprotoRepoDeleteRecord.Handler<ExtractAuth<AV>>, 149 + ComAtprotoRepoDeleteRecord.HandlerReqCtx<ExtractAuth<AV>> 150 + >, 151 + ) { 152 + const nsid = 'com.atproto.repo.deleteRecord' // @ts-ignore 153 + return this._server.xrpc.method(nsid, cfg) 154 + } 155 + 156 + describeRepo<AV extends AuthVerifier>( 157 + cfg: ConfigOf< 158 + AV, 159 + ComAtprotoRepoDescribeRepo.Handler<ExtractAuth<AV>>, 160 + ComAtprotoRepoDescribeRepo.HandlerReqCtx<ExtractAuth<AV>> 161 + >, 162 + ) { 163 + const nsid = 'com.atproto.repo.describeRepo' // @ts-ignore 164 + return this._server.xrpc.method(nsid, cfg) 165 + } 166 + 167 + getRecord<AV extends AuthVerifier>( 168 + cfg: ConfigOf< 169 + AV, 170 + ComAtprotoRepoGetRecord.Handler<ExtractAuth<AV>>, 171 + ComAtprotoRepoGetRecord.HandlerReqCtx<ExtractAuth<AV>> 172 + >, 173 + ) { 174 + const nsid = 'com.atproto.repo.getRecord' // @ts-ignore 175 + return this._server.xrpc.method(nsid, cfg) 176 + } 177 + 178 + importRepo<AV extends AuthVerifier>( 179 + cfg: ConfigOf< 180 + AV, 181 + ComAtprotoRepoImportRepo.Handler<ExtractAuth<AV>>, 182 + ComAtprotoRepoImportRepo.HandlerReqCtx<ExtractAuth<AV>> 183 + >, 184 + ) { 185 + const nsid = 'com.atproto.repo.importRepo' // @ts-ignore 186 + return this._server.xrpc.method(nsid, cfg) 187 + } 188 + 189 + listMissingBlobs<AV extends AuthVerifier>( 190 + cfg: ConfigOf< 191 + AV, 192 + ComAtprotoRepoListMissingBlobs.Handler<ExtractAuth<AV>>, 193 + ComAtprotoRepoListMissingBlobs.HandlerReqCtx<ExtractAuth<AV>> 194 + >, 195 + ) { 196 + const nsid = 'com.atproto.repo.listMissingBlobs' // @ts-ignore 197 + return this._server.xrpc.method(nsid, cfg) 198 + } 199 + 200 + listRecords<AV extends AuthVerifier>( 201 + cfg: ConfigOf< 202 + AV, 203 + ComAtprotoRepoListRecords.Handler<ExtractAuth<AV>>, 204 + ComAtprotoRepoListRecords.HandlerReqCtx<ExtractAuth<AV>> 205 + >, 206 + ) { 207 + const nsid = 'com.atproto.repo.listRecords' // @ts-ignore 208 + return this._server.xrpc.method(nsid, cfg) 209 + } 210 + 211 + putRecord<AV extends AuthVerifier>( 212 + cfg: ConfigOf< 213 + AV, 214 + ComAtprotoRepoPutRecord.Handler<ExtractAuth<AV>>, 215 + ComAtprotoRepoPutRecord.HandlerReqCtx<ExtractAuth<AV>> 216 + >, 217 + ) { 218 + const nsid = 'com.atproto.repo.putRecord' // @ts-ignore 219 + return this._server.xrpc.method(nsid, cfg) 220 + } 221 + 222 + uploadBlob<AV extends AuthVerifier>( 223 + cfg: ConfigOf< 224 + AV, 225 + ComAtprotoRepoUploadBlob.Handler<ExtractAuth<AV>>, 226 + ComAtprotoRepoUploadBlob.HandlerReqCtx<ExtractAuth<AV>> 227 + >, 228 + ) { 229 + const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore 230 + return this._server.xrpc.method(nsid, cfg) 231 + } 232 + } 233 + 234 + export class AppNS { 235 + _server: Server 236 + bsky: AppBskyNS 237 + 238 + constructor(server: Server) { 239 + this._server = server 240 + this.bsky = new AppBskyNS(server) 241 + } 242 + } 243 + 244 + export class AppBskyNS { 245 + _server: Server 246 + actor: AppBskyActorNS 247 + 248 + constructor(server: Server) { 249 + this._server = server 250 + this.actor = new AppBskyActorNS(server) 251 + } 252 + } 253 + 254 + export class AppBskyActorNS { 255 + _server: Server 256 + 257 + constructor(server: Server) { 258 + this._server = server 259 + } 260 + } 261 + 262 + type SharedRateLimitOpts<T> = { 263 + name: string 264 + calcKey?: (ctx: T) => string | null 265 + calcPoints?: (ctx: T) => number 266 + } 267 + type RouteRateLimitOpts<T> = { 268 + durationMs: number 269 + points: number 270 + calcKey?: (ctx: T) => string | null 271 + calcPoints?: (ctx: T) => number 272 + } 273 + type HandlerOpts = { blobLimit?: number } 274 + type HandlerRateLimitOpts<T> = SharedRateLimitOpts<T> | RouteRateLimitOpts<T> 275 + type ConfigOf<Auth, Handler, ReqCtx> = 276 + | Handler 277 + | { 278 + auth?: Auth 279 + opts?: HandlerOpts 280 + rateLimit?: HandlerRateLimitOpts<ReqCtx> | HandlerRateLimitOpts<ReqCtx>[] 281 + handler: Handler 282 + } 283 + type ExtractAuth<AV extends AuthVerifier | StreamAuthVerifier> = Extract< 284 + Awaited<ReturnType<AV>>, 285 + { credentials: unknown } 286 + >
+1297
packages/appview/src/lexicons/lexicons.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { 5 + LexiconDoc, 6 + Lexicons, 7 + ValidationError, 8 + ValidationResult, 9 + } from '@atproto/lexicon' 10 + 11 + import { $Typed, is$typed, maybe$typed } from './util.js' 12 + 13 + export const schemaDict = { 14 + XyzStatusphereDefs: { 15 + lexicon: 1, 16 + id: 'xyz.statusphere.defs', 17 + defs: { 18 + statusView: { 19 + type: 'object', 20 + required: ['uri', 'status', 'profile', 'createdAt'], 21 + properties: { 22 + uri: { 23 + type: 'string', 24 + format: 'at-uri', 25 + }, 26 + status: { 27 + type: 'string', 28 + minLength: 1, 29 + maxGraphemes: 1, 30 + maxLength: 32, 31 + }, 32 + createdAt: { 33 + type: 'string', 34 + format: 'datetime', 35 + }, 36 + profile: { 37 + type: 'ref', 38 + ref: 'lex:xyz.statusphere.defs#profileView', 39 + }, 40 + }, 41 + }, 42 + profileView: { 43 + type: 'object', 44 + required: ['did', 'handle'], 45 + properties: { 46 + did: { 47 + type: 'string', 48 + format: 'did', 49 + }, 50 + handle: { 51 + type: 'string', 52 + format: 'handle', 53 + }, 54 + }, 55 + }, 56 + }, 57 + }, 58 + XyzStatusphereGetStatuses: { 59 + lexicon: 1, 60 + id: 'xyz.statusphere.getStatuses', 61 + defs: { 62 + main: { 63 + type: 'query', 64 + description: 'Get a list of the most recent statuses on the network.', 65 + parameters: { 66 + type: 'params', 67 + properties: { 68 + limit: { 69 + type: 'integer', 70 + minimum: 1, 71 + maximum: 100, 72 + default: 50, 73 + }, 74 + }, 75 + }, 76 + output: { 77 + encoding: 'application/json', 78 + schema: { 79 + type: 'object', 80 + required: ['statuses'], 81 + properties: { 82 + statuses: { 83 + type: 'array', 84 + items: { 85 + type: 'ref', 86 + ref: 'lex:xyz.statusphere.defs#statusView', 87 + }, 88 + }, 89 + }, 90 + }, 91 + }, 92 + }, 93 + }, 94 + }, 95 + XyzStatusphereGetUser: { 96 + lexicon: 1, 97 + id: 'xyz.statusphere.getUser', 98 + defs: { 99 + main: { 100 + type: 'query', 101 + description: "Get the current user's profile and status.", 102 + parameters: { 103 + type: 'params', 104 + properties: {}, 105 + }, 106 + output: { 107 + encoding: 'application/json', 108 + schema: { 109 + type: 'object', 110 + required: ['profile'], 111 + properties: { 112 + profile: { 113 + type: 'ref', 114 + ref: 'lex:app.bsky.actor.defs#profileView', 115 + }, 116 + status: { 117 + type: 'ref', 118 + ref: 'lex:xyz.statusphere.defs#statusView', 119 + }, 120 + }, 121 + }, 122 + }, 123 + }, 124 + }, 125 + }, 126 + XyzStatusphereSendStatus: { 127 + lexicon: 1, 128 + id: 'xyz.statusphere.sendStatus', 129 + defs: { 130 + main: { 131 + type: 'procedure', 132 + description: 'Send a status into the ATmosphere.', 133 + input: { 134 + encoding: 'application/json', 135 + schema: { 136 + type: 'object', 137 + required: ['status'], 138 + properties: { 139 + status: { 140 + type: 'string', 141 + minLength: 1, 142 + maxGraphemes: 1, 143 + maxLength: 32, 144 + }, 145 + }, 146 + }, 147 + }, 148 + output: { 149 + encoding: 'application/json', 150 + schema: { 151 + type: 'object', 152 + required: ['status'], 153 + properties: { 154 + status: { 155 + type: 'ref', 156 + ref: 'lex:xyz.statusphere.defs#statusView', 157 + }, 158 + }, 159 + }, 160 + }, 161 + }, 162 + }, 163 + }, 164 + XyzStatusphereStatus: { 165 + lexicon: 1, 166 + id: 'xyz.statusphere.status', 167 + defs: { 168 + main: { 169 + type: 'record', 170 + key: 'tid', 171 + record: { 172 + type: 'object', 173 + required: ['status', 'createdAt'], 174 + properties: { 175 + status: { 176 + type: 'string', 177 + minLength: 1, 178 + maxGraphemes: 1, 179 + maxLength: 32, 180 + }, 181 + createdAt: { 182 + type: 'string', 183 + format: 'datetime', 184 + }, 185 + }, 186 + }, 187 + }, 188 + }, 189 + }, 190 + ComAtprotoLabelDefs: { 191 + lexicon: 1, 192 + id: 'com.atproto.label.defs', 193 + defs: { 194 + label: { 195 + type: 'object', 196 + description: 197 + 'Metadata tag on an atproto resource (eg, repo or record).', 198 + required: ['src', 'uri', 'val', 'cts'], 199 + properties: { 200 + ver: { 201 + type: 'integer', 202 + description: 'The AT Protocol version of the label object.', 203 + }, 204 + src: { 205 + type: 'string', 206 + format: 'did', 207 + description: 'DID of the actor who created this label.', 208 + }, 209 + uri: { 210 + type: 'string', 211 + format: 'uri', 212 + description: 213 + 'AT URI of the record, repository (account), or other resource that this label applies to.', 214 + }, 215 + cid: { 216 + type: 'string', 217 + format: 'cid', 218 + description: 219 + "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", 220 + }, 221 + val: { 222 + type: 'string', 223 + maxLength: 128, 224 + description: 225 + 'The short string name of the value or type of this label.', 226 + }, 227 + neg: { 228 + type: 'boolean', 229 + description: 230 + 'If true, this is a negation label, overwriting a previous label.', 231 + }, 232 + cts: { 233 + type: 'string', 234 + format: 'datetime', 235 + description: 'Timestamp when this label was created.', 236 + }, 237 + exp: { 238 + type: 'string', 239 + format: 'datetime', 240 + description: 241 + 'Timestamp at which this label expires (no longer applies).', 242 + }, 243 + sig: { 244 + type: 'bytes', 245 + description: 'Signature of dag-cbor encoded label.', 246 + }, 247 + }, 248 + }, 249 + selfLabels: { 250 + type: 'object', 251 + description: 252 + 'Metadata tags on an atproto record, published by the author within the record.', 253 + required: ['values'], 254 + properties: { 255 + values: { 256 + type: 'array', 257 + items: { 258 + type: 'ref', 259 + ref: 'lex:com.atproto.label.defs#selfLabel', 260 + }, 261 + maxLength: 10, 262 + }, 263 + }, 264 + }, 265 + selfLabel: { 266 + type: 'object', 267 + description: 268 + 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.', 269 + required: ['val'], 270 + properties: { 271 + val: { 272 + type: 'string', 273 + maxLength: 128, 274 + description: 275 + 'The short string name of the value or type of this label.', 276 + }, 277 + }, 278 + }, 279 + labelValueDefinition: { 280 + type: 'object', 281 + description: 282 + 'Declares a label value and its expected interpretations and behaviors.', 283 + required: ['identifier', 'severity', 'blurs', 'locales'], 284 + properties: { 285 + identifier: { 286 + type: 'string', 287 + description: 288 + "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 289 + maxLength: 100, 290 + maxGraphemes: 100, 291 + }, 292 + severity: { 293 + type: 'string', 294 + description: 295 + "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 296 + knownValues: ['inform', 'alert', 'none'], 297 + }, 298 + blurs: { 299 + type: 'string', 300 + description: 301 + "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.", 302 + knownValues: ['content', 'media', 'none'], 303 + }, 304 + defaultSetting: { 305 + type: 'string', 306 + description: 'The default setting for this label.', 307 + knownValues: ['ignore', 'warn', 'hide'], 308 + default: 'warn', 309 + }, 310 + adultOnly: { 311 + type: 'boolean', 312 + description: 313 + 'Does the user need to have adult content enabled in order to configure this label?', 314 + }, 315 + locales: { 316 + type: 'array', 317 + items: { 318 + type: 'ref', 319 + ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings', 320 + }, 321 + }, 322 + }, 323 + }, 324 + labelValueDefinitionStrings: { 325 + type: 'object', 326 + description: 327 + 'Strings which describe the label in the UI, localized into a specific language.', 328 + required: ['lang', 'name', 'description'], 329 + properties: { 330 + lang: { 331 + type: 'string', 332 + description: 333 + 'The code of the language these strings are written in.', 334 + format: 'language', 335 + }, 336 + name: { 337 + type: 'string', 338 + description: 'A short human-readable name for the label.', 339 + maxGraphemes: 64, 340 + maxLength: 640, 341 + }, 342 + description: { 343 + type: 'string', 344 + description: 345 + 'A longer description of what the label means and why it might be applied.', 346 + maxGraphemes: 10000, 347 + maxLength: 100000, 348 + }, 349 + }, 350 + }, 351 + labelValue: { 352 + type: 'string', 353 + knownValues: [ 354 + '!hide', 355 + '!no-promote', 356 + '!warn', 357 + '!no-unauthenticated', 358 + 'dmca-violation', 359 + 'doxxing', 360 + 'porn', 361 + 'sexual', 362 + 'nudity', 363 + 'nsfl', 364 + 'gore', 365 + ], 366 + }, 367 + }, 368 + }, 369 + ComAtprotoRepoApplyWrites: { 370 + lexicon: 1, 371 + id: 'com.atproto.repo.applyWrites', 372 + defs: { 373 + main: { 374 + type: 'procedure', 375 + description: 376 + 'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.', 377 + input: { 378 + encoding: 'application/json', 379 + schema: { 380 + type: 'object', 381 + required: ['repo', 'writes'], 382 + properties: { 383 + repo: { 384 + type: 'string', 385 + format: 'at-identifier', 386 + description: 387 + 'The handle or DID of the repo (aka, current account).', 388 + }, 389 + validate: { 390 + type: 'boolean', 391 + description: 392 + "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.", 393 + }, 394 + writes: { 395 + type: 'array', 396 + items: { 397 + type: 'union', 398 + refs: [ 399 + 'lex:com.atproto.repo.applyWrites#create', 400 + 'lex:com.atproto.repo.applyWrites#update', 401 + 'lex:com.atproto.repo.applyWrites#delete', 402 + ], 403 + closed: true, 404 + }, 405 + }, 406 + swapCommit: { 407 + type: 'string', 408 + description: 409 + 'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.', 410 + format: 'cid', 411 + }, 412 + }, 413 + }, 414 + }, 415 + output: { 416 + encoding: 'application/json', 417 + schema: { 418 + type: 'object', 419 + required: [], 420 + properties: { 421 + commit: { 422 + type: 'ref', 423 + ref: 'lex:com.atproto.repo.defs#commitMeta', 424 + }, 425 + results: { 426 + type: 'array', 427 + items: { 428 + type: 'union', 429 + refs: [ 430 + 'lex:com.atproto.repo.applyWrites#createResult', 431 + 'lex:com.atproto.repo.applyWrites#updateResult', 432 + 'lex:com.atproto.repo.applyWrites#deleteResult', 433 + ], 434 + closed: true, 435 + }, 436 + }, 437 + }, 438 + }, 439 + }, 440 + errors: [ 441 + { 442 + name: 'InvalidSwap', 443 + description: 444 + "Indicates that the 'swapCommit' parameter did not match current commit.", 445 + }, 446 + ], 447 + }, 448 + create: { 449 + type: 'object', 450 + description: 'Operation which creates a new record.', 451 + required: ['collection', 'value'], 452 + properties: { 453 + collection: { 454 + type: 'string', 455 + format: 'nsid', 456 + }, 457 + rkey: { 458 + type: 'string', 459 + maxLength: 512, 460 + format: 'record-key', 461 + description: 462 + 'NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.', 463 + }, 464 + value: { 465 + type: 'unknown', 466 + }, 467 + }, 468 + }, 469 + update: { 470 + type: 'object', 471 + description: 'Operation which updates an existing record.', 472 + required: ['collection', 'rkey', 'value'], 473 + properties: { 474 + collection: { 475 + type: 'string', 476 + format: 'nsid', 477 + }, 478 + rkey: { 479 + type: 'string', 480 + format: 'record-key', 481 + }, 482 + value: { 483 + type: 'unknown', 484 + }, 485 + }, 486 + }, 487 + delete: { 488 + type: 'object', 489 + description: 'Operation which deletes an existing record.', 490 + required: ['collection', 'rkey'], 491 + properties: { 492 + collection: { 493 + type: 'string', 494 + format: 'nsid', 495 + }, 496 + rkey: { 497 + type: 'string', 498 + format: 'record-key', 499 + }, 500 + }, 501 + }, 502 + createResult: { 503 + type: 'object', 504 + required: ['uri', 'cid'], 505 + properties: { 506 + uri: { 507 + type: 'string', 508 + format: 'at-uri', 509 + }, 510 + cid: { 511 + type: 'string', 512 + format: 'cid', 513 + }, 514 + validationStatus: { 515 + type: 'string', 516 + knownValues: ['valid', 'unknown'], 517 + }, 518 + }, 519 + }, 520 + updateResult: { 521 + type: 'object', 522 + required: ['uri', 'cid'], 523 + properties: { 524 + uri: { 525 + type: 'string', 526 + format: 'at-uri', 527 + }, 528 + cid: { 529 + type: 'string', 530 + format: 'cid', 531 + }, 532 + validationStatus: { 533 + type: 'string', 534 + knownValues: ['valid', 'unknown'], 535 + }, 536 + }, 537 + }, 538 + deleteResult: { 539 + type: 'object', 540 + required: [], 541 + properties: {}, 542 + }, 543 + }, 544 + }, 545 + ComAtprotoRepoCreateRecord: { 546 + lexicon: 1, 547 + id: 'com.atproto.repo.createRecord', 548 + defs: { 549 + main: { 550 + type: 'procedure', 551 + description: 552 + 'Create a single new repository record. Requires auth, implemented by PDS.', 553 + input: { 554 + encoding: 'application/json', 555 + schema: { 556 + type: 'object', 557 + required: ['repo', 'collection', 'record'], 558 + properties: { 559 + repo: { 560 + type: 'string', 561 + format: 'at-identifier', 562 + description: 563 + 'The handle or DID of the repo (aka, current account).', 564 + }, 565 + collection: { 566 + type: 'string', 567 + format: 'nsid', 568 + description: 'The NSID of the record collection.', 569 + }, 570 + rkey: { 571 + type: 'string', 572 + format: 'record-key', 573 + description: 'The Record Key.', 574 + maxLength: 512, 575 + }, 576 + validate: { 577 + type: 'boolean', 578 + description: 579 + "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.", 580 + }, 581 + record: { 582 + type: 'unknown', 583 + description: 'The record itself. Must contain a $type field.', 584 + }, 585 + swapCommit: { 586 + type: 'string', 587 + format: 'cid', 588 + description: 589 + 'Compare and swap with the previous commit by CID.', 590 + }, 591 + }, 592 + }, 593 + }, 594 + output: { 595 + encoding: 'application/json', 596 + schema: { 597 + type: 'object', 598 + required: ['uri', 'cid'], 599 + properties: { 600 + uri: { 601 + type: 'string', 602 + format: 'at-uri', 603 + }, 604 + cid: { 605 + type: 'string', 606 + format: 'cid', 607 + }, 608 + commit: { 609 + type: 'ref', 610 + ref: 'lex:com.atproto.repo.defs#commitMeta', 611 + }, 612 + validationStatus: { 613 + type: 'string', 614 + knownValues: ['valid', 'unknown'], 615 + }, 616 + }, 617 + }, 618 + }, 619 + errors: [ 620 + { 621 + name: 'InvalidSwap', 622 + description: 623 + "Indicates that 'swapCommit' didn't match current repo commit.", 624 + }, 625 + ], 626 + }, 627 + }, 628 + }, 629 + ComAtprotoRepoDefs: { 630 + lexicon: 1, 631 + id: 'com.atproto.repo.defs', 632 + defs: { 633 + commitMeta: { 634 + type: 'object', 635 + required: ['cid', 'rev'], 636 + properties: { 637 + cid: { 638 + type: 'string', 639 + format: 'cid', 640 + }, 641 + rev: { 642 + type: 'string', 643 + format: 'tid', 644 + }, 645 + }, 646 + }, 647 + }, 648 + }, 649 + ComAtprotoRepoDeleteRecord: { 650 + lexicon: 1, 651 + id: 'com.atproto.repo.deleteRecord', 652 + defs: { 653 + main: { 654 + type: 'procedure', 655 + description: 656 + "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", 657 + input: { 658 + encoding: 'application/json', 659 + schema: { 660 + type: 'object', 661 + required: ['repo', 'collection', 'rkey'], 662 + properties: { 663 + repo: { 664 + type: 'string', 665 + format: 'at-identifier', 666 + description: 667 + 'The handle or DID of the repo (aka, current account).', 668 + }, 669 + collection: { 670 + type: 'string', 671 + format: 'nsid', 672 + description: 'The NSID of the record collection.', 673 + }, 674 + rkey: { 675 + type: 'string', 676 + format: 'record-key', 677 + description: 'The Record Key.', 678 + }, 679 + swapRecord: { 680 + type: 'string', 681 + format: 'cid', 682 + description: 683 + 'Compare and swap with the previous record by CID.', 684 + }, 685 + swapCommit: { 686 + type: 'string', 687 + format: 'cid', 688 + description: 689 + 'Compare and swap with the previous commit by CID.', 690 + }, 691 + }, 692 + }, 693 + }, 694 + output: { 695 + encoding: 'application/json', 696 + schema: { 697 + type: 'object', 698 + properties: { 699 + commit: { 700 + type: 'ref', 701 + ref: 'lex:com.atproto.repo.defs#commitMeta', 702 + }, 703 + }, 704 + }, 705 + }, 706 + errors: [ 707 + { 708 + name: 'InvalidSwap', 709 + }, 710 + ], 711 + }, 712 + }, 713 + }, 714 + ComAtprotoRepoDescribeRepo: { 715 + lexicon: 1, 716 + id: 'com.atproto.repo.describeRepo', 717 + defs: { 718 + main: { 719 + type: 'query', 720 + description: 721 + 'Get information about an account and repository, including the list of collections. Does not require auth.', 722 + parameters: { 723 + type: 'params', 724 + required: ['repo'], 725 + properties: { 726 + repo: { 727 + type: 'string', 728 + format: 'at-identifier', 729 + description: 'The handle or DID of the repo.', 730 + }, 731 + }, 732 + }, 733 + output: { 734 + encoding: 'application/json', 735 + schema: { 736 + type: 'object', 737 + required: [ 738 + 'handle', 739 + 'did', 740 + 'didDoc', 741 + 'collections', 742 + 'handleIsCorrect', 743 + ], 744 + properties: { 745 + handle: { 746 + type: 'string', 747 + format: 'handle', 748 + }, 749 + did: { 750 + type: 'string', 751 + format: 'did', 752 + }, 753 + didDoc: { 754 + type: 'unknown', 755 + description: 'The complete DID document for this account.', 756 + }, 757 + collections: { 758 + type: 'array', 759 + description: 760 + 'List of all the collections (NSIDs) for which this repo contains at least one record.', 761 + items: { 762 + type: 'string', 763 + format: 'nsid', 764 + }, 765 + }, 766 + handleIsCorrect: { 767 + type: 'boolean', 768 + description: 769 + 'Indicates if handle is currently valid (resolves bi-directionally)', 770 + }, 771 + }, 772 + }, 773 + }, 774 + }, 775 + }, 776 + }, 777 + ComAtprotoRepoGetRecord: { 778 + lexicon: 1, 779 + id: 'com.atproto.repo.getRecord', 780 + defs: { 781 + main: { 782 + type: 'query', 783 + description: 784 + 'Get a single record from a repository. Does not require auth.', 785 + parameters: { 786 + type: 'params', 787 + required: ['repo', 'collection', 'rkey'], 788 + properties: { 789 + repo: { 790 + type: 'string', 791 + format: 'at-identifier', 792 + description: 'The handle or DID of the repo.', 793 + }, 794 + collection: { 795 + type: 'string', 796 + format: 'nsid', 797 + description: 'The NSID of the record collection.', 798 + }, 799 + rkey: { 800 + type: 'string', 801 + description: 'The Record Key.', 802 + format: 'record-key', 803 + }, 804 + cid: { 805 + type: 'string', 806 + format: 'cid', 807 + description: 808 + 'The CID of the version of the record. If not specified, then return the most recent version.', 809 + }, 810 + }, 811 + }, 812 + output: { 813 + encoding: 'application/json', 814 + schema: { 815 + type: 'object', 816 + required: ['uri', 'value'], 817 + properties: { 818 + uri: { 819 + type: 'string', 820 + format: 'at-uri', 821 + }, 822 + cid: { 823 + type: 'string', 824 + format: 'cid', 825 + }, 826 + value: { 827 + type: 'unknown', 828 + }, 829 + }, 830 + }, 831 + }, 832 + errors: [ 833 + { 834 + name: 'RecordNotFound', 835 + }, 836 + ], 837 + }, 838 + }, 839 + }, 840 + ComAtprotoRepoImportRepo: { 841 + lexicon: 1, 842 + id: 'com.atproto.repo.importRepo', 843 + defs: { 844 + main: { 845 + type: 'procedure', 846 + description: 847 + 'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.', 848 + input: { 849 + encoding: 'application/vnd.ipld.car', 850 + }, 851 + }, 852 + }, 853 + }, 854 + ComAtprotoRepoListMissingBlobs: { 855 + lexicon: 1, 856 + id: 'com.atproto.repo.listMissingBlobs', 857 + defs: { 858 + main: { 859 + type: 'query', 860 + description: 861 + 'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.', 862 + parameters: { 863 + type: 'params', 864 + properties: { 865 + limit: { 866 + type: 'integer', 867 + minimum: 1, 868 + maximum: 1000, 869 + default: 500, 870 + }, 871 + cursor: { 872 + type: 'string', 873 + }, 874 + }, 875 + }, 876 + output: { 877 + encoding: 'application/json', 878 + schema: { 879 + type: 'object', 880 + required: ['blobs'], 881 + properties: { 882 + cursor: { 883 + type: 'string', 884 + }, 885 + blobs: { 886 + type: 'array', 887 + items: { 888 + type: 'ref', 889 + ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob', 890 + }, 891 + }, 892 + }, 893 + }, 894 + }, 895 + }, 896 + recordBlob: { 897 + type: 'object', 898 + required: ['cid', 'recordUri'], 899 + properties: { 900 + cid: { 901 + type: 'string', 902 + format: 'cid', 903 + }, 904 + recordUri: { 905 + type: 'string', 906 + format: 'at-uri', 907 + }, 908 + }, 909 + }, 910 + }, 911 + }, 912 + ComAtprotoRepoListRecords: { 913 + lexicon: 1, 914 + id: 'com.atproto.repo.listRecords', 915 + defs: { 916 + main: { 917 + type: 'query', 918 + description: 919 + 'List a range of records in a repository, matching a specific collection. Does not require auth.', 920 + parameters: { 921 + type: 'params', 922 + required: ['repo', 'collection'], 923 + properties: { 924 + repo: { 925 + type: 'string', 926 + format: 'at-identifier', 927 + description: 'The handle or DID of the repo.', 928 + }, 929 + collection: { 930 + type: 'string', 931 + format: 'nsid', 932 + description: 'The NSID of the record type.', 933 + }, 934 + limit: { 935 + type: 'integer', 936 + minimum: 1, 937 + maximum: 100, 938 + default: 50, 939 + description: 'The number of records to return.', 940 + }, 941 + cursor: { 942 + type: 'string', 943 + }, 944 + rkeyStart: { 945 + type: 'string', 946 + description: 947 + 'DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)', 948 + }, 949 + rkeyEnd: { 950 + type: 'string', 951 + description: 952 + 'DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)', 953 + }, 954 + reverse: { 955 + type: 'boolean', 956 + description: 'Flag to reverse the order of the returned records.', 957 + }, 958 + }, 959 + }, 960 + output: { 961 + encoding: 'application/json', 962 + schema: { 963 + type: 'object', 964 + required: ['records'], 965 + properties: { 966 + cursor: { 967 + type: 'string', 968 + }, 969 + records: { 970 + type: 'array', 971 + items: { 972 + type: 'ref', 973 + ref: 'lex:com.atproto.repo.listRecords#record', 974 + }, 975 + }, 976 + }, 977 + }, 978 + }, 979 + }, 980 + record: { 981 + type: 'object', 982 + required: ['uri', 'cid', 'value'], 983 + properties: { 984 + uri: { 985 + type: 'string', 986 + format: 'at-uri', 987 + }, 988 + cid: { 989 + type: 'string', 990 + format: 'cid', 991 + }, 992 + value: { 993 + type: 'unknown', 994 + }, 995 + }, 996 + }, 997 + }, 998 + }, 999 + ComAtprotoRepoPutRecord: { 1000 + lexicon: 1, 1001 + id: 'com.atproto.repo.putRecord', 1002 + defs: { 1003 + main: { 1004 + type: 'procedure', 1005 + description: 1006 + 'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.', 1007 + input: { 1008 + encoding: 'application/json', 1009 + schema: { 1010 + type: 'object', 1011 + required: ['repo', 'collection', 'rkey', 'record'], 1012 + nullable: ['swapRecord'], 1013 + properties: { 1014 + repo: { 1015 + type: 'string', 1016 + format: 'at-identifier', 1017 + description: 1018 + 'The handle or DID of the repo (aka, current account).', 1019 + }, 1020 + collection: { 1021 + type: 'string', 1022 + format: 'nsid', 1023 + description: 'The NSID of the record collection.', 1024 + }, 1025 + rkey: { 1026 + type: 'string', 1027 + format: 'record-key', 1028 + description: 'The Record Key.', 1029 + maxLength: 512, 1030 + }, 1031 + validate: { 1032 + type: 'boolean', 1033 + description: 1034 + "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.", 1035 + }, 1036 + record: { 1037 + type: 'unknown', 1038 + description: 'The record to write.', 1039 + }, 1040 + swapRecord: { 1041 + type: 'string', 1042 + format: 'cid', 1043 + description: 1044 + 'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation', 1045 + }, 1046 + swapCommit: { 1047 + type: 'string', 1048 + format: 'cid', 1049 + description: 1050 + 'Compare and swap with the previous commit by CID.', 1051 + }, 1052 + }, 1053 + }, 1054 + }, 1055 + output: { 1056 + encoding: 'application/json', 1057 + schema: { 1058 + type: 'object', 1059 + required: ['uri', 'cid'], 1060 + properties: { 1061 + uri: { 1062 + type: 'string', 1063 + format: 'at-uri', 1064 + }, 1065 + cid: { 1066 + type: 'string', 1067 + format: 'cid', 1068 + }, 1069 + commit: { 1070 + type: 'ref', 1071 + ref: 'lex:com.atproto.repo.defs#commitMeta', 1072 + }, 1073 + validationStatus: { 1074 + type: 'string', 1075 + knownValues: ['valid', 'unknown'], 1076 + }, 1077 + }, 1078 + }, 1079 + }, 1080 + errors: [ 1081 + { 1082 + name: 'InvalidSwap', 1083 + }, 1084 + ], 1085 + }, 1086 + }, 1087 + }, 1088 + ComAtprotoRepoStrongRef: { 1089 + lexicon: 1, 1090 + id: 'com.atproto.repo.strongRef', 1091 + description: 'A URI with a content-hash fingerprint.', 1092 + defs: { 1093 + main: { 1094 + type: 'object', 1095 + required: ['uri', 'cid'], 1096 + properties: { 1097 + uri: { 1098 + type: 'string', 1099 + format: 'at-uri', 1100 + }, 1101 + cid: { 1102 + type: 'string', 1103 + format: 'cid', 1104 + }, 1105 + }, 1106 + }, 1107 + }, 1108 + }, 1109 + ComAtprotoRepoUploadBlob: { 1110 + lexicon: 1, 1111 + id: 'com.atproto.repo.uploadBlob', 1112 + defs: { 1113 + main: { 1114 + type: 'procedure', 1115 + description: 1116 + '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.', 1117 + input: { 1118 + encoding: '*/*', 1119 + }, 1120 + output: { 1121 + encoding: 'application/json', 1122 + schema: { 1123 + type: 'object', 1124 + required: ['blob'], 1125 + properties: { 1126 + blob: { 1127 + type: 'blob', 1128 + }, 1129 + }, 1130 + }, 1131 + }, 1132 + }, 1133 + }, 1134 + }, 1135 + AppBskyActorDefs: { 1136 + lexicon: 1, 1137 + id: 'app.bsky.actor.defs', 1138 + defs: { 1139 + profileView: { 1140 + type: 'object', 1141 + required: ['did', 'handle'], 1142 + properties: { 1143 + did: { 1144 + type: 'string', 1145 + format: 'did', 1146 + }, 1147 + handle: { 1148 + type: 'string', 1149 + format: 'handle', 1150 + }, 1151 + displayName: { 1152 + type: 'string', 1153 + maxGraphemes: 64, 1154 + maxLength: 640, 1155 + }, 1156 + description: { 1157 + type: 'string', 1158 + maxGraphemes: 256, 1159 + maxLength: 2560, 1160 + }, 1161 + avatar: { 1162 + type: 'string', 1163 + format: 'uri', 1164 + }, 1165 + indexedAt: { 1166 + type: 'string', 1167 + format: 'datetime', 1168 + }, 1169 + createdAt: { 1170 + type: 'string', 1171 + format: 'datetime', 1172 + }, 1173 + labels: { 1174 + type: 'array', 1175 + items: { 1176 + type: 'ref', 1177 + ref: 'lex:com.atproto.label.defs#label', 1178 + }, 1179 + }, 1180 + }, 1181 + }, 1182 + }, 1183 + }, 1184 + AppBskyActorProfile: { 1185 + lexicon: 1, 1186 + id: 'app.bsky.actor.profile', 1187 + defs: { 1188 + main: { 1189 + type: 'record', 1190 + description: 'A declaration of a Bluesky account profile.', 1191 + key: 'literal:self', 1192 + record: { 1193 + type: 'object', 1194 + properties: { 1195 + displayName: { 1196 + type: 'string', 1197 + maxGraphemes: 64, 1198 + maxLength: 640, 1199 + }, 1200 + description: { 1201 + type: 'string', 1202 + description: 'Free-form profile description text.', 1203 + maxGraphemes: 256, 1204 + maxLength: 2560, 1205 + }, 1206 + avatar: { 1207 + type: 'blob', 1208 + description: 1209 + "Small image to be displayed next to posts from account. AKA, 'profile picture'", 1210 + accept: ['image/png', 'image/jpeg'], 1211 + maxSize: 1000000, 1212 + }, 1213 + banner: { 1214 + type: 'blob', 1215 + description: 1216 + 'Larger horizontal image to display behind profile view.', 1217 + accept: ['image/png', 'image/jpeg'], 1218 + maxSize: 1000000, 1219 + }, 1220 + labels: { 1221 + type: 'union', 1222 + description: 1223 + 'Self-label values, specific to the Bluesky application, on the overall account.', 1224 + refs: ['lex:com.atproto.label.defs#selfLabels'], 1225 + }, 1226 + joinedViaStarterPack: { 1227 + type: 'ref', 1228 + ref: 'lex:com.atproto.repo.strongRef', 1229 + }, 1230 + pinnedPost: { 1231 + type: 'ref', 1232 + ref: 'lex:com.atproto.repo.strongRef', 1233 + }, 1234 + createdAt: { 1235 + type: 'string', 1236 + format: 'datetime', 1237 + }, 1238 + }, 1239 + }, 1240 + }, 1241 + }, 1242 + }, 1243 + } as const satisfies Record<string, LexiconDoc> 1244 + 1245 + export const schemas = Object.values(schemaDict) satisfies LexiconDoc[] 1246 + export const lexicons: Lexicons = new Lexicons(schemas) 1247 + 1248 + export function validate<T extends { $type: string }>( 1249 + v: unknown, 1250 + id: string, 1251 + hash: string, 1252 + requiredType: true, 1253 + ): ValidationResult<T> 1254 + export function validate<T extends { $type?: string }>( 1255 + v: unknown, 1256 + id: string, 1257 + hash: string, 1258 + requiredType?: false, 1259 + ): ValidationResult<T> 1260 + export function validate( 1261 + v: unknown, 1262 + id: string, 1263 + hash: string, 1264 + requiredType?: boolean, 1265 + ): ValidationResult { 1266 + return (requiredType ? is$typed : maybe$typed)(v, id, hash) 1267 + ? lexicons.validate(`${id}#${hash}`, v) 1268 + : { 1269 + success: false, 1270 + error: new ValidationError( 1271 + `Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`, 1272 + ), 1273 + } 1274 + } 1275 + 1276 + export const ids = { 1277 + XyzStatusphereDefs: 'xyz.statusphere.defs', 1278 + XyzStatusphereGetStatuses: 'xyz.statusphere.getStatuses', 1279 + XyzStatusphereGetUser: 'xyz.statusphere.getUser', 1280 + XyzStatusphereSendStatus: 'xyz.statusphere.sendStatus', 1281 + XyzStatusphereStatus: 'xyz.statusphere.status', 1282 + ComAtprotoLabelDefs: 'com.atproto.label.defs', 1283 + ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites', 1284 + ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord', 1285 + ComAtprotoRepoDefs: 'com.atproto.repo.defs', 1286 + ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', 1287 + ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', 1288 + ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', 1289 + ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo', 1290 + ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs', 1291 + ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', 1292 + ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', 1293 + ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', 1294 + ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', 1295 + AppBskyActorDefs: 'app.bsky.actor.defs', 1296 + AppBskyActorProfile: 'app.bsky.actor.profile', 1297 + } as const
+35
packages/appview/src/lexicons/types/app/bsky/actor/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + 7 + import { validate as _validate } from '../../../../lexicons' 8 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 9 + import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js' 10 + 11 + const is$typed = _is$typed, 12 + validate = _validate 13 + const id = 'app.bsky.actor.defs' 14 + 15 + export interface ProfileView { 16 + $type?: 'app.bsky.actor.defs#profileView' 17 + did: string 18 + handle: string 19 + displayName?: string 20 + description?: string 21 + avatar?: string 22 + indexedAt?: string 23 + createdAt?: string 24 + labels?: ComAtprotoLabelDefs.Label[] 25 + } 26 + 27 + const hashProfileView = 'profileView' 28 + 29 + export function isProfileView<V>(v: V) { 30 + return is$typed(v, id, hashProfileView) 31 + } 32 + 33 + export function validateProfileView<V>(v: V) { 34 + return validate<ProfileView & V>(v, id, hashProfileView) 35 + }
+40
packages/appview/src/lexicons/types/app/bsky/actor/profile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + 7 + import { validate as _validate } from '../../../../lexicons' 8 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 9 + import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js' 10 + import type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'app.bsky.actor.profile' 15 + 16 + export interface Record { 17 + $type: 'app.bsky.actor.profile' 18 + displayName?: string 19 + /** Free-form profile description text. */ 20 + description?: string 21 + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 22 + avatar?: BlobRef 23 + /** Larger horizontal image to display behind profile view. */ 24 + banner?: BlobRef 25 + labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string } 26 + joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main 27 + pinnedPost?: ComAtprotoRepoStrongRef.Main 28 + createdAt?: string 29 + [k: string]: unknown 30 + } 31 + 32 + const hashRecord = 'main' 33 + 34 + export function isRecord<V>(v: V) { 35 + return is$typed(v, id, hashRecord) 36 + } 37 + 38 + export function validateRecord<V>(v: V) { 39 + return validate<Record & V>(v, id, hashRecord, true) 40 + }
+143
packages/appview/src/lexicons/types/com/atproto/label/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + 7 + import { validate as _validate } from '../../../../lexicons' 8 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'com.atproto.label.defs' 13 + 14 + /** Metadata tag on an atproto resource (eg, repo or record). */ 15 + export interface Label { 16 + $type?: 'com.atproto.label.defs#label' 17 + /** The AT Protocol version of the label object. */ 18 + ver?: number 19 + /** DID of the actor who created this label. */ 20 + src: string 21 + /** AT URI of the record, repository (account), or other resource that this label applies to. */ 22 + uri: string 23 + /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */ 24 + cid?: string 25 + /** The short string name of the value or type of this label. */ 26 + val: string 27 + /** If true, this is a negation label, overwriting a previous label. */ 28 + neg?: boolean 29 + /** Timestamp when this label was created. */ 30 + cts: string 31 + /** Timestamp at which this label expires (no longer applies). */ 32 + exp?: string 33 + /** Signature of dag-cbor encoded label. */ 34 + sig?: Uint8Array 35 + } 36 + 37 + const hashLabel = 'label' 38 + 39 + export function isLabel<V>(v: V) { 40 + return is$typed(v, id, hashLabel) 41 + } 42 + 43 + export function validateLabel<V>(v: V) { 44 + return validate<Label & V>(v, id, hashLabel) 45 + } 46 + 47 + /** Metadata tags on an atproto record, published by the author within the record. */ 48 + export interface SelfLabels { 49 + $type?: 'com.atproto.label.defs#selfLabels' 50 + values: SelfLabel[] 51 + } 52 + 53 + const hashSelfLabels = 'selfLabels' 54 + 55 + export function isSelfLabels<V>(v: V) { 56 + return is$typed(v, id, hashSelfLabels) 57 + } 58 + 59 + export function validateSelfLabels<V>(v: V) { 60 + return validate<SelfLabels & V>(v, id, hashSelfLabels) 61 + } 62 + 63 + /** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */ 64 + export interface SelfLabel { 65 + $type?: 'com.atproto.label.defs#selfLabel' 66 + /** The short string name of the value or type of this label. */ 67 + val: string 68 + } 69 + 70 + const hashSelfLabel = 'selfLabel' 71 + 72 + export function isSelfLabel<V>(v: V) { 73 + return is$typed(v, id, hashSelfLabel) 74 + } 75 + 76 + export function validateSelfLabel<V>(v: V) { 77 + return validate<SelfLabel & V>(v, id, hashSelfLabel) 78 + } 79 + 80 + /** Declares a label value and its expected interpretations and behaviors. */ 81 + export interface LabelValueDefinition { 82 + $type?: 'com.atproto.label.defs#labelValueDefinition' 83 + /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */ 84 + identifier: string 85 + /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */ 86 + severity: 'inform' | 'alert' | 'none' | (string & {}) 87 + /** 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. */ 88 + blurs: 'content' | 'media' | 'none' | (string & {}) 89 + /** The default setting for this label. */ 90 + defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {}) 91 + /** Does the user need to have adult content enabled in order to configure this label? */ 92 + adultOnly?: boolean 93 + locales: LabelValueDefinitionStrings[] 94 + } 95 + 96 + const hashLabelValueDefinition = 'labelValueDefinition' 97 + 98 + export function isLabelValueDefinition<V>(v: V) { 99 + return is$typed(v, id, hashLabelValueDefinition) 100 + } 101 + 102 + export function validateLabelValueDefinition<V>(v: V) { 103 + return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition) 104 + } 105 + 106 + /** Strings which describe the label in the UI, localized into a specific language. */ 107 + export interface LabelValueDefinitionStrings { 108 + $type?: 'com.atproto.label.defs#labelValueDefinitionStrings' 109 + /** The code of the language these strings are written in. */ 110 + lang: string 111 + /** A short human-readable name for the label. */ 112 + name: string 113 + /** A longer description of what the label means and why it might be applied. */ 114 + description: string 115 + } 116 + 117 + const hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings' 118 + 119 + export function isLabelValueDefinitionStrings<V>(v: V) { 120 + return is$typed(v, id, hashLabelValueDefinitionStrings) 121 + } 122 + 123 + export function validateLabelValueDefinitionStrings<V>(v: V) { 124 + return validate<LabelValueDefinitionStrings & V>( 125 + v, 126 + id, 127 + hashLabelValueDefinitionStrings, 128 + ) 129 + } 130 + 131 + export type LabelValue = 132 + | '!hide' 133 + | '!no-promote' 134 + | '!warn' 135 + | '!no-unauthenticated' 136 + | 'dmca-violation' 137 + | 'doxxing' 138 + | 'porn' 139 + | 'sexual' 140 + | 'nudity' 141 + | 'nsfl' 142 + | 'gore' 143 + | (string & {})
+168
packages/appview/src/lexicons/types/com/atproto/repo/applyWrites.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + import type * as ComAtprotoRepoDefs from './defs.js' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'com.atproto.repo.applyWrites' 16 + 17 + export interface QueryParams {} 18 + 19 + export interface InputSchema { 20 + /** The handle or DID of the repo (aka, current account). */ 21 + repo: string 22 + /** Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. */ 23 + validate?: boolean 24 + writes: ($Typed<Create> | $Typed<Update> | $Typed<Delete>)[] 25 + /** If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. */ 26 + swapCommit?: string 27 + } 28 + 29 + export interface OutputSchema { 30 + commit?: ComAtprotoRepoDefs.CommitMeta 31 + results?: ( 32 + | $Typed<CreateResult> 33 + | $Typed<UpdateResult> 34 + | $Typed<DeleteResult> 35 + )[] 36 + } 37 + 38 + export interface HandlerInput { 39 + encoding: 'application/json' 40 + body: InputSchema 41 + } 42 + 43 + export interface HandlerSuccess { 44 + encoding: 'application/json' 45 + body: OutputSchema 46 + headers?: { [key: string]: string } 47 + } 48 + 49 + export interface HandlerError { 50 + status: number 51 + message?: string 52 + error?: 'InvalidSwap' 53 + } 54 + 55 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 56 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 57 + auth: HA 58 + params: QueryParams 59 + input: HandlerInput 60 + req: express.Request 61 + res: express.Response 62 + resetRouteRateLimits: () => Promise<void> 63 + } 64 + export type Handler<HA extends HandlerAuth = never> = ( 65 + ctx: HandlerReqCtx<HA>, 66 + ) => Promise<HandlerOutput> | HandlerOutput 67 + 68 + /** Operation which creates a new record. */ 69 + export interface Create { 70 + $type?: 'com.atproto.repo.applyWrites#create' 71 + collection: string 72 + /** NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility. */ 73 + rkey?: string 74 + value: { [_ in string]: unknown } 75 + } 76 + 77 + const hashCreate = 'create' 78 + 79 + export function isCreate<V>(v: V) { 80 + return is$typed(v, id, hashCreate) 81 + } 82 + 83 + export function validateCreate<V>(v: V) { 84 + return validate<Create & V>(v, id, hashCreate) 85 + } 86 + 87 + /** Operation which updates an existing record. */ 88 + export interface Update { 89 + $type?: 'com.atproto.repo.applyWrites#update' 90 + collection: string 91 + rkey: string 92 + value: { [_ in string]: unknown } 93 + } 94 + 95 + const hashUpdate = 'update' 96 + 97 + export function isUpdate<V>(v: V) { 98 + return is$typed(v, id, hashUpdate) 99 + } 100 + 101 + export function validateUpdate<V>(v: V) { 102 + return validate<Update & V>(v, id, hashUpdate) 103 + } 104 + 105 + /** Operation which deletes an existing record. */ 106 + export interface Delete { 107 + $type?: 'com.atproto.repo.applyWrites#delete' 108 + collection: string 109 + rkey: string 110 + } 111 + 112 + const hashDelete = 'delete' 113 + 114 + export function isDelete<V>(v: V) { 115 + return is$typed(v, id, hashDelete) 116 + } 117 + 118 + export function validateDelete<V>(v: V) { 119 + return validate<Delete & V>(v, id, hashDelete) 120 + } 121 + 122 + export interface CreateResult { 123 + $type?: 'com.atproto.repo.applyWrites#createResult' 124 + uri: string 125 + cid: string 126 + validationStatus?: 'valid' | 'unknown' | (string & {}) 127 + } 128 + 129 + const hashCreateResult = 'createResult' 130 + 131 + export function isCreateResult<V>(v: V) { 132 + return is$typed(v, id, hashCreateResult) 133 + } 134 + 135 + export function validateCreateResult<V>(v: V) { 136 + return validate<CreateResult & V>(v, id, hashCreateResult) 137 + } 138 + 139 + export interface UpdateResult { 140 + $type?: 'com.atproto.repo.applyWrites#updateResult' 141 + uri: string 142 + cid: string 143 + validationStatus?: 'valid' | 'unknown' | (string & {}) 144 + } 145 + 146 + const hashUpdateResult = 'updateResult' 147 + 148 + export function isUpdateResult<V>(v: V) { 149 + return is$typed(v, id, hashUpdateResult) 150 + } 151 + 152 + export function validateUpdateResult<V>(v: V) { 153 + return validate<UpdateResult & V>(v, id, hashUpdateResult) 154 + } 155 + 156 + export interface DeleteResult { 157 + $type?: 'com.atproto.repo.applyWrites#deleteResult' 158 + } 159 + 160 + const hashDeleteResult = 'deleteResult' 161 + 162 + export function isDeleteResult<V>(v: V) { 163 + return is$typed(v, id, hashDeleteResult) 164 + } 165 + 166 + export function validateDeleteResult<V>(v: V) { 167 + return validate<DeleteResult & V>(v, id, hashDeleteResult) 168 + }
+69
packages/appview/src/lexicons/types/com/atproto/repo/createRecord.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + import type * as ComAtprotoRepoDefs from './defs.js' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'com.atproto.repo.createRecord' 16 + 17 + export interface QueryParams {} 18 + 19 + export interface InputSchema { 20 + /** The handle or DID of the repo (aka, current account). */ 21 + repo: string 22 + /** The NSID of the record collection. */ 23 + collection: string 24 + /** The Record Key. */ 25 + rkey?: string 26 + /** 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. */ 27 + validate?: boolean 28 + /** The record itself. Must contain a $type field. */ 29 + record: { [_ in string]: unknown } 30 + /** Compare and swap with the previous commit by CID. */ 31 + swapCommit?: string 32 + } 33 + 34 + export interface OutputSchema { 35 + uri: string 36 + cid: string 37 + commit?: ComAtprotoRepoDefs.CommitMeta 38 + validationStatus?: 'valid' | 'unknown' | (string & {}) 39 + } 40 + 41 + export interface HandlerInput { 42 + encoding: 'application/json' 43 + body: InputSchema 44 + } 45 + 46 + export interface HandlerSuccess { 47 + encoding: 'application/json' 48 + body: OutputSchema 49 + headers?: { [key: string]: string } 50 + } 51 + 52 + export interface HandlerError { 53 + status: number 54 + message?: string 55 + error?: 'InvalidSwap' 56 + } 57 + 58 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 59 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 60 + auth: HA 61 + params: QueryParams 62 + input: HandlerInput 63 + req: express.Request 64 + res: express.Response 65 + resetRouteRateLimits: () => Promise<void> 66 + } 67 + export type Handler<HA extends HandlerAuth = never> = ( 68 + ctx: HandlerReqCtx<HA>, 69 + ) => Promise<HandlerOutput> | HandlerOutput
+28
packages/appview/src/lexicons/types/com/atproto/repo/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + 7 + import { validate as _validate } from '../../../../lexicons' 8 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'com.atproto.repo.defs' 13 + 14 + export interface CommitMeta { 15 + $type?: 'com.atproto.repo.defs#commitMeta' 16 + cid: string 17 + rev: string 18 + } 19 + 20 + const hashCommitMeta = 'commitMeta' 21 + 22 + export function isCommitMeta<V>(v: V) { 23 + return is$typed(v, id, hashCommitMeta) 24 + } 25 + 26 + export function validateCommitMeta<V>(v: V) { 27 + return validate<CommitMeta & V>(v, id, hashCommitMeta) 28 + }
+64
packages/appview/src/lexicons/types/com/atproto/repo/deleteRecord.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + import type * as ComAtprotoRepoDefs from './defs.js' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'com.atproto.repo.deleteRecord' 16 + 17 + export interface QueryParams {} 18 + 19 + export interface InputSchema { 20 + /** The handle or DID of the repo (aka, current account). */ 21 + repo: string 22 + /** The NSID of the record collection. */ 23 + collection: string 24 + /** The Record Key. */ 25 + rkey: string 26 + /** Compare and swap with the previous record by CID. */ 27 + swapRecord?: string 28 + /** Compare and swap with the previous commit by CID. */ 29 + swapCommit?: string 30 + } 31 + 32 + export interface OutputSchema { 33 + commit?: ComAtprotoRepoDefs.CommitMeta 34 + } 35 + 36 + export interface HandlerInput { 37 + encoding: 'application/json' 38 + body: InputSchema 39 + } 40 + 41 + export interface HandlerSuccess { 42 + encoding: 'application/json' 43 + body: OutputSchema 44 + headers?: { [key: string]: string } 45 + } 46 + 47 + export interface HandlerError { 48 + status: number 49 + message?: string 50 + error?: 'InvalidSwap' 51 + } 52 + 53 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 54 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 55 + auth: HA 56 + params: QueryParams 57 + input: HandlerInput 58 + req: express.Request 59 + res: express.Response 60 + resetRouteRateLimits: () => Promise<void> 61 + } 62 + export type Handler<HA extends HandlerAuth = never> = ( 63 + ctx: HandlerReqCtx<HA>, 64 + ) => Promise<HandlerOutput> | HandlerOutput
+58
packages/appview/src/lexicons/types/com/atproto/repo/describeRepo.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'com.atproto.repo.describeRepo' 15 + 16 + export interface QueryParams { 17 + /** The handle or DID of the repo. */ 18 + repo: string 19 + } 20 + 21 + export type InputSchema = undefined 22 + 23 + export interface OutputSchema { 24 + handle: string 25 + did: string 26 + /** The complete DID document for this account. */ 27 + didDoc: { [_ in string]: unknown } 28 + /** List of all the collections (NSIDs) for which this repo contains at least one record. */ 29 + collections: string[] 30 + /** Indicates if handle is currently valid (resolves bi-directionally) */ 31 + handleIsCorrect: boolean 32 + } 33 + 34 + export type HandlerInput = undefined 35 + 36 + export interface HandlerSuccess { 37 + encoding: 'application/json' 38 + body: OutputSchema 39 + headers?: { [key: string]: string } 40 + } 41 + 42 + export interface HandlerError { 43 + status: number 44 + message?: string 45 + } 46 + 47 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 48 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 49 + auth: HA 50 + params: QueryParams 51 + input: HandlerInput 52 + req: express.Request 53 + res: express.Response 54 + resetRouteRateLimits: () => Promise<void> 55 + } 56 + export type Handler<HA extends HandlerAuth = never> = ( 57 + ctx: HandlerReqCtx<HA>, 58 + ) => Promise<HandlerOutput> | HandlerOutput
+60
packages/appview/src/lexicons/types/com/atproto/repo/getRecord.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'com.atproto.repo.getRecord' 15 + 16 + export interface QueryParams { 17 + /** The handle or DID of the repo. */ 18 + repo: string 19 + /** The NSID of the record collection. */ 20 + collection: string 21 + /** The Record Key. */ 22 + rkey: string 23 + /** The CID of the version of the record. If not specified, then return the most recent version. */ 24 + cid?: string 25 + } 26 + 27 + export type InputSchema = undefined 28 + 29 + export interface OutputSchema { 30 + uri: string 31 + cid?: string 32 + value: { [_ in string]: unknown } 33 + } 34 + 35 + export type HandlerInput = undefined 36 + 37 + export interface HandlerSuccess { 38 + encoding: 'application/json' 39 + body: OutputSchema 40 + headers?: { [key: string]: string } 41 + } 42 + 43 + export interface HandlerError { 44 + status: number 45 + message?: string 46 + error?: 'RecordNotFound' 47 + } 48 + 49 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 50 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 51 + auth: HA 52 + params: QueryParams 53 + input: HandlerInput 54 + req: express.Request 55 + res: express.Response 56 + resetRouteRateLimits: () => Promise<void> 57 + } 58 + export type Handler<HA extends HandlerAuth = never> = ( 59 + ctx: HandlerReqCtx<HA>, 60 + ) => Promise<HandlerOutput> | HandlerOutput
+42
packages/appview/src/lexicons/types/com/atproto/repo/importRepo.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import stream from 'node:stream' 5 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 6 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 7 + import express from 'express' 8 + import { CID } from 'multiformats/cid' 9 + 10 + import { validate as _validate } from '../../../../lexicons' 11 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'com.atproto.repo.importRepo' 16 + 17 + export interface QueryParams {} 18 + 19 + export type InputSchema = string | Uint8Array | Blob 20 + 21 + export interface HandlerInput { 22 + encoding: 'application/vnd.ipld.car' 23 + body: stream.Readable 24 + } 25 + 26 + export interface HandlerError { 27 + status: number 28 + message?: string 29 + } 30 + 31 + export type HandlerOutput = HandlerError | void 32 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 33 + auth: HA 34 + params: QueryParams 35 + input: HandlerInput 36 + req: express.Request 37 + res: express.Response 38 + resetRouteRateLimits: () => Promise<void> 39 + } 40 + export type Handler<HA extends HandlerAuth = never> = ( 41 + ctx: HandlerReqCtx<HA>, 42 + ) => Promise<HandlerOutput> | HandlerOutput
+68
packages/appview/src/lexicons/types/com/atproto/repo/listMissingBlobs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'com.atproto.repo.listMissingBlobs' 15 + 16 + export interface QueryParams { 17 + limit: number 18 + cursor?: string 19 + } 20 + 21 + export type InputSchema = undefined 22 + 23 + export interface OutputSchema { 24 + cursor?: string 25 + blobs: RecordBlob[] 26 + } 27 + 28 + export type HandlerInput = undefined 29 + 30 + export interface HandlerSuccess { 31 + encoding: 'application/json' 32 + body: OutputSchema 33 + headers?: { [key: string]: string } 34 + } 35 + 36 + export interface HandlerError { 37 + status: number 38 + message?: string 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA 44 + params: QueryParams 45 + input: HandlerInput 46 + req: express.Request 47 + res: express.Response 48 + resetRouteRateLimits: () => Promise<void> 49 + } 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput 53 + 54 + export interface RecordBlob { 55 + $type?: 'com.atproto.repo.listMissingBlobs#recordBlob' 56 + cid: string 57 + recordUri: string 58 + } 59 + 60 + const hashRecordBlob = 'recordBlob' 61 + 62 + export function isRecordBlob<V>(v: V) { 63 + return is$typed(v, id, hashRecordBlob) 64 + } 65 + 66 + export function validateRecordBlob<V>(v: V) { 67 + return validate<RecordBlob & V>(v, id, hashRecordBlob) 68 + }
+80
packages/appview/src/lexicons/types/com/atproto/repo/listRecords.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'com.atproto.repo.listRecords' 15 + 16 + export interface QueryParams { 17 + /** The handle or DID of the repo. */ 18 + repo: string 19 + /** The NSID of the record type. */ 20 + collection: string 21 + /** The number of records to return. */ 22 + limit: number 23 + cursor?: string 24 + /** DEPRECATED: The lowest sort-ordered rkey to start from (exclusive) */ 25 + rkeyStart?: string 26 + /** DEPRECATED: The highest sort-ordered rkey to stop at (exclusive) */ 27 + rkeyEnd?: string 28 + /** Flag to reverse the order of the returned records. */ 29 + reverse?: boolean 30 + } 31 + 32 + export type InputSchema = undefined 33 + 34 + export interface OutputSchema { 35 + cursor?: string 36 + records: Record[] 37 + } 38 + 39 + export type HandlerInput = undefined 40 + 41 + export interface HandlerSuccess { 42 + encoding: 'application/json' 43 + body: OutputSchema 44 + headers?: { [key: string]: string } 45 + } 46 + 47 + export interface HandlerError { 48 + status: number 49 + message?: string 50 + } 51 + 52 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 53 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 54 + auth: HA 55 + params: QueryParams 56 + input: HandlerInput 57 + req: express.Request 58 + res: express.Response 59 + resetRouteRateLimits: () => Promise<void> 60 + } 61 + export type Handler<HA extends HandlerAuth = never> = ( 62 + ctx: HandlerReqCtx<HA>, 63 + ) => Promise<HandlerOutput> | HandlerOutput 64 + 65 + export interface Record { 66 + $type?: 'com.atproto.repo.listRecords#record' 67 + uri: string 68 + cid: string 69 + value: { [_ in string]: unknown } 70 + } 71 + 72 + const hashRecord = 'record' 73 + 74 + export function isRecord<V>(v: V) { 75 + return is$typed(v, id, hashRecord) 76 + } 77 + 78 + export function validateRecord<V>(v: V) { 79 + return validate<Record & V>(v, id, hashRecord) 80 + }
+71
packages/appview/src/lexicons/types/com/atproto/repo/putRecord.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 11 + import type * as ComAtprotoRepoDefs from './defs.js' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'com.atproto.repo.putRecord' 16 + 17 + export interface QueryParams {} 18 + 19 + export interface InputSchema { 20 + /** The handle or DID of the repo (aka, current account). */ 21 + repo: string 22 + /** The NSID of the record collection. */ 23 + collection: string 24 + /** The Record Key. */ 25 + rkey: string 26 + /** 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. */ 27 + validate?: boolean 28 + /** The record to write. */ 29 + record: { [_ in string]: unknown } 30 + /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */ 31 + swapRecord?: string | null 32 + /** Compare and swap with the previous commit by CID. */ 33 + swapCommit?: string 34 + } 35 + 36 + export interface OutputSchema { 37 + uri: string 38 + cid: string 39 + commit?: ComAtprotoRepoDefs.CommitMeta 40 + validationStatus?: 'valid' | 'unknown' | (string & {}) 41 + } 42 + 43 + export interface HandlerInput { 44 + encoding: 'application/json' 45 + body: InputSchema 46 + } 47 + 48 + export interface HandlerSuccess { 49 + encoding: 'application/json' 50 + body: OutputSchema 51 + headers?: { [key: string]: string } 52 + } 53 + 54 + export interface HandlerError { 55 + status: number 56 + message?: string 57 + error?: 'InvalidSwap' 58 + } 59 + 60 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 61 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 62 + auth: HA 63 + params: QueryParams 64 + input: HandlerInput 65 + req: express.Request 66 + res: express.Response 67 + resetRouteRateLimits: () => Promise<void> 68 + } 69 + export type Handler<HA extends HandlerAuth = never> = ( 70 + ctx: HandlerReqCtx<HA>, 71 + ) => Promise<HandlerOutput> | HandlerOutput
+28
packages/appview/src/lexicons/types/com/atproto/repo/strongRef.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + 7 + import { validate as _validate } from '../../../../lexicons' 8 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'com.atproto.repo.strongRef' 13 + 14 + export interface Main { 15 + $type?: 'com.atproto.repo.strongRef' 16 + uri: string 17 + cid: string 18 + } 19 + 20 + const hashMain = 'main' 21 + 22 + export function isMain<V>(v: V) { 23 + return is$typed(v, id, hashMain) 24 + } 25 + 26 + export function validateMain<V>(v: V) { 27 + return validate<Main & V>(v, id, hashMain) 28 + }
+52
packages/appview/src/lexicons/types/com/atproto/repo/uploadBlob.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import stream from 'node:stream' 5 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 6 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 7 + import express from 'express' 8 + import { CID } from 'multiformats/cid' 9 + 10 + import { validate as _validate } from '../../../../lexicons' 11 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'com.atproto.repo.uploadBlob' 16 + 17 + export interface QueryParams {} 18 + 19 + export type InputSchema = string | Uint8Array | Blob 20 + 21 + export interface OutputSchema { 22 + blob: BlobRef 23 + } 24 + 25 + export interface HandlerInput { 26 + encoding: '*/*' 27 + body: stream.Readable 28 + } 29 + 30 + export interface HandlerSuccess { 31 + encoding: 'application/json' 32 + body: OutputSchema 33 + headers?: { [key: string]: string } 34 + } 35 + 36 + export interface HandlerError { 37 + status: number 38 + message?: string 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA 44 + params: QueryParams 45 + input: HandlerInput 46 + req: express.Request 47 + res: express.Response 48 + resetRouteRateLimits: () => Promise<void> 49 + } 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput
+46
packages/appview/src/lexicons/types/xyz/statusphere/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + 7 + import { validate as _validate } from '../../../lexicons' 8 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'xyz.statusphere.defs' 13 + 14 + export interface StatusView { 15 + $type?: 'xyz.statusphere.defs#statusView' 16 + uri: string 17 + status: string 18 + createdAt: string 19 + profile: ProfileView 20 + } 21 + 22 + const hashStatusView = 'statusView' 23 + 24 + export function isStatusView<V>(v: V) { 25 + return is$typed(v, id, hashStatusView) 26 + } 27 + 28 + export function validateStatusView<V>(v: V) { 29 + return validate<StatusView & V>(v, id, hashStatusView) 30 + } 31 + 32 + export interface ProfileView { 33 + $type?: 'xyz.statusphere.defs#profileView' 34 + did: string 35 + handle: string 36 + } 37 + 38 + const hashProfileView = 'profileView' 39 + 40 + export function isProfileView<V>(v: V) { 41 + return is$typed(v, id, hashProfileView) 42 + } 43 + 44 + export function validateProfileView<V>(v: V) { 45 + return validate<ProfileView & V>(v, id, hashProfileView) 46 + }
+51
packages/appview/src/lexicons/types/xyz/statusphere/getStatuses.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 11 + import type * as XyzStatusphereDefs from './defs.js' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'xyz.statusphere.getStatuses' 16 + 17 + export interface QueryParams { 18 + limit: number 19 + } 20 + 21 + export type InputSchema = undefined 22 + 23 + export interface OutputSchema { 24 + statuses: XyzStatusphereDefs.StatusView[] 25 + } 26 + 27 + export type HandlerInput = undefined 28 + 29 + export interface HandlerSuccess { 30 + encoding: 'application/json' 31 + body: OutputSchema 32 + headers?: { [key: string]: string } 33 + } 34 + 35 + export interface HandlerError { 36 + status: number 37 + message?: string 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 41 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 42 + auth: HA 43 + params: QueryParams 44 + input: HandlerInput 45 + req: express.Request 46 + res: express.Response 47 + resetRouteRateLimits: () => Promise<void> 48 + } 49 + export type Handler<HA extends HandlerAuth = never> = ( 50 + ctx: HandlerReqCtx<HA>, 51 + ) => Promise<HandlerOutput> | HandlerOutput
+51
packages/appview/src/lexicons/types/xyz/statusphere/getUser.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 11 + import type * as AppBskyActorDefs from '../../app/bsky/actor/defs.js' 12 + import type * as XyzStatusphereDefs from './defs.js' 13 + 14 + const is$typed = _is$typed, 15 + validate = _validate 16 + const id = 'xyz.statusphere.getUser' 17 + 18 + export interface QueryParams {} 19 + 20 + export type InputSchema = undefined 21 + 22 + export interface OutputSchema { 23 + profile: AppBskyActorDefs.ProfileView 24 + status?: XyzStatusphereDefs.StatusView 25 + } 26 + 27 + export type HandlerInput = undefined 28 + 29 + export interface HandlerSuccess { 30 + encoding: 'application/json' 31 + body: OutputSchema 32 + headers?: { [key: string]: string } 33 + } 34 + 35 + export interface HandlerError { 36 + status: number 37 + message?: string 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 41 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 42 + auth: HA 43 + params: QueryParams 44 + input: HandlerInput 45 + req: express.Request 46 + res: express.Response 47 + resetRouteRateLimits: () => Promise<void> 48 + } 49 + export type Handler<HA extends HandlerAuth = never> = ( 50 + ctx: HandlerReqCtx<HA>, 51 + ) => Promise<HandlerOutput> | HandlerOutput
+54
packages/appview/src/lexicons/types/xyz/statusphere/sendStatus.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server' 6 + import express from 'express' 7 + import { CID } from 'multiformats/cid' 8 + 9 + import { validate as _validate } from '../../../lexicons' 10 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 11 + import type * as XyzStatusphereDefs from './defs.js' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'xyz.statusphere.sendStatus' 16 + 17 + export interface QueryParams {} 18 + 19 + export interface InputSchema { 20 + status: string 21 + } 22 + 23 + export interface OutputSchema { 24 + status: XyzStatusphereDefs.StatusView 25 + } 26 + 27 + export interface HandlerInput { 28 + encoding: 'application/json' 29 + body: InputSchema 30 + } 31 + 32 + export interface HandlerSuccess { 33 + encoding: 'application/json' 34 + body: OutputSchema 35 + headers?: { [key: string]: string } 36 + } 37 + 38 + export interface HandlerError { 39 + status: number 40 + message?: string 41 + } 42 + 43 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough 44 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 45 + auth: HA 46 + params: QueryParams 47 + input: HandlerInput 48 + req: express.Request 49 + res: express.Response 50 + resetRouteRateLimits: () => Promise<void> 51 + } 52 + export type Handler<HA extends HandlerAuth = never> = ( 53 + ctx: HandlerReqCtx<HA>, 54 + ) => Promise<HandlerOutput> | HandlerOutput
+29
packages/appview/src/lexicons/types/xyz/statusphere/status.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + 7 + import { validate as _validate } from '../../../lexicons' 8 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'xyz.statusphere.status' 13 + 14 + export interface Record { 15 + $type: 'xyz.statusphere.status' 16 + status: string 17 + createdAt: string 18 + [k: string]: unknown 19 + } 20 + 21 + const hashRecord = 'main' 22 + 23 + export function isRecord<V>(v: V) { 24 + return is$typed(v, id, hashRecord) 25 + } 26 + 27 + export function validateRecord<V>(v: V) { 28 + return validate<Record & V>(v, id, hashRecord, true) 29 + }
+82
packages/appview/src/lexicons/util.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + 5 + import { ValidationResult } from '@atproto/lexicon' 6 + 7 + export type OmitKey<T, K extends keyof T> = { 8 + [K2 in keyof T as K2 extends K ? never : K2]: T[K2] 9 + } 10 + 11 + export type $Typed<V, T extends string = string> = V & { $type: T } 12 + export type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'> 13 + 14 + export type $Type<Id extends string, Hash extends string> = Hash extends 'main' 15 + ? Id 16 + : `${Id}#${Hash}` 17 + 18 + function isObject<V>(v: V): v is V & object { 19 + return v != null && typeof v === 'object' 20 + } 21 + 22 + function is$type<Id extends string, Hash extends string>( 23 + $type: unknown, 24 + id: Id, 25 + hash: Hash, 26 + ): $type is $Type<Id, Hash> { 27 + return hash === 'main' 28 + ? $type === id 29 + : // $type === `${id}#${hash}` 30 + typeof $type === 'string' && 31 + $type.length === id.length + 1 + hash.length && 32 + $type.charCodeAt(id.length) === 35 /* '#' */ && 33 + $type.startsWith(id) && 34 + $type.endsWith(hash) 35 + } 36 + 37 + export type $TypedObject< 38 + V, 39 + Id extends string, 40 + Hash extends string, 41 + > = V extends { 42 + $type: $Type<Id, Hash> 43 + } 44 + ? V 45 + : V extends { $type?: string } 46 + ? V extends { $type?: infer T extends $Type<Id, Hash> } 47 + ? V & { $type: T } 48 + : never 49 + : V & { $type: $Type<Id, Hash> } 50 + 51 + export function is$typed<V, Id extends string, Hash extends string>( 52 + v: V, 53 + id: Id, 54 + hash: Hash, 55 + ): v is $TypedObject<V, Id, Hash> { 56 + return isObject(v) && '$type' in v && is$type(v.$type, id, hash) 57 + } 58 + 59 + export function maybe$typed<V, Id extends string, Hash extends string>( 60 + v: V, 61 + id: Id, 62 + hash: Hash, 63 + ): v is V & object & { $type?: $Type<Id, Hash> } { 64 + return ( 65 + isObject(v) && 66 + ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true) 67 + ) 68 + } 69 + 70 + export type Validator<R = unknown> = (v: unknown) => ValidationResult<R> 71 + export type ValidatorParam<V extends Validator> = 72 + V extends Validator<infer R> ? R : never 73 + 74 + /** 75 + * Utility function that allows to convert a "validate*" utility function into a 76 + * type predicate. 77 + */ 78 + export function asPredicate<V extends Validator>(validate: V) { 79 + return function <T>(v: T): v is T & ValidatorParam<V> { 80 + return validate(v).success 81 + } 82 + }
+1
packages/appview/src/lib/env.ts
··· 15 15 COOKIE_SECRET: str({ devDefault: '0'.repeat(32) }), 16 16 SERVICE_DID: str({ default: undefined }), 17 17 PUBLIC_URL: str({ devDefault: '' }), 18 + JETSTREAM_INSTANCE: str({ default: 'wss://jetstream2.us-east.bsky.network' }), 18 19 })
+27 -3
packages/appview/src/lib/hydrate.ts
··· 1 - import { XyzStatusphereDefs } from '@statusphere/lexicon' 1 + import { 2 + AppBskyActorDefs, 3 + AppBskyActorProfile, 4 + XyzStatusphereDefs, 5 + } from '@statusphere/lexicon' 2 6 7 + import { AppContext } from '#/context' 3 8 import { Status } from '#/db' 4 - import { AppContext } from '#/index' 9 + 10 + const INVALID_HANDLE = 'handle.invalid' 5 11 6 12 export async function statusToStatusView( 7 13 status: Status, ··· 15 21 did: status.authorDid, 16 22 handle: await ctx.resolver 17 23 .resolveDidToHandle(status.authorDid) 18 - .catch(() => 'invalid.handle'), 24 + .then((handle) => (handle.startsWith('did:') ? INVALID_HANDLE : handle)) 25 + .catch(() => INVALID_HANDLE), 19 26 }, 20 27 } 21 28 } 29 + 30 + export async function bskyProfileToProfileView( 31 + did: string, 32 + profile: AppBskyActorProfile.Record, 33 + ctx: AppContext, 34 + ): Promise<AppBskyActorDefs.ProfileView> { 35 + return { 36 + $type: 'app.bsky.actor.defs#profileView', 37 + did: did, 38 + handle: await ctx.resolver.resolveDidToHandle(did), 39 + avatar: profile.avatar 40 + ? `https://atproto.pictures/img/${did}/${profile.avatar.ref}` 41 + : undefined, 42 + displayName: profile.displayName, 43 + createdAt: profile.createdAt, 44 + } 45 + }
-333
packages/appview/src/routes.ts
··· 1 - import type { IncomingMessage, ServerResponse } from 'node:http' 2 - import { Agent } from '@atproto/api' 3 - import { TID } from '@atproto/common' 4 - import { OAuthResolverError } from '@atproto/oauth-client-node' 5 - import { isValidHandle } from '@atproto/syntax' 6 - import { AppBskyActorProfile, XyzStatusphereStatus } from '@statusphere/lexicon' 7 - import express from 'express' 8 - import { getIronSession, SessionOptions } from 'iron-session' 9 - 10 - import type { AppContext } from '#/index' 11 - import { env } from '#/lib/env' 12 - import { statusToStatusView } from '#/lib/hydrate' 13 - 14 - type Session = { did: string } 15 - 16 - // Common session options 17 - const sessionOptions: SessionOptions = { 18 - cookieName: 'sid', 19 - password: env.COOKIE_SECRET, 20 - cookieOptions: { 21 - secure: env.NODE_ENV === 'production', 22 - httpOnly: true, 23 - sameSite: true, 24 - path: '/', 25 - // Don't set domain explicitly - let browser determine it 26 - domain: undefined, 27 - }, 28 - } 29 - 30 - // Helper function for defining routes 31 - const handler = 32 - ( 33 - fn: ( 34 - req: express.Request, 35 - res: express.Response, 36 - next: express.NextFunction, 37 - ) => Promise<void> | void, 38 - ) => 39 - async ( 40 - req: express.Request, 41 - res: express.Response, 42 - next: express.NextFunction, 43 - ) => { 44 - try { 45 - await fn(req, res, next) 46 - } catch (err) { 47 - next(err) 48 - } 49 - } 50 - 51 - // Helper function to get the Atproto Agent for the active session 52 - async function getSessionAgent( 53 - req: IncomingMessage | express.Request, 54 - res: ServerResponse<IncomingMessage> | express.Response, 55 - ctx: AppContext, 56 - ) { 57 - const session = await getIronSession<Session>(req, res, sessionOptions) 58 - 59 - if (!session.did) { 60 - return null 61 - } 62 - 63 - try { 64 - const oauthSession = await ctx.oauthClient.restore(session.did) 65 - return oauthSession ? new Agent(oauthSession) : null 66 - } catch (err) { 67 - ctx.logger.warn({ err }, 'oauth restore failed') 68 - session.destroy() 69 - return null 70 - } 71 - } 72 - 73 - export const createRouter = (ctx: AppContext) => { 74 - const router = express.Router() 75 - 76 - // Simple CORS configuration for all routes 77 - router.use((req, res, next) => { 78 - // Allow requests from either the specific origin or any origin during development 79 - res.header('Access-Control-Allow-Origin', req.headers.origin || '*') 80 - res.header('Access-Control-Allow-Credentials', 'true') 81 - res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 82 - res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization') 83 - 84 - if (req.method === 'OPTIONS') { 85 - res.status(200).end() 86 - return 87 - } 88 - next() 89 - }) 90 - 91 - // OAuth metadata 92 - router.get( 93 - '/client-metadata.json', 94 - handler((_req, res) => { 95 - res.json(ctx.oauthClient.clientMetadata) 96 - }), 97 - ) 98 - 99 - // OAuth callback to complete session creation 100 - router.get( 101 - '/oauth/callback', 102 - handler(async (req, res) => { 103 - // Get the query parameters from the URL 104 - const params = new URLSearchParams(req.originalUrl.split('?')[1]) 105 - 106 - try { 107 - const { session } = await ctx.oauthClient.callback(params) 108 - 109 - // Use the common session options 110 - const clientSession = await getIronSession<Session>( 111 - req, 112 - res, 113 - sessionOptions, 114 - ) 115 - 116 - // Set the DID on the session 117 - clientSession.did = session.did 118 - await clientSession.save() 119 - 120 - // Get the origin and determine appropriate redirect 121 - const host = req.get('host') || '' 122 - const protocol = req.protocol || 'http' 123 - const baseUrl = `${protocol}://${host}` 124 - 125 - ctx.logger.info( 126 - `OAuth callback successful, redirecting to ${baseUrl}/oauth-callback`, 127 - ) 128 - 129 - // Redirect to the frontend oauth-callback page 130 - res.redirect('/oauth-callback') 131 - } catch (err) { 132 - ctx.logger.error({ err }, 'oauth callback failed') 133 - 134 - // Handle error redirect - stay on same domain 135 - res.redirect('/oauth-callback?error=auth') 136 - } 137 - }), 138 - ) 139 - 140 - // Login handler 141 - router.post( 142 - '/login', 143 - handler(async (req, res) => { 144 - // Validate 145 - const handle = req.body?.handle 146 - if (typeof handle !== 'string' || !isValidHandle(handle)) { 147 - res.status(400).json({ error: 'invalid handle' }) 148 - return 149 - } 150 - 151 - // Initiate the OAuth flow 152 - try { 153 - const url = await ctx.oauthClient.authorize(handle, { 154 - scope: 'atproto transition:generic', 155 - }) 156 - res.json({ redirectUrl: url.toString() }) 157 - } catch (err) { 158 - ctx.logger.error({ err }, 'oauth authorize failed') 159 - const errorMsg = 160 - err instanceof OAuthResolverError 161 - ? err.message 162 - : "couldn't initiate login" 163 - res.status(500).json({ error: errorMsg }) 164 - } 165 - }), 166 - ) 167 - 168 - // Logout handler 169 - router.post( 170 - '/logout', 171 - handler(async (req, res) => { 172 - const session = await getIronSession<Session>(req, res, sessionOptions) 173 - session.destroy() 174 - res.json({ success: true }) 175 - }), 176 - ) 177 - 178 - // Get current user info 179 - router.get( 180 - '/user', 181 - handler(async (req, res) => { 182 - const agent = await getSessionAgent(req, res, ctx) 183 - if (!agent) { 184 - res.status(401).json({ error: 'Not logged in' }) 185 - return 186 - } 187 - 188 - const did = agent.assertDid 189 - 190 - // Fetch user profile 191 - try { 192 - const profileResponse = await agent.com.atproto.repo 193 - .getRecord({ 194 - repo: did, 195 - collection: 'app.bsky.actor.profile', 196 - rkey: 'self', 197 - }) 198 - .catch(() => undefined) 199 - 200 - const profileRecord = profileResponse?.data 201 - const profile = 202 - profileRecord && 203 - AppBskyActorProfile.isRecord(profileRecord.value) && 204 - AppBskyActorProfile.validateRecord(profileRecord.value).success 205 - ? profileRecord.value 206 - : ({} as AppBskyActorProfile.Record) 207 - 208 - profile.did = did 209 - profile.handle = await ctx.resolver.resolveDidToHandle(did) 210 - 211 - // Fetch user status 212 - const status = await ctx.db 213 - .selectFrom('status') 214 - .selectAll() 215 - .where('authorDid', '=', did) 216 - .orderBy('indexedAt', 'desc') 217 - .executeTakeFirst() 218 - 219 - res.json({ 220 - did: agent.assertDid, 221 - profile, 222 - status: status ? await statusToStatusView(status, ctx) : undefined, 223 - }) 224 - } catch (err) { 225 - ctx.logger.error({ err }, 'Failed to get user info') 226 - res.status(500).json({ error: 'Failed to get user info' }) 227 - } 228 - }), 229 - ) 230 - 231 - // Get statuses 232 - router.get( 233 - '/statuses', 234 - handler(async (req, res) => { 235 - try { 236 - // Fetch data stored in our SQLite 237 - const statuses = await ctx.db 238 - .selectFrom('status') 239 - .selectAll() 240 - .orderBy('indexedAt', 'desc') 241 - .limit(30) 242 - .execute() 243 - 244 - res.json({ 245 - statuses: await Promise.all( 246 - statuses.map((status) => statusToStatusView(status, ctx)), 247 - ), 248 - }) 249 - } catch (err) { 250 - ctx.logger.error({ err }, 'Failed to get statuses') 251 - res.status(500).json({ error: 'Failed to get statuses' }) 252 - } 253 - }), 254 - ) 255 - 256 - // Create status 257 - router.post( 258 - '/status', 259 - handler(async (req, res) => { 260 - // If the user is signed in, get an agent which communicates with their server 261 - const agent = await getSessionAgent(req, res, ctx) 262 - if (!agent) { 263 - res.status(401).json({ error: 'Session required' }) 264 - return 265 - } 266 - 267 - // Construct & validate their status record 268 - const rkey = TID.nextStr() 269 - const record = { 270 - $type: 'xyz.statusphere.status', 271 - status: req.body?.status, 272 - createdAt: new Date().toISOString(), 273 - } 274 - if (!XyzStatusphereStatus.validateRecord(record).success) { 275 - res.status(400).json({ error: 'Invalid status' }) 276 - return 277 - } 278 - 279 - let uri 280 - try { 281 - // Write the status record to the user's repository 282 - const response = await agent.com.atproto.repo.putRecord({ 283 - repo: agent.assertDid, 284 - collection: 'xyz.statusphere.status', 285 - rkey, 286 - record, 287 - validate: false, 288 - }) 289 - uri = response.data.uri 290 - } catch (err) { 291 - ctx.logger.warn({ err }, 'failed to write record') 292 - res.status(500).json({ error: 'Failed to write record' }) 293 - return 294 - } 295 - 296 - try { 297 - // Optimistically update our SQLite 298 - // This isn't strictly necessary because the write event will be 299 - // handled in #/firehose/ingestor.ts, but it ensures that future reads 300 - // will be up-to-date after this method finishes. 301 - await ctx.db 302 - .insertInto('status') 303 - .values({ 304 - uri, 305 - authorDid: agent.assertDid, 306 - status: record.status, 307 - createdAt: record.createdAt, 308 - indexedAt: new Date().toISOString(), 309 - }) 310 - .execute() 311 - 312 - res.json({ 313 - success: true, 314 - uri, 315 - status: await statusToStatusView(record.status, ctx), 316 - }) 317 - } catch (err) { 318 - ctx.logger.warn( 319 - { err }, 320 - 'failed to update computed view; ignoring as it should be caught by the firehose', 321 - ) 322 - res.json({ 323 - success: true, 324 - uri, 325 - status: await statusToStatusView(record.status, ctx), 326 - warning: 'Database not updated', 327 - }) 328 - } 329 - }), 330 - ) 331 - 332 - return router 333 - }
+48
packages/appview/src/session.ts
··· 1 + import { IncomingMessage, ServerResponse } from 'node:http' 2 + import { Agent } from '@atproto/api' 3 + import { Request, Response } from 'express' 4 + import { getIronSession, SessionOptions } from 'iron-session' 5 + 6 + import { AppContext } from '#/context' 7 + import { env } from '#/lib/env' 8 + 9 + type Session = { did: string } 10 + 11 + // Common session options 12 + const sessionOptions: SessionOptions = { 13 + cookieName: 'sid', 14 + password: env.COOKIE_SECRET, 15 + cookieOptions: { 16 + secure: env.NODE_ENV === 'production', 17 + httpOnly: true, 18 + sameSite: true, 19 + path: '/', 20 + // Don't set domain explicitly - let browser determine it 21 + domain: undefined, 22 + }, 23 + } 24 + 25 + export async function getSessionAgent( 26 + req: IncomingMessage | Request, 27 + res: ServerResponse<IncomingMessage> | Response, 28 + ctx: AppContext, 29 + ) { 30 + const session = await getIronSession<Session>(req, res, sessionOptions) 31 + 32 + if (!session.did) { 33 + return null 34 + } 35 + 36 + try { 37 + const oauthSession = await ctx.oauthClient.restore(session.did) 38 + return oauthSession ? new Agent(oauthSession) : null 39 + } catch (err) { 40 + ctx.logger.warn({ err }, 'oauth restore failed') 41 + session.destroy() 42 + return null 43 + } 44 + } 45 + 46 + export async function getSession(req: Request, res: Response) { 47 + return getIronSession<Session>(req, res, sessionOptions) 48 + }
+7 -2
packages/client/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 5 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 7 <title>Statusphere React</title> 8 + <script 9 + defer 10 + data-domain="statusphere.mozzius.dev" 11 + src="https://plausible.mozzius.dev/js/script.js" 12 + ></script> 8 13 </head> 9 14 <body> 10 15 <div id="root"></div> 11 16 <script type="module" src="/src/main.tsx"></script> 12 17 </body> 13 - </html> 18 + </html>
+1
packages/client/package.json
··· 14 14 }, 15 15 "dependencies": { 16 16 "@atproto/api": "^0.14.7", 17 + "@atproto/xrpc": "^0.6.9", 17 18 "@statusphere/lexicon": "workspace:*", 18 19 "@tailwindcss/vite": "^4.0.9", 19 20 "@tanstack/react-query": "^5.66.11",
+10 -3
packages/client/src/components/Header.tsx
··· 27 27 <nav> 28 28 {user ? ( 29 29 <div className="flex gap-4 items-center"> 30 + {user.profile.avatar ? ( 31 + <img 32 + src={user.profile.avatar} 33 + alt={user.profile.displayName || user.profile.handle} 34 + className="w-8 h-8 rounded-full text-transparent" 35 + /> 36 + ) : ( 37 + <div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded-full"></div> 38 + )} 30 39 <span className="text-gray-700 dark:text-gray-300"> 31 - {user.profile?.displayName || 32 - user.profile?.handle || 33 - user.did.substring(0, 15)} 40 + {user.profile.displayName || user.profile.handle} 34 41 </span> 35 42 <button 36 43 onClick={handleLogout}
+3 -3
packages/client/src/components/StatusForm.tsx
··· 5 5 import useAuth from '#/hooks/useAuth' 6 6 import api from '#/services/api' 7 7 8 - const STATUS_OPTIONS = [ 8 + export const STATUS_OPTIONS = [ 9 9 '๐Ÿ‘', 10 10 '๐Ÿ‘Ž', 11 11 '๐Ÿ’™', ··· 46 46 47 47 // Use React Query mutation for creating a status 48 48 const mutation = useMutation({ 49 - mutationFn: (emoji: string) => api.createStatus(emoji), 49 + mutationFn: (emoji: string) => api.createStatus({ status: emoji }), 50 50 onMutate: async (emoji) => { 51 51 // Cancel any outgoing refetches so they don't overwrite our optimistic updates 52 52 await queryClient.cancelQueries({ queryKey: ['statuses'] }) ··· 65 65 const optimisticStatus = { 66 66 uri: `optimistic-${Date.now()}`, 67 67 profile: { 68 - did: user.did, 68 + did: user.profile.did, 69 69 handle: user.profile.handle, 70 70 }, 71 71 status: emoji,
+20 -3
packages/client/src/components/StatusList.tsx
··· 1 + import { useEffect } from 'react' 1 2 import { useQuery } from '@tanstack/react-query' 2 3 3 4 import api from '#/services/api' 5 + import { STATUS_OPTIONS } from './StatusForm' 4 6 5 7 const StatusList = () => { 6 8 // Use React Query to fetch and cache statuses 7 9 const { data, isPending, isError, error } = useQuery({ 8 10 queryKey: ['statuses'], 9 11 queryFn: async () => { 10 - const data = await api.getStatuses() 12 + const { data } = await api.getStatuses({ limit: 30 }) 11 13 return data 12 14 }, 13 15 placeholderData: (previousData) => previousData, // Use previous data while refetching 14 16 refetchInterval: 30e3, // Refetch every 30 seconds 15 17 }) 16 18 19 + useEffect(() => { 20 + if (error) { 21 + console.error(error) 22 + } 23 + }, [error]) 24 + 17 25 // Destructure data 18 26 const statuses = data?.statuses || [] 19 27 28 + // Get a random emoji from the STATUS_OPTIONS array 29 + const randomEmoji = 30 + STATUS_OPTIONS[Math.floor(Math.random() * STATUS_OPTIONS.length)] 31 + 20 32 if (isPending && !data) { 21 33 return ( 22 - <div className="py-4 text-center text-gray-500 dark:text-gray-400"> 23 - Loading statuses... 34 + <div className="py-8 text-center"> 35 + <div className="text-5xl mb-2 animate-pulse inline-block"> 36 + {randomEmoji} 37 + </div> 38 + <div className="text-gray-500 dark:text-gray-400"> 39 + Loading statuses... 40 + </div> 24 41 </div> 25 42 ) 26 43 }
+13 -13
packages/client/src/hooks/useAuth.tsx
··· 1 1 import { createContext, ReactNode, useContext, useState } from 'react' 2 2 import { useQuery, useQueryClient } from '@tanstack/react-query' 3 + import { XRPCError } from '@atproto/xrpc' 4 + import { XyzStatusphereGetUser } from '@statusphere/lexicon' 3 5 4 - import api, { User } from '#/services/api' 6 + import api from '#/services/api' 5 7 6 8 interface AuthContextType { 7 - user: User | null 9 + user: XyzStatusphereGetUser.OutputSchema | null 8 10 loading: boolean 9 11 error: string | null 10 12 login: (handle: string) => Promise<{ redirectUrl: string }> ··· 39 41 } 40 42 41 43 try { 42 - const userData = await api.getCurrentUser() 44 + const { data: userData } = await api.getCurrentUser({}) 43 45 44 46 // Clean up URL if needed 45 47 if (window.location.search && userData) { ··· 52 54 53 55 return userData 54 56 } catch (apiErr) { 57 + if ( 58 + apiErr instanceof XRPCError && 59 + apiErr.error === 'AuthenticationRequired' 60 + ) { 61 + return null 62 + } 63 + 55 64 console.error('๐Ÿšซ API error during auth check:', apiErr) 56 65 57 66 // If it's a network error, provide a more helpful message ··· 75 84 setError(null) 76 85 77 86 try { 78 - // Add a small artificial delay for UX purposes 79 - const loginPromise = api.login(handle) 80 - 81 - // Ensure the loading state shows for at least 800ms for better UX 82 - const result = await Promise.all([ 83 - loginPromise, 84 - new Promise((resolve) => setTimeout(resolve, 800)), 85 - ]).then(([loginResult]) => loginResult) 86 - 87 - return result 87 + return await api.login(handle) 88 88 } catch (err) { 89 89 const message = err instanceof Error ? err.message : 'Login failed' 90 90 setError(message)
+7 -10
packages/client/src/pages/HomePage.tsx
··· 1 1 import Header from '#/components/Header' 2 - import StatusForm from '#/components/StatusForm' 2 + import StatusForm, { STATUS_OPTIONS } from '#/components/StatusForm' 3 3 import StatusList from '#/components/StatusList' 4 4 import { useAuth } from '#/hooks/useAuth' 5 5 6 6 const HomePage = () => { 7 7 const { user, loading, error } = useAuth() 8 8 9 + // Get a random emoji from the STATUS_OPTIONS array 10 + const randomEmoji = 11 + STATUS_OPTIONS[Math.floor(Math.random() * STATUS_OPTIONS.length)] 12 + 9 13 if (loading) { 10 14 return ( 11 - <div className="flex justify-center items-center py-16"> 12 - <div className="text-center p-6"> 13 - <h2 className="text-2xl font-semibold mb-2 text-gray-800 dark:text-gray-200"> 14 - Loading Statusphere... 15 - </h2> 16 - <p className="text-gray-600 dark:text-gray-400"> 17 - Setting up your experience 18 - </p> 19 - </div> 15 + <div className="flex justify-center items-center h-[80vh]"> 16 + <div className="text-9xl animate-pulse">{randomEmoji}</div> 20 17 </div> 21 18 ) 22 19 }
+5 -1
packages/client/src/pages/LoginPage.tsx
··· 49 49 <Header /> 50 50 51 51 <div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm max-w-md mx-auto w-full"> 52 - <h2 className="text-xl font-semibold mb-4">Login with your handle</h2> 52 + <h2 className="text-xl font-semibold mb-4">Login with ATProto</h2> 53 53 54 54 {error && ( 55 55 <div className="text-red-500 mb-4 p-2 bg-red-50 dark:bg-red-950 dark:bg-opacity-30 rounded-md"> ··· 74 74 disabled={pending} 75 75 className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors" 76 76 /> 77 + <p className="text-gray-400 dark:text-gray-500 text-sm mt-2"> 78 + You can also enter an AT Protocol PDS URL, i.e.{' '} 79 + <span className="whitespace-nowrap">https://bsky.social</span> 80 + </p> 77 81 </div> 78 82 79 83 <button
+1 -1
packages/client/src/pages/OAuthCallbackPage.tsx
··· 37 37 .join(', '), 38 38 ) 39 39 40 - const user = await api.getCurrentUser() 40 + const user = await api.getCurrentUser({}) 41 41 console.log('Current user check result:', user) 42 42 43 43 if (user) {
+31 -105
packages/client/src/services/api.ts
··· 1 - import { AppBskyActorDefs, XyzStatusphereDefs } from '@statusphere/lexicon' 1 + import * as Lexicon from '@statusphere/lexicon' 2 + import type { 3 + XyzStatusphereGetStatuses, 4 + XyzStatusphereGetUser, 5 + XyzStatusphereSendStatus, 6 + } from '@statusphere/lexicon' 2 7 3 - // Use '/api' prefix consistently for all API calls 4 - const API_URL = '/api' 8 + class StatusphereAgent extends Lexicon.AtpBaseClient { 9 + constructor() { 10 + super(StatusphereAgent.fetchHandler) 11 + } 5 12 6 - // Helper function for logging API actions 7 - function logApiCall( 8 - method: string, 9 - endpoint: string, 10 - status?: number, 11 - error?: any, 12 - ) { 13 - const statusStr = status ? `[${status}]` : '' 14 - const errorStr = error 15 - ? ` - Error: ${error.message || JSON.stringify(error)}` 16 - : '' 17 - console.log(`๐Ÿ”„ API ${method} ${endpoint} ${statusStr}${errorStr}`) 13 + private static fetchHandler: Lexicon.AtpBaseClient['fetchHandler'] = ( 14 + path, 15 + options, 16 + ) => { 17 + return fetch(path, { 18 + ...options, 19 + headers: { 20 + 'Content-Type': 'application/json', 21 + }, 22 + credentials: 'include', 23 + }) 24 + } 18 25 } 19 26 20 - export interface User { 21 - did: string 22 - profile: AppBskyActorDefs.ProfileView 23 - status?: XyzStatusphereDefs.StatusView 24 - } 27 + const agent = new StatusphereAgent() 25 28 26 29 // API service 27 30 export const api = { 28 31 // Login 29 32 async login(handle: string) { 30 - const url = `${API_URL}/login` 31 - logApiCall('POST', url) 32 - 33 - const response = await fetch(url, { 33 + const response = await fetch('/oauth/initiate', { 34 34 method: 'POST', 35 35 headers: { 36 36 'Content-Type': 'application/json', ··· 49 49 50 50 // Logout 51 51 async logout() { 52 - const url = `${API_URL}/logout` 53 - logApiCall('POST', url) 54 - const response = await fetch(url, { 52 + const response = await fetch('/oauth/logout', { 55 53 method: 'POST', 56 54 credentials: 'include', 57 55 }) ··· 64 62 }, 65 63 66 64 // Get current user 67 - async getCurrentUser() { 68 - const url = `${API_URL}/user` 69 - logApiCall('GET', url) 70 - try { 71 - const headers = { 72 - Accept: 'application/json', 73 - } 74 - 75 - const response = await fetch(url, { 76 - credentials: 'include', // This is crucial for sending cookies 77 - headers, 78 - cache: 'no-cache', // Don't cache this request 79 - }) 80 - 81 - logApiCall('GET', '/user', response.status) 82 - 83 - if (!response.ok) { 84 - if (response.status === 401) { 85 - return null 86 - } 87 - 88 - // Try to get error details 89 - let errorText = '' 90 - try { 91 - const errorData = await response.text() 92 - errorText = errorData 93 - } catch (e) { 94 - // Ignore error reading error 95 - } 96 - 97 - throw new Error( 98 - `Failed to get user: ${response.status} ${response.statusText} ${errorText}`, 99 - ) 100 - } 101 - 102 - return response.json() 103 - } catch (error) { 104 - logApiCall('GET', '/user', undefined, error) 105 - if ( 106 - error instanceof TypeError && 107 - error.message.includes('Failed to fetch') 108 - ) { 109 - console.error('Network error - Unable to connect to API server') 110 - } 111 - throw error 112 - } 65 + getCurrentUser(params: XyzStatusphereGetUser.QueryParams) { 66 + return agent.xyz.statusphere.getUser(params) 113 67 }, 114 68 115 69 // Get statuses 116 - async getStatuses() { 117 - const url = `${API_URL}/statuses` 118 - logApiCall('GET', url) 119 - const response = await fetch(url, { 120 - credentials: 'include', 121 - }) 122 - 123 - if (!response.ok) { 124 - throw new Error('Failed to get statuses') 125 - } 126 - 127 - return response.json() as Promise<{ 128 - statuses: XyzStatusphereDefs.StatusView[] 129 - }> 70 + getStatuses(params: XyzStatusphereGetStatuses.QueryParams) { 71 + return agent.xyz.statusphere.getStatuses(params) 130 72 }, 131 73 132 74 // Create status 133 - async createStatus(status: string) { 134 - const url = `${API_URL}/status` 135 - logApiCall('POST', url) 136 - const response = await fetch(url, { 137 - method: 'POST', 138 - headers: { 139 - 'Content-Type': 'application/json', 140 - }, 141 - credentials: 'include', 142 - body: JSON.stringify({ status }), 143 - }) 144 - 145 - if (!response.ok) { 146 - const error = await response.json() 147 - throw new Error(error.error || 'Failed to create status') 148 - } 149 - 150 - return response.json() 75 + createStatus(params: XyzStatusphereSendStatus.InputSchema) { 76 + return agent.xyz.statusphere.sendStatus(params) 151 77 }, 152 78 } 153 79
+1 -1
packages/client/vite.config.ts
··· 18 18 host: '127.0.0.1', 19 19 port: 3000, 20 20 proxy: { 21 - '/api': { 21 + '^/(xrpc|oauth|client-metadata\.json)/.*': { 22 22 target: 'http://localhost:3001', 23 23 changeOrigin: true, 24 24 },
+8
packages/lexicon/package.json
··· 5 5 "author": "", 6 6 "license": "MIT", 7 7 "main": "dist/index.js", 8 + "module": "dist/index.mjs", 8 9 "types": "dist/index.d.ts", 10 + "exports": { 11 + ".": { 12 + "types": "./dist/index.d.ts", 13 + "import": "./dist/index.mjs", 14 + "require": "./dist/index.js" 15 + } 16 + }, 9 17 "private": true, 10 18 "scripts": { 11 19 "build": "pnpm lexgen && tsup",
+32
packages/lexicon/src/index.ts
··· 21 21 import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef.js' 22 22 import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js' 23 23 import * as XyzStatusphereDefs from './types/xyz/statusphere/defs.js' 24 + import * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js' 25 + import * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js' 26 + import * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js' 24 27 import * as XyzStatusphereStatus from './types/xyz/statusphere/status.js' 25 28 import { OmitKey, Un$Typed } from './util.js' 26 29 27 30 export * as XyzStatusphereDefs from './types/xyz/statusphere/defs.js' 31 + export * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js' 32 + export * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js' 33 + export * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js' 28 34 export * as XyzStatusphereStatus from './types/xyz/statusphere/status.js' 29 35 export * as ComAtprotoLabelDefs from './types/com/atproto/label/defs.js' 30 36 export * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js' ··· 77 83 constructor(client: XrpcClient) { 78 84 this._client = client 79 85 this.status = new StatusRecord(client) 86 + } 87 + 88 + getStatuses( 89 + params?: XyzStatusphereGetStatuses.QueryParams, 90 + opts?: XyzStatusphereGetStatuses.CallOptions, 91 + ): Promise<XyzStatusphereGetStatuses.Response> { 92 + return this._client.call( 93 + 'xyz.statusphere.getStatuses', 94 + params, 95 + undefined, 96 + opts, 97 + ) 98 + } 99 + 100 + getUser( 101 + params?: XyzStatusphereGetUser.QueryParams, 102 + opts?: XyzStatusphereGetUser.CallOptions, 103 + ): Promise<XyzStatusphereGetUser.Response> { 104 + return this._client.call('xyz.statusphere.getUser', params, undefined, opts) 105 + } 106 + 107 + sendStatus( 108 + data?: XyzStatusphereSendStatus.InputSchema, 109 + opts?: XyzStatusphereSendStatus.CallOptions, 110 + ): Promise<XyzStatusphereSendStatus.Response> { 111 + return this._client.call('xyz.statusphere.sendStatus', opts?.qp, data, opts) 80 112 } 81 113 } 82 114
+109
packages/lexicon/src/lexicons.ts
··· 55 55 }, 56 56 }, 57 57 }, 58 + XyzStatusphereGetStatuses: { 59 + lexicon: 1, 60 + id: 'xyz.statusphere.getStatuses', 61 + defs: { 62 + main: { 63 + type: 'query', 64 + description: 'Get a list of the most recent statuses on the network.', 65 + parameters: { 66 + type: 'params', 67 + properties: { 68 + limit: { 69 + type: 'integer', 70 + minimum: 1, 71 + maximum: 100, 72 + default: 50, 73 + }, 74 + }, 75 + }, 76 + output: { 77 + encoding: 'application/json', 78 + schema: { 79 + type: 'object', 80 + required: ['statuses'], 81 + properties: { 82 + statuses: { 83 + type: 'array', 84 + items: { 85 + type: 'ref', 86 + ref: 'lex:xyz.statusphere.defs#statusView', 87 + }, 88 + }, 89 + }, 90 + }, 91 + }, 92 + }, 93 + }, 94 + }, 95 + XyzStatusphereGetUser: { 96 + lexicon: 1, 97 + id: 'xyz.statusphere.getUser', 98 + defs: { 99 + main: { 100 + type: 'query', 101 + description: "Get the current user's profile and status.", 102 + parameters: { 103 + type: 'params', 104 + properties: {}, 105 + }, 106 + output: { 107 + encoding: 'application/json', 108 + schema: { 109 + type: 'object', 110 + required: ['profile'], 111 + properties: { 112 + profile: { 113 + type: 'ref', 114 + ref: 'lex:app.bsky.actor.defs#profileView', 115 + }, 116 + status: { 117 + type: 'ref', 118 + ref: 'lex:xyz.statusphere.defs#statusView', 119 + }, 120 + }, 121 + }, 122 + }, 123 + }, 124 + }, 125 + }, 126 + XyzStatusphereSendStatus: { 127 + lexicon: 1, 128 + id: 'xyz.statusphere.sendStatus', 129 + defs: { 130 + main: { 131 + type: 'procedure', 132 + description: 'Send a status into the ATmosphere.', 133 + input: { 134 + encoding: 'application/json', 135 + schema: { 136 + type: 'object', 137 + required: ['status'], 138 + properties: { 139 + status: { 140 + type: 'string', 141 + minLength: 1, 142 + maxGraphemes: 1, 143 + maxLength: 32, 144 + }, 145 + }, 146 + }, 147 + }, 148 + output: { 149 + encoding: 'application/json', 150 + schema: { 151 + type: 'object', 152 + required: ['status'], 153 + properties: { 154 + status: { 155 + type: 'ref', 156 + ref: 'lex:xyz.statusphere.defs#statusView', 157 + }, 158 + }, 159 + }, 160 + }, 161 + }, 162 + }, 163 + }, 58 164 XyzStatusphereStatus: { 59 165 lexicon: 1, 60 166 id: 'xyz.statusphere.status', ··· 1169 1275 1170 1276 export const ids = { 1171 1277 XyzStatusphereDefs: 'xyz.statusphere.defs', 1278 + XyzStatusphereGetStatuses: 'xyz.statusphere.getStatuses', 1279 + XyzStatusphereGetUser: 'xyz.statusphere.getUser', 1280 + XyzStatusphereSendStatus: 'xyz.statusphere.sendStatus', 1172 1281 XyzStatusphereStatus: 'xyz.statusphere.status', 1173 1282 ComAtprotoLabelDefs: 'com.atproto.label.defs', 1174 1283 ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites',
+39
packages/lexicon/src/types/xyz/statusphere/getStatuses.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HeadersMap, XRPCError } from '@atproto/xrpc' 6 + import { CID } from 'multiformats/cid' 7 + 8 + import { validate as _validate } from '../../../lexicons' 9 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 10 + import type * as XyzStatusphereDefs from './defs.js' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'xyz.statusphere.getStatuses' 15 + 16 + export interface QueryParams { 17 + limit?: number 18 + } 19 + 20 + export type InputSchema = undefined 21 + 22 + export interface OutputSchema { 23 + statuses: XyzStatusphereDefs.StatusView[] 24 + } 25 + 26 + export interface CallOptions { 27 + signal?: AbortSignal 28 + headers?: HeadersMap 29 + } 30 + 31 + export interface Response { 32 + success: boolean 33 + headers: HeadersMap 34 + data: OutputSchema 35 + } 36 + 37 + export function toKnownErr(e: any) { 38 + return e 39 + }
+39
packages/lexicon/src/types/xyz/statusphere/getUser.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HeadersMap, XRPCError } from '@atproto/xrpc' 6 + import { CID } from 'multiformats/cid' 7 + 8 + import { validate as _validate } from '../../../lexicons' 9 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 10 + import type * as AppBskyActorDefs from '../../app/bsky/actor/defs.js' 11 + import type * as XyzStatusphereDefs from './defs.js' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'xyz.statusphere.getUser' 16 + 17 + export interface QueryParams {} 18 + 19 + export type InputSchema = undefined 20 + 21 + export interface OutputSchema { 22 + profile: AppBskyActorDefs.ProfileView 23 + status?: XyzStatusphereDefs.StatusView 24 + } 25 + 26 + export interface CallOptions { 27 + signal?: AbortSignal 28 + headers?: HeadersMap 29 + } 30 + 31 + export interface Response { 32 + success: boolean 33 + headers: HeadersMap 34 + data: OutputSchema 35 + } 36 + 37 + export function toKnownErr(e: any) { 38 + return e 39 + }
+41
packages/lexicon/src/types/xyz/statusphere/sendStatus.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { BlobRef, ValidationResult } from '@atproto/lexicon' 5 + import { HeadersMap, XRPCError } from '@atproto/xrpc' 6 + import { CID } from 'multiformats/cid' 7 + 8 + import { validate as _validate } from '../../../lexicons' 9 + import { is$typed as _is$typed, $Typed, OmitKey } from '../../../util' 10 + import type * as XyzStatusphereDefs from './defs.js' 11 + 12 + const is$typed = _is$typed, 13 + validate = _validate 14 + const id = 'xyz.statusphere.sendStatus' 15 + 16 + export interface QueryParams {} 17 + 18 + export interface InputSchema { 19 + status: string 20 + } 21 + 22 + export interface OutputSchema { 23 + status: XyzStatusphereDefs.StatusView 24 + } 25 + 26 + export interface CallOptions { 27 + signal?: AbortSignal 28 + headers?: HeadersMap 29 + qp?: QueryParams 30 + encoding?: 'application/json' 31 + } 32 + 33 + export interface Response { 34 + success: boolean 35 + headers: HeadersMap 36 + data: OutputSchema 37 + } 38 + 39 + export function toKnownErr(e: any) { 40 + return e 41 + }
+58
pnpm-lock.yaml
··· 62 62 better-sqlite3: 63 63 specifier: ^11.8.1 64 64 version: 11.8.1 65 + compression: 66 + specifier: ^1.8.0 67 + version: 1.8.0 65 68 cors: 66 69 specifier: ^2.8.5 67 70 version: 2.8.5 ··· 86 89 pino: 87 90 specifier: ^9.6.0 88 91 version: 9.6.0 92 + ws: 93 + specifier: ^8.18.1 94 + version: 8.18.1 89 95 devDependencies: 96 + '@atproto/lex-cli': 97 + specifier: ^0.6.1 98 + version: 0.6.1 90 99 '@types/better-sqlite3': 91 100 specifier: ^7.6.12 92 101 version: 7.6.12 102 + '@types/compression': 103 + specifier: ^1.7.5 104 + version: 1.7.5 93 105 '@types/cors': 94 106 specifier: ^2.8.17 95 107 version: 2.8.17 ··· 123 135 '@atproto/api': 124 136 specifier: ^0.14.7 125 137 version: 0.14.7 138 + '@atproto/xrpc': 139 + specifier: ^0.6.9 140 + version: 0.6.9 126 141 '@statusphere/lexicon': 127 142 specifier: workspace:* 128 143 version: link:../lexicon ··· 959 974 '@types/body-parser@1.19.5': 960 975 resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} 961 976 977 + '@types/compression@1.7.5': 978 + resolution: {integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==} 979 + 962 980 '@types/connect@3.4.38': 963 981 resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} 964 982 ··· 1250 1268 commander@9.5.0: 1251 1269 resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} 1252 1270 engines: {node: ^12.20.0 || >=14} 1271 + 1272 + compressible@2.0.18: 1273 + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} 1274 + engines: {node: '>= 0.6'} 1275 + 1276 + compression@1.8.0: 1277 + resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==} 1278 + engines: {node: '>= 0.8.0'} 1253 1279 1254 1280 concat-map@0.0.1: 1255 1281 resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} ··· 1995 2021 resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 1996 2022 engines: {node: '>= 0.6'} 1997 2023 2024 + negotiator@0.6.4: 2025 + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} 2026 + engines: {node: '>= 0.6'} 2027 + 1998 2028 node-abi@3.74.0: 1999 2029 resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} 2000 2030 engines: {node: '>=10'} ··· 2024 2054 2025 2055 on-finished@2.4.1: 2026 2056 resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 2057 + engines: {node: '>= 0.8'} 2058 + 2059 + on-headers@1.0.2: 2060 + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} 2027 2061 engines: {node: '>= 0.8'} 2028 2062 2029 2063 once@1.4.0: ··· 3569 3603 '@types/connect': 3.4.38 3570 3604 '@types/node': 22.13.8 3571 3605 3606 + '@types/compression@1.7.5': 3607 + dependencies: 3608 + '@types/express': 5.0.0 3609 + 3572 3610 '@types/connect@3.4.38': 3573 3611 dependencies: 3574 3612 '@types/node': 22.13.8 ··· 3919 3957 commander@4.1.1: {} 3920 3958 3921 3959 commander@9.5.0: {} 3960 + 3961 + compressible@2.0.18: 3962 + dependencies: 3963 + mime-db: 1.52.0 3964 + 3965 + compression@1.8.0: 3966 + dependencies: 3967 + bytes: 3.1.2 3968 + compressible: 2.0.18 3969 + debug: 2.6.9 3970 + negotiator: 0.6.4 3971 + on-headers: 1.0.2 3972 + safe-buffer: 5.2.1 3973 + vary: 1.1.2 3974 + transitivePeerDependencies: 3975 + - supports-color 3922 3976 3923 3977 concat-map@0.0.1: {} 3924 3978 ··· 4604 4658 4605 4659 negotiator@0.6.3: {} 4606 4660 4661 + negotiator@0.6.4: {} 4662 + 4607 4663 node-abi@3.74.0: 4608 4664 dependencies: 4609 4665 semver: 7.7.1 ··· 4626 4682 on-finished@2.4.1: 4627 4683 dependencies: 4628 4684 ee-first: 1.1.1 4685 + 4686 + on-headers@1.0.2: {} 4629 4687 4630 4688 once@1.4.0: 4631 4689 dependencies: