+27
.tangled/workflows/deploy.yml
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
+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
+2
packages/appview/src/ingestors/index.ts
+223
packages/appview/src/ingestors/jetstream.ts
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
packages/appview/src/lib/env.ts
+27
-3
packages/appview/src/lib/hydrate.ts
+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
-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
+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
+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
+1
packages/client/package.json
+10
-3
packages/client/src/components/Header.tsx
+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
+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
+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
+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
+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
+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
+1
-1
packages/client/src/pages/OAuthCallbackPage.tsx
+31
-105
packages/client/src/services/api.ts
+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
+1
-1
packages/client/vite.config.ts
+8
packages/lexicon/package.json
+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
+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
+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
+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
+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
+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
+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: