Scratch space for learning atproto app development

Align code with guide

Changed files
+39 -46
src
+6 -6
src/ingester.ts
···
export function createIngester(db: Database) {
const logger = pino({ name: 'firehose', level: env.LOG_LEVEL })
return new Firehose({
-
service: env.FIREHOSE_URL,
-
idResolver: new IdResolver({
-
plcUrl: env.PLC_URL,
-
didCache: new MemoryCache(HOUR, DAY),
-
}),
handleEvent: async (evt: Event) => {
// Watch for write events
if (evt.event === 'create' || evt.event === 'update') {
···
onError: (err: unknown) => {
logger.error({ err }, 'error on firehose ingestion')
},
-
filterCollections: ['xyz.statusphere.status'],
excludeIdentity: true,
excludeAccount: true,
})
}
···
export function createIngester(db: Database) {
const logger = pino({ name: 'firehose', level: env.LOG_LEVEL })
return new Firehose({
+
filterCollections: ['xyz.statusphere.status'],
handleEvent: async (evt: Event) => {
// Watch for write events
if (evt.event === 'create' || evt.event === 'update') {
···
onError: (err: unknown) => {
logger.error({ err }, 'error on firehose ingestion')
},
excludeIdentity: true,
excludeAccount: true,
+
service: env.FIREHOSE_URL,
+
idResolver: new IdResolver({
+
plcUrl: env.PLC_URL,
+
didCache: new MemoryCache(HOUR, DAY),
+
}),
})
}
+5 -4
src/lib/http.ts
···
import { createHttpTerminator } from 'http-terminator'
import { once } from 'node:events'
import type {
···
export type Middleware<
Req extends IncomingMessage = IncomingMessage,
-
Res extends ServerResponse<Req> = ServerResponse<Req>,
> = (req: Req, res: Res, next: NextFunction) => void
export type Handler<
Req extends IncomingMessage = IncomingMessage,
-
Res extends ServerResponse<Req> = ServerResponse<Req>,
> = (req: Req, res: Res) => unknown | Promise<unknown>
/**
* Wraps a request handler middleware to ensure that `next` is called if it
* throws or returns a promise that rejects.
*/
export function handler<
-
Req extends IncomingMessage = IncomingMessage,
-
Res extends ServerResponse<Req> = ServerResponse<Req>,
>(fn: Handler<Req, Res>): Middleware<Req, Res> {
return async (req, res, next) => {
try {
···
+
import { Request, Response } from 'express'
import { createHttpTerminator } from 'http-terminator'
import { once } from 'node:events'
import type {
···
export type Middleware<
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse = ServerResponse,
> = (req: Req, res: Res, next: NextFunction) => void
export type Handler<
Req extends IncomingMessage = IncomingMessage,
+
Res extends ServerResponse = ServerResponse,
> = (req: Req, res: Res) => unknown | Promise<unknown>
/**
* Wraps a request handler middleware to ensure that `next` is called if it
* throws or returns a promise that rejects.
*/
export function handler<
+
Req extends IncomingMessage = Request,
+
Res extends ServerResponse = Response,
>(fn: Handler<Req, Res>): Middleware<Req, Res> {
return async (req, res, next) => {
try {
+28 -36
src/routes.ts
···
res: ServerResponse,
ctx: AppContext,
) {
const session = await getIronSession<Session>(req, res, {
cookieName: 'sid',
password: env.COOKIE_SECRET,
})
if (!session.did) return null
try {
const oauthSession = await ctx.oauthClient.restore(session.did)
return oauthSession ? new Agent(oauthSession) : null
···
// OAuth metadata
router.get(
'/oauth-client-metadata.json',
-
handler((req: Request, res: Response) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
res.json(ctx.oauthClient.clientMetadata)
}),
···
// Public keys
router.get(
'/.well-known/jwks.json',
-
handler((req: Request, res: Response) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
res.json(ctx.oauthClient.jwks)
}),
···
// OAuth callback to complete session creation
router.get(
'/oauth/callback',
-
handler(async (req: Request, res: Response) => {
res.setHeader('cache-control', 'no-store')
const params = new URLSearchParams(req.originalUrl.split('?')[1])
···
// Login page
router.get(
'/login',
-
handler(async (req: Request, res: Response) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
-
-
return res.type('html').send(page(login({})))
}),
)
···
router.post(
'/login',
express.urlencoded(),
-
handler(async (req: Request, res: Response) => {
res.setHeader('cache-control', 'no-store')
-
const input = ifString(req.body.input)
-
-
// Validate
-
if (!input) {
-
return res.type('html').send(page(login({ error: 'invalid input' })))
-
}
-
-
// @NOTE "input" can be a handle, a DID or a service URL (PDS).
-
// Initiate the OAuth flow
try {
const url = await ctx.oauthClient.authorize(input, {
scope: 'atproto transition:generic',
})
res.redirect(url.toString())
} catch (err) {
ctx.logger.error({ err }, 'oauth authorize failed')
-
const error =
-
err instanceof OAuthResolverError
-
? err.message
-
: "couldn't initiate login"
return res.type('html').send(page(login({ error })))
}
···
// Signup
router.get(
'/signup',
-
handler(async (req: Request, res: Response) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
try {
···
// Logout handler
router.post(
'/logout',
-
handler(async (req: Request, res: Response) => {
// Never store this route
res.setHeader('cache-control', 'no-store')
···
// Homepage
router.get(
'/',
-
handler(async (req: Request, res: Response) => {
-
// Prevent caching of this page when the credentials change
-
res.setHeader('Vary', 'Cookie')
-
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
···
return res.type('html').send(page(home({ statuses, didHandleMap })))
}
-
// Make sure this page does not get cached in public caches (proxies)
-
res.setHeader('cache-control', 'private')
-
// Fetch additional information about the logged-in user
const profileResponse = await agent.com.atproto.repo
.getRecord({
···
router.post(
'/status',
express.urlencoded(),
-
handler(async (req: Request, res: Response) => {
-
// Never store this route
-
res.setHeader('cache-control', 'no-store')
-
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
if (!agent) {
···
.send('<h1>Error: Session required</h1>')
}
-
// Construct & validate their status record
-
const rkey = TID.nextStr()
const record = {
$type: 'xyz.statusphere.status',
status: req.body?.status,
···
const res = await agent.com.atproto.repo.putRecord({
repo: agent.assertDid,
collection: 'xyz.statusphere.status',
-
rkey,
record,
validate: false,
})
···
res: ServerResponse,
ctx: AppContext,
) {
+
res.setHeader('Vary', 'Cookie')
+
const session = await getIronSession<Session>(req, res, {
cookieName: 'sid',
password: env.COOKIE_SECRET,
})
if (!session.did) return null
+
+
// This page is dynamic and should not be cached publicly
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, private`)
+
try {
const oauthSession = await ctx.oauthClient.restore(session.did)
return oauthSession ? new Agent(oauthSession) : null
···
// OAuth metadata
router.get(
'/oauth-client-metadata.json',
+
handler((req, res) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
res.json(ctx.oauthClient.clientMetadata)
}),
···
// Public keys
router.get(
'/.well-known/jwks.json',
+
handler((req, res) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
res.json(ctx.oauthClient.jwks)
}),
···
// OAuth callback to complete session creation
router.get(
'/oauth/callback',
+
handler(async (req, res) => {
res.setHeader('cache-control', 'no-store')
const params = new URLSearchParams(req.originalUrl.split('?')[1])
···
// Login page
router.get(
'/login',
+
handler(async (req, res) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
+
res.type('html').send(page(login({})))
}),
)
···
router.post(
'/login',
express.urlencoded(),
+
handler(async (req, res) => {
+
// Never store this route
res.setHeader('cache-control', 'no-store')
// Initiate the OAuth flow
try {
+
// Validate input: can be a handle, a DID or a service URL (PDS).
+
const input = ifString(req.body.input)
+
if (!input) {
+
throw new Error('Invalid input')
+
}
+
+
// Initiate the OAuth flow
const url = await ctx.oauthClient.authorize(input, {
scope: 'atproto transition:generic',
})
+
res.redirect(url.toString())
} catch (err) {
ctx.logger.error({ err }, 'oauth authorize failed')
+
const error = err instanceof Error ? err.message : 'unexpected error'
return res.type('html').send(page(login({ error })))
}
···
// Signup
router.get(
'/signup',
+
handler(async (req, res) => {
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
try {
···
// Logout handler
router.post(
'/logout',
+
handler(async (req, res) => {
// Never store this route
res.setHeader('cache-control', 'no-store')
···
// Homepage
router.get(
'/',
+
handler(async (req, res) => {
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
···
return res.type('html').send(page(home({ statuses, didHandleMap })))
}
// Fetch additional information about the logged-in user
const profileResponse = await agent.com.atproto.repo
.getRecord({
···
router.post(
'/status',
express.urlencoded(),
+
handler(async (req, res) => {
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
if (!agent) {
···
.send('<h1>Error: Session required</h1>')
}
+
// Construct their status record
const record = {
$type: 'xyz.statusphere.status',
status: req.body?.status,
···
const res = await agent.com.atproto.repo.putRecord({
repo: agent.assertDid,
collection: 'xyz.statusphere.status',
+
rkey: TID.nextStr(),
record,
validate: false,
})