diff --git a/README.md b/README.md index 427ee13..9073f75 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ For production deployment: The backend server will: -- Serve the API at `/api/*` endpoints +- Serve the API at `/xrpc/*` and `/oauth/*` endpoints - Serve the frontend static files from the client's build directory - Handle client-side routing by serving index.html for all non-API routes diff --git a/lexicons/xyz/statusphere/getStatuses.json b/lexicons/xyz/statusphere/getStatuses.json new file mode 100644 index 0000000..b2eb0a9 --- /dev/null +++ b/lexicons/xyz/statusphere/getStatuses.json @@ -0,0 +1,39 @@ +{ + "lexicon": 1, + "id": "xyz.statusphere.getStatuses", + "defs": { + "main": { + "type": "query", + "description": "Get a list of the most recent statuses on the network.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["statuses"], + "properties": { + "cursor": { "type": "string" }, + "statuses": { + "type": "array", + "items": { + "type": "ref", + "ref": "xyz.statusphere.defs#statusView" + } + } + } + } + } + } + } +} diff --git a/lexicons/xyz/statusphere/getUser.json b/lexicons/xyz/statusphere/getUser.json new file mode 100644 index 0000000..c26bb7b --- /dev/null +++ b/lexicons/xyz/statusphere/getUser.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "xyz.statusphere.getUser", + "defs": { + "main": { + "type": "query", + "description": "Get the current user's profile and status.", + "parameters": { + "type": "params", + "properties": {} + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["profile"], + "properties": { + "profile": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + }, + "status": { + "type": "ref", + "ref": "xyz.statusphere.defs#statusView" + } + } + } + } + } + } +} diff --git a/lexicons/xyz/statusphere/sendStatus.json b/lexicons/xyz/statusphere/sendStatus.json new file mode 100644 index 0000000..c13556f --- /dev/null +++ b/lexicons/xyz/statusphere/sendStatus.json @@ -0,0 +1,38 @@ +{ + "lexicon": 1, + "id": "xyz.statusphere.sendStatus", + "defs": { + "main": { + "type": "procedure", + "description": "Send a status into the ATmosphere.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string", + "minLength": 1, + "maxGraphemes": 1, + "maxLength": 32 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "ref", + "ref": "xyz.statusphere.defs#statusView" + } + } + } + } + } + } +} diff --git a/package.json b/package.json index f08d2b6..da27859 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev:lexicon": "pnpm --filter @statusphere/lexicon dev", "dev:appview": "pnpm --filter @statusphere/appview dev", "dev:client": "pnpm --filter @statusphere/client dev", - "lexgen": "pnpm --filter @statusphere/lexicon lexgen", + "lexgen": "pnpm -r lexgen", "build": "pnpm build:lexicon && pnpm build:client && pnpm build:appview", "build:lexicon": "pnpm --filter @statusphere/lexicon build", "build:appview": "pnpm --filter @statusphere/appview build", diff --git a/packages/appview/package.json b/packages/appview/package.json index 253c9cd..bc7a6ff 100644 --- a/packages/appview/package.json +++ b/packages/appview/package.json @@ -10,6 +10,7 @@ "dev": "tsx watch --clear-screen=false src/index.ts | pino-pretty", "build": "tsup", "start": "node dist/index.js", + "lexgen": "lex gen-server ./src/lexicons ../../lexicons/xyz/statusphere/* ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* --yes && pnpm format", "clean": "rimraf dist coverage", "format": "prettier --write src", "typecheck": "tsc --noEmit" @@ -25,6 +26,7 @@ "@atproto/xrpc-server": "^0.7.11", "@statusphere/lexicon": "workspace:*", "better-sqlite3": "^11.8.1", + "compression": "^1.8.0", "cors": "^2.8.5", "dotenv": "^16.4.7", "envalid": "^8.0.0", @@ -35,7 +37,9 @@ "pino": "^9.6.0" }, "devDependencies": { + "@atproto/lex-cli": "^0.6.1", "@types/better-sqlite3": "^7.6.12", + "@types/compression": "^1.7.5", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.13.8", diff --git a/packages/appview/src/api/health.ts b/packages/appview/src/api/health.ts new file mode 100644 index 0000000..89a0391 --- /dev/null +++ b/packages/appview/src/api/health.ts @@ -0,0 +1,13 @@ +import { Router } from 'express' + +import { AppContext } from '#/context' + +export const createRouter = (ctx: AppContext) => { + const router = Router() + + router.get('/health', async function (req, res) { + res.status(200).send('OK') + }) + + return router +} diff --git a/packages/appview/src/api/index.ts b/packages/appview/src/api/index.ts new file mode 100644 index 0000000..cf712a4 --- /dev/null +++ b/packages/appview/src/api/index.ts @@ -0,0 +1,15 @@ +import { AppContext } from '#/context' +import { Server } from '#/lexicons' +import getStatuses from './lexicons/getStatuses' +import getUser from './lexicons/getUser' +import sendStatus from './lexicons/sendStatus' + +export * as health from './health' +export * as oauth from './oauth' + +export default function (server: Server, ctx: AppContext) { + getStatuses(server, ctx) + sendStatus(server, ctx) + getUser(server, ctx) + return server +} diff --git a/packages/appview/src/api/lexicons/getStatuses.ts b/packages/appview/src/api/lexicons/getStatuses.ts new file mode 100644 index 0000000..e079c51 --- /dev/null +++ b/packages/appview/src/api/lexicons/getStatuses.ts @@ -0,0 +1,26 @@ +import { AppContext } from '#/context' +import { Server } from '#/lexicons' +import { statusToStatusView } from '#/lib/hydrate' + +export default function (server: Server, ctx: AppContext) { + server.xyz.statusphere.getStatuses({ + handler: async ({ params }) => { + // Fetch data stored in our SQLite + const statuses = await ctx.db + .selectFrom('status') + .selectAll() + .orderBy('indexedAt', 'desc') + .limit(params.limit) + .execute() + + return { + encoding: 'application/json', + body: { + statuses: await Promise.all( + statuses.map((status) => statusToStatusView(status, ctx)), + ), + }, + } + }, + }) +} diff --git a/packages/appview/src/api/lexicons/getUser.ts b/packages/appview/src/api/lexicons/getUser.ts new file mode 100644 index 0000000..d65b749 --- /dev/null +++ b/packages/appview/src/api/lexicons/getUser.ts @@ -0,0 +1,61 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' +import { AppBskyActorProfile } from '@statusphere/lexicon' + +import { AppContext } from '#/context' +import { Server } from '#/lexicons' +import { bskyProfileToProfileView, statusToStatusView } from '#/lib/hydrate' +import { getSessionAgent } from '#/session' + +export default function (server: Server, ctx: AppContext) { + server.xyz.statusphere.getUser({ + handler: async ({ req, res }) => { + const agent = await getSessionAgent(req, res, ctx) + if (!agent) { + throw new AuthRequiredError('Authentication required') + } + + const did = agent.assertDid + + const profileResponse = await agent.com.atproto.repo + .getRecord({ + repo: did, + collection: 'app.bsky.actor.profile', + rkey: 'self', + }) + .catch(() => undefined) + + const profileRecord = profileResponse?.data + let profile: AppBskyActorProfile.Record = {} as AppBskyActorProfile.Record + + if (profileRecord && AppBskyActorProfile.isRecord(profileRecord.value)) { + const validated = AppBskyActorProfile.validateRecord( + profileRecord.value, + ) + if (validated.success) { + profile = profileRecord.value + } else { + ctx.logger.error( + { err: validated.error }, + 'Failed to validate user profile', + ) + } + } + + // Fetch user status + const status = await ctx.db + .selectFrom('status') + .selectAll() + .where('authorDid', '=', did) + .orderBy('indexedAt', 'desc') + .executeTakeFirst() + + return { + encoding: 'application/json', + body: { + profile: await bskyProfileToProfileView(did, profile, ctx), + status: status ? await statusToStatusView(status, ctx) : undefined, + }, + } + }, + }) +} diff --git a/packages/appview/src/api/lexicons/sendStatus.ts b/packages/appview/src/api/lexicons/sendStatus.ts new file mode 100644 index 0000000..342cb72 --- /dev/null +++ b/packages/appview/src/api/lexicons/sendStatus.ts @@ -0,0 +1,79 @@ +import { TID } from '@atproto/common' +import { + AuthRequiredError, + InvalidRequestError, + UpstreamFailureError, +} from '@atproto/xrpc-server' +import { XyzStatusphereStatus } from '@statusphere/lexicon' + +import { AppContext } from '#/context' +import { Server } from '#/lexicons' +import { statusToStatusView } from '#/lib/hydrate' +import { getSessionAgent } from '#/session' + +export default function (server: Server, ctx: AppContext) { + server.xyz.statusphere.sendStatus({ + handler: async ({ input, req, res }) => { + const agent = await getSessionAgent(req, res, ctx) + if (!agent) { + throw new AuthRequiredError('Authentication required') + } + + // Construct & validate their status record + const rkey = TID.nextStr() + const record = { + $type: 'xyz.statusphere.status', + status: input.body.status, + createdAt: new Date().toISOString(), + } + + const validation = XyzStatusphereStatus.validateRecord(record) + if (!validation.success) { + throw new InvalidRequestError('Invalid status') + } + + let uri + try { + // Write the status record to the user's repository + const response = await agent.com.atproto.repo.putRecord({ + repo: agent.assertDid, + collection: 'xyz.statusphere.status', + rkey, + record: validation.value, + validate: false, + }) + uri = response.data.uri + } catch (err) { + throw new UpstreamFailureError('Failed to write record') + } + + const optimisticStatus = { + uri, + authorDid: agent.assertDid, + status: record.status, + createdAt: record.createdAt, + indexedAt: new Date().toISOString(), + } + + try { + // Optimistically update our SQLite + // This isn't strictly necessary because the write event will be + // handled in #/firehose/ingestor.ts, but it ensures that future reads + // will be up-to-date after this method finishes. + await ctx.db.insertInto('status').values(optimisticStatus).execute() + } catch (err) { + ctx.logger.warn( + { err }, + 'failed to update computed view; ignoring as it should be caught by the firehose', + ) + } + + return { + encoding: 'application/json', + body: { + status: await statusToStatusView(optimisticStatus, ctx), + }, + } + }, + }) +} diff --git a/packages/appview/src/api/oauth.ts b/packages/appview/src/api/oauth.ts new file mode 100644 index 0000000..32bbad1 --- /dev/null +++ b/packages/appview/src/api/oauth.ts @@ -0,0 +1,83 @@ +import { OAuthResolverError } from '@atproto/oauth-client-node' +import { isValidHandle } from '@atproto/syntax' +import express from 'express' + +import { AppContext } from '#/context' +import { getSession } from '#/session' + +export const createRouter = (ctx: AppContext) => { + const router = express.Router() + + // OAuth metadata + router.get('/client-metadata.json', (_req, res) => { + res.json(ctx.oauthClient.clientMetadata) + }) + + // OAuth callback to complete session creation + router.get('/oauth/callback', async (req, res) => { + // Get the query parameters from the URL + const params = new URLSearchParams(req.originalUrl.split('?')[1]) + + try { + const { session } = await ctx.oauthClient.callback(params) + + // Use the common session options + const clientSession = await getSession(req, res) + + // Set the DID on the session + clientSession.did = session.did + await clientSession.save() + + // Get the origin and determine appropriate redirect + const host = req.get('host') || '' + const protocol = req.protocol || 'http' + const baseUrl = `${protocol}://${host}` + + ctx.logger.info( + `OAuth callback successful, redirecting to ${baseUrl}/oauth-callback`, + ) + + // Redirect to the frontend oauth-callback page + res.redirect('/oauth-callback') + } catch (err) { + ctx.logger.error({ err }, 'oauth callback failed') + + // Handle error redirect - stay on same domain + res.redirect('/oauth-callback?error=auth') + } + }) + + // Login handler + router.post('/oauth/initiate', async (req, res) => { + // Validate + const handle = req.body?.handle + if (typeof handle !== 'string' || !isValidHandle(handle)) { + res.status(400).json({ error: 'Invalid handle' }) + return + } + + // Initiate the OAuth flow + try { + const url = await ctx.oauthClient.authorize(handle, { + scope: 'atproto transition:generic', + }) + res.json({ redirectUrl: url.toString() }) + } catch (err) { + ctx.logger.error({ err }, 'oauth authorize failed') + const errorMsg = + err instanceof OAuthResolverError + ? err.message + : "Couldn't initiate login" + res.status(500).json({ error: errorMsg }) + } + }) + + // Logout handler + router.post('/oauth/logout', async (req, res) => { + const session = await getSession(req, res) + session.destroy() + res.json({ success: true }) + }) + + return router +} diff --git a/packages/appview/src/auth/client.ts b/packages/appview/src/auth/client.ts index e41c2e7..f11eecc 100644 --- a/packages/appview/src/auth/client.ts +++ b/packages/appview/src/auth/client.ts @@ -17,10 +17,10 @@ export const createClient = async (db: Database) => { clientMetadata: { client_name: 'Statusphere React App', client_id: publicUrl - ? `${url}/api/client-metadata.json` - : `http://localhost?redirect_uri=${enc(`${url}/api/oauth/callback`)}&scope=${enc('atproto transition:generic')}`, + ? `${url}/client-metadata.json` + : `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc('atproto transition:generic')}`, client_uri: url, - redirect_uris: [`${url}/api/oauth/callback`], + redirect_uris: [`${url}/oauth/callback`], scope: 'atproto transition:generic', grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], diff --git a/packages/appview/src/context.ts b/packages/appview/src/context.ts new file mode 100644 index 0000000..8094bb2 --- /dev/null +++ b/packages/appview/src/context.ts @@ -0,0 +1,15 @@ +import { OAuthClient } from '@atproto/oauth-client-node' +import { Firehose } from '@atproto/sync' +import pino from 'pino' + +import { Database } from './db' +import { BidirectionalResolver } from './id-resolver' + +// Application state passed to the router and elsewhere +export type AppContext = { + db: Database + ingester: Firehose + logger: pino.Logger + oauthClient: OAuthClient + resolver: BidirectionalResolver +} diff --git a/packages/appview/src/error.ts b/packages/appview/src/error.ts new file mode 100644 index 0000000..c8f0e35 --- /dev/null +++ b/packages/appview/src/error.ts @@ -0,0 +1,14 @@ +import { XRPCError } from '@atproto/xrpc-server' +import { ErrorRequestHandler } from 'express' + +import { AppContext } from '#/context' + +export const createHandler: (ctx: AppContext) => ErrorRequestHandler = + (ctx) => (err, _req, res, next) => { + ctx.logger.error('unexpected internal server error', err) + if (res.headersSent) { + return next(err) + } + const serverError = XRPCError.fromError(err) + res.status(serverError.type).json(serverError.payload) + } diff --git a/packages/appview/src/index.ts b/packages/appview/src/index.ts index 55d25fb..fc574e2 100644 --- a/packages/appview/src/index.ts +++ b/packages/appview/src/index.ts @@ -2,32 +2,21 @@ import events from 'node:events' import fs from 'node:fs' import type http from 'node:http' import path from 'node:path' -import type { OAuthClient } from '@atproto/oauth-client-node' -import { Firehose } from '@atproto/sync' +import { DAY, SECOND } from '@atproto/common' +import compression from 'compression' import cors from 'cors' -import express, { type Express } from 'express' +import express from 'express' import { pino } from 'pino' +import API, { health, oauth } from '#/api' import { createClient } from '#/auth/client' +import { AppContext } from '#/context' import { createDb, migrateToLatest } from '#/db' -import type { Database } from '#/db' -import { - BidirectionalResolver, - createBidirectionalResolver, - createIdResolver, -} from '#/id-resolver' +import * as error from '#/error' +import { createBidirectionalResolver, createIdResolver } from '#/id-resolver' import { createIngester } from '#/ingester' +import { createServer } from '#/lexicons' import { env } from '#/lib/env' -import { createRouter } from '#/routes' - -// Application state passed to the router and elsewhere -export type AppContext = { - db: Database - ingester: Firehose - logger: pino.Logger - oauthClient: OAuthClient - resolver: BidirectionalResolver -} export class Server { constructor( @@ -60,55 +49,29 @@ export class Server { // Subscribe to events on the firehose ingester.start() - // Create our server - const app: Express = express() - app.set('trust proxy', true) - - // CORS configuration based on environment - if (env.NODE_ENV === 'development') { - // In development, allow multiple origins including ngrok - app.use( - cors({ - origin: function (origin, callback) { - // Allow requests with no origin (like mobile apps, curl) - if (!origin) return callback(null, true) - - // List of allowed origins - const allowedOrigins = [ - 'http://localhost:3000', // Standard React port - 'http://127.0.0.1:3000', // Alternative React address - ] - - // Check if the request origin is in our allowed list or is an ngrok domain - if (allowedOrigins.indexOf(origin) !== -1) { - callback(null, true) - } else { - console.warn(`⚠️ CORS blocked origin: ${origin}`) - callback(null, false) - } - }, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], - }), - ) - } else { - // In production, CORS is not needed if frontend and API are on same domain - // But we'll still enable it for flexibility with minimal configuration - app.use( - cors({ - origin: true, // Use req.origin, which means same-origin requests will always be allowed - credentials: true, - }), - ) - } - - // Routes & middlewares - const router = createRouter(ctx) + const app = express() + app.use(cors({ maxAge: DAY / SECOND })) + app.use(compression()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) - app.use('/api', router) + // Create our server + let server = createServer({ + validateResponse: env.isDevelopment, + payload: { + jsonLimit: 100 * 1024, // 100kb + textLimit: 100 * 1024, // 100kb + // no blobs + blobLimit: 0, + }, + }) + + server = API(server, ctx) + + app.use(health.createRouter(ctx)) + app.use(oauth.createRouter(ctx)) + app.use(server.xrpc.router) + app.use(error.createHandler(ctx)) // Serve static files from the frontend build - prod only if (env.isProduction) { @@ -124,15 +87,10 @@ export class Server { // Serve static files app.use(express.static(frontendPath)) - // Heathcheck - app.get('/health', (req, res) => { - res.status(200).json({ status: 'ok' }) - }) - // For any other requests, send the index.html file app.get('*', (req, res) => { // Only handle non-API paths - if (!req.path.startsWith('/api/')) { + if (!req.path.startsWith('/xrpc/')) { res.sendFile(path.join(frontendPath, 'index.html')) } else { res.status(404).json({ error: 'API endpoint not found' }) @@ -144,16 +102,18 @@ export class Server { res.sendStatus(404) }) } + } else { + server.xrpc.router.set('trust proxy', true) } // Use the port from env (should be 3001 for the API server) - const server = app.listen(env.PORT) - await events.once(server, 'listening') + const httpServer = app.listen(env.PORT) + await events.once(httpServer, 'listening') logger.info( `API Server (${NODE_ENV}) running on port http://${HOST}:${env.PORT}`, ) - return new Server(app, server, ctx) + return new Server(app, httpServer, ctx) } async close() { diff --git a/packages/appview/src/lexicons/index.ts b/packages/appview/src/lexicons/index.ts new file mode 100644 index 0000000..862b1a4 --- /dev/null +++ b/packages/appview/src/lexicons/index.ts @@ -0,0 +1,286 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { + AuthVerifier, + createServer as createXrpcServer, + StreamAuthVerifier, + Options as XrpcOptions, + Server as XrpcServer, +} from '@atproto/xrpc-server' + +import { schemas } from './lexicons.js' +import * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js' +import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js' +import * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js' +import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo.js' +import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord.js' +import * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo.js' +import * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs.js' +import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords.js' +import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord.js' +import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js' +import * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js' +import * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js' +import * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js' + +export function createServer(options?: XrpcOptions): Server { + return new Server(options) +} + +export class Server { + xrpc: XrpcServer + xyz: XyzNS + com: ComNS + app: AppNS + + constructor(options?: XrpcOptions) { + this.xrpc = createXrpcServer(schemas, options) + this.xyz = new XyzNS(this) + this.com = new ComNS(this) + this.app = new AppNS(this) + } +} + +export class XyzNS { + _server: Server + statusphere: XyzStatusphereNS + + constructor(server: Server) { + this._server = server + this.statusphere = new XyzStatusphereNS(server) + } +} + +export class XyzStatusphereNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getStatuses( + cfg: ConfigOf< + AV, + XyzStatusphereGetStatuses.Handler>, + XyzStatusphereGetStatuses.HandlerReqCtx> + >, + ) { + const nsid = 'xyz.statusphere.getStatuses' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getUser( + cfg: ConfigOf< + AV, + XyzStatusphereGetUser.Handler>, + XyzStatusphereGetUser.HandlerReqCtx> + >, + ) { + const nsid = 'xyz.statusphere.getUser' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + sendStatus( + cfg: ConfigOf< + AV, + XyzStatusphereSendStatus.Handler>, + XyzStatusphereSendStatus.HandlerReqCtx> + >, + ) { + const nsid = 'xyz.statusphere.sendStatus' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class ComNS { + _server: Server + atproto: ComAtprotoNS + + constructor(server: Server) { + this._server = server + this.atproto = new ComAtprotoNS(server) + } +} + +export class ComAtprotoNS { + _server: Server + repo: ComAtprotoRepoNS + + constructor(server: Server) { + this._server = server + this.repo = new ComAtprotoRepoNS(server) + } +} + +export class ComAtprotoRepoNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + applyWrites( + cfg: ConfigOf< + AV, + ComAtprotoRepoApplyWrites.Handler>, + ComAtprotoRepoApplyWrites.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.applyWrites' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + createRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoCreateRecord.Handler>, + ComAtprotoRepoCreateRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.createRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoDeleteRecord.Handler>, + ComAtprotoRepoDeleteRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.deleteRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + describeRepo( + cfg: ConfigOf< + AV, + ComAtprotoRepoDescribeRepo.Handler>, + ComAtprotoRepoDescribeRepo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.describeRepo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoGetRecord.Handler>, + ComAtprotoRepoGetRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.getRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + importRepo( + cfg: ConfigOf< + AV, + ComAtprotoRepoImportRepo.Handler>, + ComAtprotoRepoImportRepo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.importRepo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listMissingBlobs( + cfg: ConfigOf< + AV, + ComAtprotoRepoListMissingBlobs.Handler>, + ComAtprotoRepoListMissingBlobs.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.listMissingBlobs' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listRecords( + cfg: ConfigOf< + AV, + ComAtprotoRepoListRecords.Handler>, + ComAtprotoRepoListRecords.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.listRecords' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + putRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoPutRecord.Handler>, + ComAtprotoRepoPutRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.putRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + uploadBlob( + cfg: ConfigOf< + AV, + ComAtprotoRepoUploadBlob.Handler>, + ComAtprotoRepoUploadBlob.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class AppNS { + _server: Server + bsky: AppBskyNS + + constructor(server: Server) { + this._server = server + this.bsky = new AppBskyNS(server) + } +} + +export class AppBskyNS { + _server: Server + actor: AppBskyActorNS + + constructor(server: Server) { + this._server = server + this.actor = new AppBskyActorNS(server) + } +} + +export class AppBskyActorNS { + _server: Server + + constructor(server: Server) { + this._server = server + } +} + +type SharedRateLimitOpts = { + name: string + calcKey?: (ctx: T) => string | null + calcPoints?: (ctx: T) => number +} +type RouteRateLimitOpts = { + durationMs: number + points: number + calcKey?: (ctx: T) => string | null + calcPoints?: (ctx: T) => number +} +type HandlerOpts = { blobLimit?: number } +type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts +type ConfigOf = + | Handler + | { + auth?: Auth + opts?: HandlerOpts + rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] + handler: Handler + } +type ExtractAuth = Extract< + Awaited>, + { credentials: unknown } +> diff --git a/packages/appview/src/lexicons/lexicons.ts b/packages/appview/src/lexicons/lexicons.ts new file mode 100644 index 0000000..2831960 --- /dev/null +++ b/packages/appview/src/lexicons/lexicons.ts @@ -0,0 +1,1303 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { + LexiconDoc, + Lexicons, + ValidationError, + ValidationResult, +} from '@atproto/lexicon' + +import { $Typed, is$typed, maybe$typed } from './util.js' + +export const schemaDict = { + XyzStatusphereDefs: { + lexicon: 1, + id: 'xyz.statusphere.defs', + defs: { + statusView: { + type: 'object', + required: ['uri', 'status', 'profile', 'createdAt'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + status: { + type: 'string', + minLength: 1, + maxGraphemes: 1, + maxLength: 32, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + profile: { + type: 'ref', + ref: 'lex:xyz.statusphere.defs#profileView', + }, + }, + }, + profileView: { + type: 'object', + required: ['did', 'handle'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + }, + }, + }, + }, + XyzStatusphereGetStatuses: { + lexicon: 1, + id: 'xyz.statusphere.getStatuses', + defs: { + main: { + type: 'query', + description: 'Get a list of the most recent statuses on the network.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['statuses'], + properties: { + cursor: { + type: 'string', + }, + statuses: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:xyz.statusphere.defs#statusView', + }, + }, + }, + }, + }, + }, + }, + }, + XyzStatusphereGetUser: { + lexicon: 1, + id: 'xyz.statusphere.getUser', + defs: { + main: { + type: 'query', + description: "Get the current user's profile and status.", + parameters: { + type: 'params', + properties: {}, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['profile'], + properties: { + profile: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + status: { + type: 'ref', + ref: 'lex:xyz.statusphere.defs#statusView', + }, + }, + }, + }, + }, + }, + }, + XyzStatusphereSendStatus: { + lexicon: 1, + id: 'xyz.statusphere.sendStatus', + defs: { + main: { + type: 'procedure', + description: 'Send a status into the ATmosphere.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + minLength: 1, + maxGraphemes: 1, + maxLength: 32, + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'ref', + ref: 'lex:xyz.statusphere.defs#statusView', + }, + }, + }, + }, + }, + }, + }, + XyzStatusphereStatus: { + lexicon: 1, + id: 'xyz.statusphere.status', + defs: { + main: { + type: 'record', + key: 'tid', + record: { + type: 'object', + required: ['status', 'createdAt'], + properties: { + status: { + type: 'string', + minLength: 1, + maxGraphemes: 1, + maxLength: 32, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + ComAtprotoLabelDefs: { + lexicon: 1, + id: 'com.atproto.label.defs', + defs: { + label: { + type: 'object', + description: + 'Metadata tag on an atproto resource (eg, repo or record).', + required: ['src', 'uri', 'val', 'cts'], + properties: { + ver: { + type: 'integer', + description: 'The AT Protocol version of the label object.', + }, + src: { + type: 'string', + format: 'did', + description: 'DID of the actor who created this label.', + }, + uri: { + type: 'string', + format: 'uri', + description: + 'AT URI of the record, repository (account), or other resource that this label applies to.', + }, + cid: { + type: 'string', + format: 'cid', + description: + "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", + }, + val: { + type: 'string', + maxLength: 128, + description: + 'The short string name of the value or type of this label.', + }, + neg: { + type: 'boolean', + description: + 'If true, this is a negation label, overwriting a previous label.', + }, + cts: { + type: 'string', + format: 'datetime', + description: 'Timestamp when this label was created.', + }, + exp: { + type: 'string', + format: 'datetime', + description: + 'Timestamp at which this label expires (no longer applies).', + }, + sig: { + type: 'bytes', + description: 'Signature of dag-cbor encoded label.', + }, + }, + }, + selfLabels: { + type: 'object', + description: + 'Metadata tags on an atproto record, published by the author within the record.', + required: ['values'], + properties: { + values: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#selfLabel', + }, + maxLength: 10, + }, + }, + }, + selfLabel: { + type: 'object', + description: + 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.', + required: ['val'], + properties: { + val: { + type: 'string', + maxLength: 128, + description: + 'The short string name of the value or type of this label.', + }, + }, + }, + labelValueDefinition: { + type: 'object', + description: + 'Declares a label value and its expected interpretations and behaviors.', + required: ['identifier', 'severity', 'blurs', 'locales'], + properties: { + identifier: { + type: 'string', + description: + "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", + maxLength: 100, + maxGraphemes: 100, + }, + severity: { + type: 'string', + description: + "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", + knownValues: ['inform', 'alert', 'none'], + }, + blurs: { + type: 'string', + description: + "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", + knownValues: ['content', 'media', 'none'], + }, + defaultSetting: { + type: 'string', + description: 'The default setting for this label.', + knownValues: ['ignore', 'warn', 'hide'], + default: 'warn', + }, + adultOnly: { + type: 'boolean', + description: + 'Does the user need to have adult content enabled in order to configure this label?', + }, + locales: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings', + }, + }, + }, + }, + labelValueDefinitionStrings: { + type: 'object', + description: + 'Strings which describe the label in the UI, localized into a specific language.', + required: ['lang', 'name', 'description'], + properties: { + lang: { + type: 'string', + description: + 'The code of the language these strings are written in.', + format: 'language', + }, + name: { + type: 'string', + description: 'A short human-readable name for the label.', + maxGraphemes: 64, + maxLength: 640, + }, + description: { + type: 'string', + description: + 'A longer description of what the label means and why it might be applied.', + maxGraphemes: 10000, + maxLength: 100000, + }, + }, + }, + labelValue: { + type: 'string', + knownValues: [ + '!hide', + '!no-promote', + '!warn', + '!no-unauthenticated', + 'dmca-violation', + 'doxxing', + 'porn', + 'sexual', + 'nudity', + 'nsfl', + 'gore', + ], + }, + }, + }, + ComAtprotoRepoApplyWrites: { + lexicon: 1, + id: 'com.atproto.repo.applyWrites', + defs: { + main: { + type: 'procedure', + description: + 'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'writes'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: + 'The handle or DID of the repo (aka, current account).', + }, + validate: { + type: 'boolean', + description: + "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.", + }, + writes: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:com.atproto.repo.applyWrites#create', + 'lex:com.atproto.repo.applyWrites#update', + 'lex:com.atproto.repo.applyWrites#delete', + ], + closed: true, + }, + }, + swapCommit: { + type: 'string', + description: + 'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.', + format: 'cid', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [], + properties: { + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + results: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:com.atproto.repo.applyWrites#createResult', + 'lex:com.atproto.repo.applyWrites#updateResult', + 'lex:com.atproto.repo.applyWrites#deleteResult', + ], + closed: true, + }, + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + description: + "Indicates that the 'swapCommit' parameter did not match current commit.", + }, + ], + }, + create: { + type: 'object', + description: 'Operation which creates a new record.', + required: ['collection', 'value'], + properties: { + collection: { + type: 'string', + format: 'nsid', + }, + rkey: { + type: 'string', + maxLength: 512, + format: 'record-key', + description: + 'NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.', + }, + value: { + type: 'unknown', + }, + }, + }, + update: { + type: 'object', + description: 'Operation which updates an existing record.', + required: ['collection', 'rkey', 'value'], + properties: { + collection: { + type: 'string', + format: 'nsid', + }, + rkey: { + type: 'string', + format: 'record-key', + }, + value: { + type: 'unknown', + }, + }, + }, + delete: { + type: 'object', + description: 'Operation which deletes an existing record.', + required: ['collection', 'rkey'], + properties: { + collection: { + type: 'string', + format: 'nsid', + }, + rkey: { + type: 'string', + format: 'record-key', + }, + }, + }, + createResult: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + updateResult: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + deleteResult: { + type: 'object', + required: [], + properties: {}, + }, + }, + }, + ComAtprotoRepoCreateRecord: { + lexicon: 1, + id: 'com.atproto.repo.createRecord', + defs: { + main: { + type: 'procedure', + description: + 'Create a single new repository record. Requires auth, implemented by PDS.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'collection', 'record'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: + 'The handle or DID of the repo (aka, current account).', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + format: 'record-key', + description: 'The Record Key.', + maxLength: 512, + }, + validate: { + type: 'boolean', + description: + "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.", + }, + record: { + type: 'unknown', + description: 'The record itself. Must contain a $type field.', + }, + swapCommit: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous commit by CID.', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + description: + "Indicates that 'swapCommit' didn't match current repo commit.", + }, + ], + }, + }, + }, + ComAtprotoRepoDefs: { + lexicon: 1, + id: 'com.atproto.repo.defs', + defs: { + commitMeta: { + type: 'object', + required: ['cid', 'rev'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + rev: { + type: 'string', + format: 'tid', + }, + }, + }, + }, + }, + ComAtprotoRepoDeleteRecord: { + lexicon: 1, + id: 'com.atproto.repo.deleteRecord', + defs: { + main: { + type: 'procedure', + description: + "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'collection', 'rkey'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: + 'The handle or DID of the repo (aka, current account).', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + format: 'record-key', + description: 'The Record Key.', + }, + swapRecord: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous record by CID.', + }, + swapCommit: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous commit by CID.', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + }, + ], + }, + }, + }, + ComAtprotoRepoDescribeRepo: { + lexicon: 1, + id: 'com.atproto.repo.describeRepo', + defs: { + main: { + type: 'query', + description: + 'Get information about an account and repository, including the list of collections. Does not require auth.', + parameters: { + type: 'params', + required: ['repo'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [ + 'handle', + 'did', + 'didDoc', + 'collections', + 'handleIsCorrect', + ], + properties: { + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + didDoc: { + type: 'unknown', + description: 'The complete DID document for this account.', + }, + collections: { + type: 'array', + description: + 'List of all the collections (NSIDs) for which this repo contains at least one record.', + items: { + type: 'string', + format: 'nsid', + }, + }, + handleIsCorrect: { + type: 'boolean', + description: + 'Indicates if handle is currently valid (resolves bi-directionally)', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoRepoGetRecord: { + lexicon: 1, + id: 'com.atproto.repo.getRecord', + defs: { + main: { + type: 'query', + description: + 'Get a single record from a repository. Does not require auth.', + parameters: { + type: 'params', + required: ['repo', 'collection', 'rkey'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + description: 'The Record Key.', + format: 'record-key', + }, + cid: { + type: 'string', + format: 'cid', + description: + 'The CID of the version of the record. If not specified, then return the most recent version.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'value'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + value: { + type: 'unknown', + }, + }, + }, + }, + errors: [ + { + name: 'RecordNotFound', + }, + ], + }, + }, + }, + ComAtprotoRepoImportRepo: { + lexicon: 1, + id: 'com.atproto.repo.importRepo', + defs: { + main: { + type: 'procedure', + description: + 'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.', + input: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, + ComAtprotoRepoListMissingBlobs: { + lexicon: 1, + id: 'com.atproto.repo.listMissingBlobs', + defs: { + main: { + type: 'query', + description: + 'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['blobs'], + properties: { + cursor: { + type: 'string', + }, + blobs: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob', + }, + }, + }, + }, + }, + }, + recordBlob: { + type: 'object', + required: ['cid', 'recordUri'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + recordUri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, + ComAtprotoRepoListRecords: { + lexicon: 1, + id: 'com.atproto.repo.listRecords', + defs: { + main: { + type: 'query', + description: + 'List a range of records in a repository, matching a specific collection. Does not require auth.', + parameters: { + type: 'params', + required: ['repo', 'collection'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record type.', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + description: 'The number of records to return.', + }, + cursor: { + type: 'string', + }, + rkeyStart: { + type: 'string', + description: + 'DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)', + }, + rkeyEnd: { + type: 'string', + description: + 'DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)', + }, + reverse: { + type: 'boolean', + description: 'Flag to reverse the order of the returned records.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['records'], + properties: { + cursor: { + type: 'string', + }, + records: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.repo.listRecords#record', + }, + }, + }, + }, + }, + }, + record: { + type: 'object', + required: ['uri', 'cid', 'value'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + value: { + type: 'unknown', + }, + }, + }, + }, + }, + ComAtprotoRepoPutRecord: { + lexicon: 1, + id: 'com.atproto.repo.putRecord', + defs: { + main: { + type: 'procedure', + description: + 'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'collection', 'rkey', 'record'], + nullable: ['swapRecord'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: + 'The handle or DID of the repo (aka, current account).', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + format: 'record-key', + description: 'The Record Key.', + maxLength: 512, + }, + validate: { + type: 'boolean', + description: + "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.", + }, + record: { + type: 'unknown', + description: 'The record to write.', + }, + swapRecord: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation', + }, + swapCommit: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous commit by CID.', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + commit: { + type: 'ref', + ref: 'lex:com.atproto.repo.defs#commitMeta', + }, + validationStatus: { + type: 'string', + knownValues: ['valid', 'unknown'], + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + }, + ], + }, + }, + }, + ComAtprotoRepoStrongRef: { + lexicon: 1, + id: 'com.atproto.repo.strongRef', + description: 'A URI with a content-hash fingerprint.', + defs: { + main: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + }, + ComAtprotoRepoUploadBlob: { + lexicon: 1, + id: 'com.atproto.repo.uploadBlob', + defs: { + main: { + type: 'procedure', + description: + 'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.', + input: { + encoding: '*/*', + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['blob'], + properties: { + blob: { + type: 'blob', + }, + }, + }, + }, + }, + }, + }, + AppBskyActorDefs: { + lexicon: 1, + id: 'app.bsky.actor.defs', + defs: { + profileView: { + type: 'object', + required: ['did', 'handle'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + displayName: { + type: 'string', + maxGraphemes: 64, + maxLength: 640, + }, + description: { + type: 'string', + maxGraphemes: 256, + maxLength: 2560, + }, + avatar: { + type: 'string', + format: 'uri', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + }, + }, + AppBskyActorProfile: { + lexicon: 1, + id: 'app.bsky.actor.profile', + defs: { + main: { + type: 'record', + description: 'A declaration of a Bluesky account profile.', + key: 'literal:self', + record: { + type: 'object', + properties: { + displayName: { + type: 'string', + maxGraphemes: 64, + maxLength: 640, + }, + description: { + type: 'string', + description: 'Free-form profile description text.', + maxGraphemes: 256, + maxLength: 2560, + }, + avatar: { + type: 'blob', + description: + "Small image to be displayed next to posts from account. AKA, 'profile picture'", + accept: ['image/png', 'image/jpeg'], + maxSize: 1000000, + }, + banner: { + type: 'blob', + description: + 'Larger horizontal image to display behind profile view.', + accept: ['image/png', 'image/jpeg'], + maxSize: 1000000, + }, + labels: { + type: 'union', + description: + 'Self-label values, specific to the Bluesky application, on the overall account.', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, + joinedViaStarterPack: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + pinnedPost: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, +} as const satisfies Record + +export const schemas = Object.values(schemaDict) satisfies LexiconDoc[] +export const lexicons: Lexicons = new Lexicons(schemas) + +export function validate( + v: unknown, + id: string, + hash: string, + requiredType: true, +): ValidationResult +export function validate( + v: unknown, + id: string, + hash: string, + requiredType?: false, +): ValidationResult +export function validate( + v: unknown, + id: string, + hash: string, + requiredType?: boolean, +): ValidationResult { + return (requiredType ? is$typed : maybe$typed)(v, id, hash) + ? lexicons.validate(`${id}#${hash}`, v) + : { + success: false, + error: new ValidationError( + `Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`, + ), + } +} + +export const ids = { + XyzStatusphereDefs: 'xyz.statusphere.defs', + XyzStatusphereGetStatuses: 'xyz.statusphere.getStatuses', + XyzStatusphereGetUser: 'xyz.statusphere.getUser', + XyzStatusphereSendStatus: 'xyz.statusphere.sendStatus', + XyzStatusphereStatus: 'xyz.statusphere.status', + ComAtprotoLabelDefs: 'com.atproto.label.defs', + ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites', + ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord', + ComAtprotoRepoDefs: 'com.atproto.repo.defs', + ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', + ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', + ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', + ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo', + ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs', + ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', + ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', + ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', + ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', + AppBskyActorDefs: 'app.bsky.actor.defs', + AppBskyActorProfile: 'app.bsky.actor.profile', +} as const diff --git a/packages/appview/src/lexicons/types/app/bsky/actor/defs.ts b/packages/appview/src/lexicons/types/app/bsky/actor/defs.ts new file mode 100644 index 0000000..5f65150 --- /dev/null +++ b/packages/appview/src/lexicons/types/app/bsky/actor/defs.ts @@ -0,0 +1,35 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' +import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.bsky.actor.defs' + +export interface ProfileView { + $type?: 'app.bsky.actor.defs#profileView' + did: string + handle: string + displayName?: string + description?: string + avatar?: string + indexedAt?: string + createdAt?: string + labels?: ComAtprotoLabelDefs.Label[] +} + +const hashProfileView = 'profileView' + +export function isProfileView(v: V) { + return is$typed(v, id, hashProfileView) +} + +export function validateProfileView(v: V) { + return validate(v, id, hashProfileView) +} diff --git a/packages/appview/src/lexicons/types/app/bsky/actor/profile.ts b/packages/appview/src/lexicons/types/app/bsky/actor/profile.ts new file mode 100644 index 0000000..579bcae --- /dev/null +++ b/packages/appview/src/lexicons/types/app/bsky/actor/profile.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' +import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js' +import type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.bsky.actor.profile' + +export interface Record { + $type: 'app.bsky.actor.profile' + displayName?: string + /** Free-form profile description text. */ + description?: string + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ + avatar?: BlobRef + /** Larger horizontal image to display behind profile view. */ + banner?: BlobRef + labels?: $Typed | { $type: string } + joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main + pinnedPost?: ComAtprotoRepoStrongRef.Main + createdAt?: string + [k: string]: unknown +} + +const hashRecord = 'main' + +export function isRecord(v: V) { + return is$typed(v, id, hashRecord) +} + +export function validateRecord(v: V) { + return validate(v, id, hashRecord, true) +} diff --git a/packages/appview/src/lexicons/types/com/atproto/label/defs.ts b/packages/appview/src/lexicons/types/com/atproto/label/defs.ts new file mode 100644 index 0000000..af45f4a --- /dev/null +++ b/packages/appview/src/lexicons/types/com/atproto/label/defs.ts @@ -0,0 +1,143 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { BlobRef, ValidationResult } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' + +import { validate as _validate } from '../../../../lexicons' +import { is$typed as _is$typed, $Typed, OmitKey } from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'com.atproto.label.defs' + +/** Metadata tag on an atproto resource (eg, repo or record). */ +export interface Label { + $type?: 'com.atproto.label.defs#label' + /** The AT Protocol version of the label object. */ + ver?: number + /** DID of the actor who created this label. */ + src: string + /** AT URI of the record, repository (account), or other resource that this label applies to. */ + uri: string + /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */ + cid?: string + /** The short string name of the value or type of this label. */ + val: string + /** If true, this is a negation label, overwriting a previous label. */ + neg?: boolean + /** Timestamp when this label was created. */ + cts: string + /** Timestamp at which this label expires (no longer applies). */ + exp?: string + /** Signature of dag-cbor encoded label. */ + sig?: Uint8Array +} + +const hashLabel = 'label' + +export function isLabel(v: V) { + return is$typed(v, id, hashLabel) +} + +export function validateLabel(v: V) { + return validate