Add auth token verification, minor fixes and docker
This commit is contained in:
parent
fd8c2073f0
commit
41c2e27f5e
16 changed files with 197 additions and 116 deletions
5
.dockerignore
Normal file
5
.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
README.md
|
||||
db.sqlite*
|
||||
_data
|
||||
.vscode
|
||||
node_modules
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -173,4 +173,5 @@ _content/_data/_inbox
|
|||
_content/_data/_outbox
|
||||
_content/posts
|
||||
_site
|
||||
_data
|
||||
db.sqlite*
|
||||
|
|
38
.vscode/tasks.json
vendored
Normal file
38
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000",
|
||||
"windows":{
|
||||
"command": "C:\\Users\\death\\AppData\\Local\\Microsoft\\WinGet\\Links\\ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000"
|
||||
},
|
||||
"label": "ngrok tunnel",
|
||||
"detail": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "bun run --watch src/index.ts",
|
||||
"label": "bun run",
|
||||
"detail": "bun run --watch src/index.ts",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "docker compose up",
|
||||
"label": "docker compose up",
|
||||
"detail": "docker compose up",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
1
CNAME
1
CNAME
|
@ -1 +0,0 @@
|
|||
death.id.au
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
FROM oven/bun:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json /app
|
||||
COPY ./bun.lockb /app
|
||||
RUN bun install
|
||||
|
||||
COPY . /app
|
||||
|
||||
CMD [ "bun", "run", "/app/src/index.ts"]
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
19
docker-compose.yaml
Normal file
19
docker-compose.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: bun-activitypub
|
||||
command: bun run --watch /app/src/index.ts
|
||||
environment:
|
||||
- ACCOUNT=death.au
|
||||
- REAL_NAME=Gordon Pedersen
|
||||
- HOSTNAME=death.id.au
|
||||
- TOKEN_ENDPOINT=https://deathau-cellar-door.glitch.me/token
|
||||
- WEB_SITE_HOSTNAME=www.death.id.au
|
||||
- DATA_PATH=/data
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./_data:/data
|
|
@ -3,12 +3,11 @@
|
|||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun run --watch src/index.ts",
|
||||
"ngrok": "ngrok tunnel --label edge=edghts_2VNJvaPttrFlAPWxrGyVKu0s3ad http://localhost:3000"
|
||||
"start": "bun run --watch src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-forge": "^1.3.5",
|
||||
"bun-types": "^1.0.3"
|
||||
"bun-types": "^1.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { verify } from "./request"
|
||||
import { authorized, verify } from "./request"
|
||||
import outbox from "./outbox"
|
||||
import inbox from "./inbox"
|
||||
import ACTOR from "../actor"
|
||||
|
@ -31,20 +31,12 @@ export function reqIsActivityPub(req:Request) {
|
|||
return activityPubTypes.some(t => contentType?.includes(t))
|
||||
}
|
||||
|
||||
export function idsFromValue(value:any):string[] {
|
||||
if (!value) return []
|
||||
else if (typeof value === 'string') return [value]
|
||||
else if (value.id) return [value.id]
|
||||
else if (Array.isArray(value)) return value.map(v => idsFromValue(v)).flat(Infinity) as string[]
|
||||
return []
|
||||
}
|
||||
|
||||
const postOutbox = async (req:Request):Promise<Response> => {
|
||||
console.log("PostOutbox")
|
||||
|
||||
const bodyText = await req.text()
|
||||
if(!authorized(req)) return new Response('', { status: 401 })
|
||||
|
||||
// TODO: verify calls to the outbox, whether that be by basic authentication, bearer, or otherwise.
|
||||
const bodyText = await req.text()
|
||||
|
||||
const body = JSON.parse(bodyText)
|
||||
// ensure that the verified actor matches the actor in the request body
|
||||
|
|
43
src/admin.ts
43
src/admin.ts
|
@ -1,20 +1,19 @@
|
|||
import { idsFromValue } from "./activitypub"
|
||||
import { ADMIN_PASSWORD, ADMIN_USERNAME, activityPubTypes } from "./env"
|
||||
import { activityPubTypes } from "./env"
|
||||
import outbox from "./outbox"
|
||||
import { fetchObject } from "./request"
|
||||
import { authorized, fetchObject, idsFromValue } from "./request"
|
||||
import ACTOR from "../actor"
|
||||
import ActivityPubDB from "./db"
|
||||
import ActivityPubDB, { Activity } from "./db"
|
||||
|
||||
let db:ActivityPubDB
|
||||
|
||||
export default (req: Request, database: ActivityPubDB): Response | Promise<Response> | undefined => {
|
||||
export default async (req: Request, database: ActivityPubDB): Promise<Response | Promise<Response> | undefined> => {
|
||||
db = database
|
||||
const url = new URL(req.url)
|
||||
|
||||
if(!url.pathname.startsWith('/admin')) return undefined
|
||||
url.pathname = url.pathname.substring(6)
|
||||
|
||||
if(ADMIN_USERNAME && ADMIN_PASSWORD && !checkAuth(req.headers)) return new Response("", { status: 401 })
|
||||
if(!(await authorized(req))) return new Response("", { status: 401 })
|
||||
|
||||
let match
|
||||
if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 })
|
||||
|
@ -31,30 +30,14 @@ export default (req: Request, database: ActivityPubDB): Response | Promise<Respo
|
|||
else if(req.method == "POST" && (match = url.pathname.match(/^\/reply\/(.+)\/?$/i))) return create(req, match[1])
|
||||
else if(req.method == "DELETE" && (match = url.pathname.match(/^\/delete\/(.+)\/?$/i))) return deletePost(req, match[1])
|
||||
|
||||
console.log(`Couldn't match admin path ${req.method} "${url.pathname}"`)
|
||||
console.info(`Couldn't match admin path ${req.method} "${url.pathname}"`)
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const checkAuth = (headers: Headers): Boolean => {
|
||||
// check the basic auth header
|
||||
const auth = headers.get("Authorization")
|
||||
const split = auth?.split("")
|
||||
if(!split || split.length != 2 || split[0] !== "Basic") return false
|
||||
const decoded = atob(split[1])
|
||||
const [username, password] = decoded.split(":")
|
||||
return username === ADMIN_USERNAME && password === ADMIN_PASSWORD
|
||||
}
|
||||
|
||||
// // rebuild the 11ty static pages
|
||||
// export const rebuild = async(req:Request):Promise<Response> => {
|
||||
// db.rebuild()
|
||||
// return new Response("", { status: 201 })
|
||||
// }
|
||||
|
||||
// create an activity
|
||||
const create = async (req:Request, inReplyTo:string|null = null):Promise<Response> => {
|
||||
const body = await req.json()
|
||||
const body = await req.json() as any
|
||||
|
||||
if(!inReplyTo && body.object.inReplyTo) inReplyTo = body.object.inReplyTo
|
||||
|
||||
|
@ -100,7 +83,7 @@ const idFromHandle = async(handle:string) => {
|
|||
if(!host) return new Response('account not url or name@domain.tld', { status: 400 })
|
||||
|
||||
const res = await fetch(`https://${host}/.well-known/webfinger/?resource=acct:${handle}`, { headers: { 'accept': 'application/jrd+json'}})
|
||||
const webfinger = await res.json()
|
||||
const webfinger = await res.json() as any
|
||||
if(!webfinger.links) return new Response("", { status: 404 })
|
||||
|
||||
const links:any[] = webfinger.links
|
||||
|
@ -114,7 +97,7 @@ const idFromHandle = async(handle:string) => {
|
|||
|
||||
const follow = async (req:Request, handle:string):Promise<Response> => {
|
||||
const url = await idFromHandle(handle)
|
||||
console.log(`Following ${url}`)
|
||||
console.info(`Following ${url}`)
|
||||
|
||||
// send the follow request to the supplied actor
|
||||
return await outbox({
|
||||
|
@ -143,7 +126,7 @@ const unfollow = async (req:Request, handle:string):Promise<Response> => {
|
|||
}
|
||||
|
||||
const like = async (req:Request, object_url:string):Promise<Response> => {
|
||||
const object = await (await fetchObject(object_url)).json()
|
||||
const object = await (await fetchObject(object_url)).json() as any
|
||||
|
||||
return await outbox({
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
|
@ -160,7 +143,7 @@ const unlike = async (req:Request, activity_id:string):Promise<Response> => {
|
|||
const liked = db.listLiked()
|
||||
let existing = liked?.find(o => o?.activity_id === activity_id)
|
||||
if (!existing){
|
||||
const object = await (await fetchObject(activity_id)).json()
|
||||
const object = await (await fetchObject(activity_id)).json() as any
|
||||
idsFromValue(object).forEach(id => {
|
||||
const e = liked?.find(o => o.activity_id === id)
|
||||
if(e) existing = e
|
||||
|
@ -172,7 +155,7 @@ const unlike = async (req:Request, activity_id:string):Promise<Response> => {
|
|||
}
|
||||
|
||||
const dislike = async (req:Request, object_url:string):Promise<Response> => {
|
||||
const object = await (await fetchObject(object_url)).json()
|
||||
const object = await (await fetchObject(object_url)).json() as any
|
||||
|
||||
return await outbox({
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
|
@ -201,7 +184,7 @@ const undislike = async (req:Request, object_id:string):Promise<Response> => {
|
|||
}
|
||||
|
||||
const share = async (req:Request, object_url:string):Promise<Response> => {
|
||||
const object = await (await fetchObject(object_url)).json()
|
||||
const object = await (await fetchObject(object_url)).json() as any
|
||||
|
||||
return await outbox({
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
|
|
25
src/db.ts
25
src/db.ts
|
@ -1,12 +1,15 @@
|
|||
import { Database, Statement } from "bun:sqlite"
|
||||
import { existsSync, mkdirSync } from "fs"
|
||||
import { dirname } from "path"
|
||||
import { DB_PATH } from "./env"
|
||||
|
||||
export interface ActivityObject {
|
||||
export type ActivityObject = {
|
||||
activity_id: string
|
||||
id: string
|
||||
published: string
|
||||
}
|
||||
|
||||
export interface Activity extends ActivityObject {
|
||||
export type Activity = ActivityObject & {
|
||||
type: string
|
||||
actor: string
|
||||
published: string
|
||||
|
@ -15,11 +18,11 @@ export interface Activity extends ActivityObject {
|
|||
object?: any
|
||||
}
|
||||
|
||||
export interface Following extends ActivityObject {
|
||||
export type Following = ActivityObject & {
|
||||
accepted?: boolean
|
||||
}
|
||||
|
||||
export interface Post extends ActivityObject {
|
||||
export type Post = ActivityObject & {
|
||||
attributedTo: string
|
||||
type: string
|
||||
content?: string
|
||||
|
@ -32,7 +35,9 @@ export default class ActivityPubDB {
|
|||
// Cached Statements
|
||||
|
||||
constructor() {
|
||||
this.db = new Database('./db.sqlite')
|
||||
const dir = dirname(DB_PATH)
|
||||
if(!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
this.db = new Database(DB_PATH, {create:true, readwrite:true})
|
||||
this.migrate()
|
||||
this.cacheQueries()
|
||||
}
|
||||
|
@ -43,13 +48,11 @@ export default class ActivityPubDB {
|
|||
|
||||
migrate() {
|
||||
let version = (this.db.query("PRAGMA user_version;").get() as {user_version:number})?.user_version
|
||||
console.log(`Hi from migrate! User version: ${version}`)
|
||||
|
||||
const statements:Statement[] = []
|
||||
|
||||
switch(version) {
|
||||
case 0:
|
||||
console.log("migrating db version 1")
|
||||
this.db.exec("PRAGMA journal_mode = WAL;")
|
||||
|
||||
// Create the inbox table
|
||||
|
@ -59,7 +62,7 @@ export default class ActivityPubDB {
|
|||
[type] TEXT NOT NULL,
|
||||
[actor] TEXT NOT NULL,
|
||||
[published] TEXT NOT NULL,
|
||||
[to] TEXT NOT NULL,
|
||||
[to] TEXT,
|
||||
[cc] TEXT,
|
||||
[activity] TEXT NOT NULL
|
||||
)`))
|
||||
|
@ -71,7 +74,7 @@ export default class ActivityPubDB {
|
|||
[type] TEXT NOT NULL,
|
||||
[actor] TEXT NOT NULL,
|
||||
[published] TEXT NOT NULL,
|
||||
[to] TEXT NOT NULL,
|
||||
[to] TEXT,
|
||||
[cc] TEXT,
|
||||
[activity] TEXT NOT NULL
|
||||
)`))
|
||||
|
@ -131,7 +134,6 @@ export default class ActivityPubDB {
|
|||
|
||||
const migration = this.db.transaction((statements:Statement[]) => {
|
||||
statements.forEach(s => {
|
||||
console.log(s.toString());
|
||||
s.run();
|
||||
s.finalize()
|
||||
})
|
||||
|
@ -271,7 +273,7 @@ export default class ActivityPubDB {
|
|||
$id: activity.id,
|
||||
$type: activity.type,
|
||||
$actor: activity.actor,
|
||||
$published: activity.published,
|
||||
$published: activity.published || new Date().toISOString(),
|
||||
$to: JSON.stringify(activity.to),
|
||||
$cc: JSON.stringify(activity.cc),
|
||||
$activity: JSON.stringify(activity)
|
||||
|
@ -325,7 +327,6 @@ export default class ActivityPubDB {
|
|||
}
|
||||
|
||||
deleteFollower(id:string) {
|
||||
console.log("! DELETE Follower ", id)
|
||||
return this.queries.deleteFollower?.run(id)
|
||||
}
|
||||
|
||||
|
|
42
src/env.ts
42
src/env.ts
|
@ -1,53 +1,37 @@
|
|||
import forge from "node-forge" // import crypto from "node:crypto"
|
||||
import forge from "node-forge" // Bun does not implement the required node:crypto functions
|
||||
import path from "path"
|
||||
|
||||
// set up username and password for admin actions
|
||||
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "";
|
||||
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "";
|
||||
// the endpoint for auth token validation
|
||||
export const TOKEN_ENDPOINT = process.env.TOKEN_ENDPOINT
|
||||
export const WEB_SITE_HOSTNAME = process.env.WEB_SITE_HOSTNAME
|
||||
|
||||
// get the hostname (`PROJECT_DOMAIN` is set via glitch, but since we're using Bun now, this won't matter)
|
||||
export const HOSTNAME = /*(process.env.PROJECT_DOMAIN && `${process.env.PROJECT_DOMAIN}.glitch.me`) ||*/ process.env.HOSTNAME || "localhost"
|
||||
export const NODE_ENV = process.env.NODE_ENV || "development"
|
||||
export const HOSTNAME = process.env.HOSTNAME || "localhost"
|
||||
export const PORT = process.env.PORT || "3000"
|
||||
|
||||
export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME
|
||||
|
||||
export const NODE_ENV = process.env.NODE_ENV || "production"
|
||||
|
||||
// in development, generate a key pair to make it easier to get started
|
||||
const keypair =
|
||||
NODE_ENV === "development"
|
||||
? forge.pki.rsa.generateKeyPair({bits: 4096}) //crypto.generateKeyPairSync("rsa", { modulusLength: 4096 })
|
||||
? forge.pki.rsa.generateKeyPair({bits: 4096})
|
||||
: undefined
|
||||
|
||||
export const PUBLIC_KEY =
|
||||
process.env.PUBLIC_KEY ||
|
||||
(keypair && forge.pki.publicKeyToPem(keypair.publicKey)) || //keypair?.publicKey.export({ type: "spki", format: "pem" }) ||
|
||||
(keypair && forge.pki.publicKeyToPem(keypair.publicKey)) ||
|
||||
""
|
||||
export const PRIVATE_KEY =
|
||||
process.env.PRIVATE_KEY ||
|
||||
(keypair && forge.pki.privateKeyToPem(keypair.privateKey)) || //keypair?.privateKey.export({ type: "pkcs8", format: "pem" }) ||
|
||||
(keypair && forge.pki.privateKeyToPem(keypair.privateKey)) ||
|
||||
""
|
||||
|
||||
export const STATIC_PATH = path.join('.', '_site')
|
||||
export const CONTENT_PATH = path.join('.', '_content')
|
||||
export const POSTS_PATH = path.join(CONTENT_PATH, "posts")
|
||||
export const DATA_PATH = path.join(CONTENT_PATH, "_data")
|
||||
export const DB_PATH = path.join(DATA_PATH, "db.sqlite")
|
||||
export const ACTIVITY_INBOX_PATH = path.join(DATA_PATH, "_inbox")
|
||||
export const ACTIVITY_OUTBOX_PATH = path.join(DATA_PATH, "_outbox")
|
||||
|
||||
export const DEFAULT_DOCUMENTS = process.env.DEFAULT_DOCUMENTS || [
|
||||
'index.html',
|
||||
'index.shtml',
|
||||
'index.htm',
|
||||
'Index.html',
|
||||
'Index.shtml',
|
||||
'Index.htm',
|
||||
'default.html',
|
||||
'default.htm'
|
||||
]
|
||||
export const DATA_PATH = process.env.DATA_PATH || "/data"
|
||||
export const DB_PATH = process.env.DB_PATH || path.join(DATA_PATH, "db.sqlite")
|
||||
|
||||
export const activityPubTypes = [
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'application/ld+json; profile="http://www.w3.org/ns/activitystreams"',
|
||||
'application/activity+json'
|
||||
]
|
||||
export const contentTypeHeader = { 'Content-Type': activityPubTypes[0] }
|
|
@ -1,6 +1,5 @@
|
|||
import { idsFromValue } from "./activitypub"
|
||||
import outbox from "./outbox"
|
||||
import { send } from "./request"
|
||||
import { idsFromValue, send } from "./request"
|
||||
import ACTOR from "../actor"
|
||||
import ActivityPubDB from "./db"
|
||||
|
||||
|
@ -18,7 +17,7 @@ export default async function inbox(activity:any, database:ActivityPubDB) {
|
|||
|
||||
// save this activity to my inbox
|
||||
const activity_id = `${date.getTime().toString(32)}`
|
||||
console.log(`New inbox activity ${activity_id} (${activity.type})`, activity)
|
||||
console.info(`New inbox activity ${activity_id} (${activity.type})`, activity)
|
||||
db.createInboxActivity(activity_id, activity)
|
||||
|
||||
// TODO: process the activity and update local data
|
||||
|
|
33
src/index.ts
33
src/index.ts
|
@ -3,12 +3,14 @@ import activitypub, { reqIsActivityPub } from "./activitypub"
|
|||
import { fetchObject } from "./request"
|
||||
import { handle, webfinger } from "../actor"
|
||||
import ActivityPubDB from './db'
|
||||
import { HOSTNAME, PORT, WEB_SITE_HOSTNAME } from './env'
|
||||
|
||||
const db = new ActivityPubDB()
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
fetch(req: Request): Response | Promise<Response> {
|
||||
port: PORT,
|
||||
// hostname: HOSTNAME,
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url)
|
||||
|
||||
// log the incoming request info
|
||||
|
@ -41,9 +43,30 @@ const server = Bun.serve({
|
|||
return fetchObject(object_url)
|
||||
}
|
||||
|
||||
// admin and activitypub routes
|
||||
return admin(req, db) || activitypub(req, db) || new Response("", { status: 404 })
|
||||
// admin and activitypub routes, plus a fallback
|
||||
return await admin(req, db) || activitypub(req, db) || fallback(req)
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Listening on http://localhost:${server.port} ...`)
|
||||
const fallback = async (req: Request) => {
|
||||
// if we haven't a fallback hostname defined, just return 404
|
||||
if(!WEB_SITE_HOSTNAME) return new Response('', { status: 404 })
|
||||
|
||||
// swap the hostname in the current request
|
||||
const url = new URL(req.url)
|
||||
url.hostname = WEB_SITE_HOSTNAME
|
||||
console.info(`➡️ Forwarding ${req.method} request to ${url}`)
|
||||
// override the host header, because this can cause problems if wrong
|
||||
if(req.headers.get("host")) req.headers.set('host', WEB_SITE_HOSTNAME)
|
||||
|
||||
// const body = ['GET', 'HEAD'].includes(req.method) ? undefined : await req.blob()
|
||||
// // just use *all the things* from the request object (body is handled separately)
|
||||
|
||||
// TODO: possibly not forwarding the body correctly.
|
||||
// My current website is static, but probably want to test and/or fix this in the future.
|
||||
// @ts-ignore typescript doesn't think Request is compatible with RequestInit
|
||||
const newRequest = new Request(url, req)
|
||||
return await fetch(newRequest)
|
||||
}
|
||||
|
||||
console.info(`📡 Listening on http://${server.hostname}:${server.port} ...`)
|
|
@ -1,7 +1,6 @@
|
|||
import { idsFromValue } from "./activitypub"
|
||||
import { fetchObject, send } from "./request"
|
||||
import { fetchObject, idsFromValue, send } from "./request"
|
||||
import ACTOR from "../actor"
|
||||
import ActivityPubDB from "./db"
|
||||
import ActivityPubDB, { Activity } from "./db"
|
||||
|
||||
let db:ActivityPubDB
|
||||
|
||||
|
@ -9,7 +8,7 @@ export default async function outbox(activity:any, database:ActivityPubDB):Promi
|
|||
db = database
|
||||
const date = new Date()
|
||||
const activity_id = `${date.getTime().toString(32)}`
|
||||
console.log('outbox', activity_id, activity)
|
||||
console.info('outbox', activity_id, activity)
|
||||
|
||||
// https://www.w3.org/TR/activitypub/#object-without-create
|
||||
if(!activity.actor && !(activity.object || activity.target || activity.result || activity.origin || activity.instrument)) {
|
||||
|
@ -46,17 +45,11 @@ export default async function outbox(activity:any, database:ActivityPubDB):Promi
|
|||
delete activity.bcc
|
||||
|
||||
// now that has been taken care of, it's time to update our local data, depending on the contents of the activity
|
||||
switch(activity.type) {
|
||||
case "Accept": await accept(activity, activity_id); break;
|
||||
case "Follow": await follow(activity, activity_id); break;
|
||||
case "Like": await like(activity, activity_id); break;
|
||||
case "Dislike": await dislike(activity, activity_id); break;
|
||||
case "Annouce": await announce(activity, activity_id); break;
|
||||
case "Create": await create(activity, activity_id); break;
|
||||
case "Undo": await undo(activity); break;
|
||||
case "Delete": await deletePost(activity); break;
|
||||
// TODO: case "Anncounce": return await share(activity)
|
||||
}
|
||||
// aka "side effects"
|
||||
// Note: I do this *before* adding the activity to the database so I can cache specific objects
|
||||
// for example, for likes / dislikes / shares, the object probably only exists as the url,
|
||||
// but I want to fetch it and store it so I can use that data later.
|
||||
await outboxSideEffect(activity_id, activity)
|
||||
// save the activity data for the outbox
|
||||
db.createOutboxActivity(activity_id, activity)
|
||||
|
||||
|
@ -70,6 +63,20 @@ export default async function outbox(activity:any, database:ActivityPubDB):Promi
|
|||
return new Response("", { status: 201, headers: { location: activity.id } })
|
||||
}
|
||||
|
||||
async function outboxSideEffect(activity_id:string, activity:Activity) {
|
||||
switch(activity.type) {
|
||||
case "Accept": await accept(activity, activity_id); break;
|
||||
case "Follow": await follow(activity, activity_id); break;
|
||||
case "Like": await like(activity, activity_id); break;
|
||||
case "Dislike": await dislike(activity, activity_id); break;
|
||||
case "Annouce": await announce(activity, activity_id); break;
|
||||
case "Create": await create(activity, activity_id); break;
|
||||
case "Undo": await undo(activity); break;
|
||||
case "Delete": await deletePost(activity); break;
|
||||
// TODO: case "Anncounce": return await share(activity)
|
||||
}
|
||||
}
|
||||
|
||||
async function create(activity:any, activity_id:string) {
|
||||
activity.object.id = activity.object.url = `${ACTOR.url}/posts/${activity_id}`
|
||||
db.createPost(activity_id, activity.object)
|
||||
|
@ -143,7 +150,7 @@ async function undo(activity:any) {
|
|||
if (!id) return true
|
||||
const match = id.match(/\/([0-9a-z]+)\/?$/)
|
||||
const activity_id = match ? match[1] : id
|
||||
console.log('undo', activity_id)
|
||||
console.info('undo', activity_id)
|
||||
try{
|
||||
const existing = db.getOutboxActivity(activity_id)
|
||||
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import forge from "node-forge" // import crypto from "node:crypto"
|
||||
import { PRIVATE_KEY } from "./env";
|
||||
import { PRIVATE_KEY, TOKEN_ENDPOINT } from "./env";
|
||||
import ACTOR from "../actor"
|
||||
|
||||
export function idsFromValue(value:any):string[] {
|
||||
if (!value) return []
|
||||
else if (typeof value === 'string') return [value]
|
||||
else if (value.id) return [value.id]
|
||||
else if (Array.isArray(value)) return value.map(v => idsFromValue(v)).flat(Infinity) as string[]
|
||||
return []
|
||||
}
|
||||
|
||||
// this function adds / modifies the appropriate headers for signing a request, then calls fetch
|
||||
export function signedFetch(url: string | URL | Request, init?: FetchRequestInit): Promise<Response>
|
||||
{
|
||||
|
@ -52,7 +60,6 @@ async function fetchActor(url:string) {
|
|||
|
||||
/** Fetches and returns an object at a URL. */
|
||||
export async function fetchObject(object_url:string) {
|
||||
console.log(`fetch ${object_url}`)
|
||||
|
||||
const res = await signedFetch(object_url);
|
||||
|
||||
|
@ -69,9 +76,9 @@ export async function fetchObject(object_url:string) {
|
|||
* @param message the body of the request to send.
|
||||
*/
|
||||
export async function send(recipient:string, message:any, from:string=ACTOR.id) {
|
||||
console.log(`Sending to ${recipient}`, message)
|
||||
console.info(`Sending to ${recipient}`, message)
|
||||
// TODO: revisit fetch actor to use webfinger to get the inbox maybe?
|
||||
const actor = await fetchActor(recipient)
|
||||
const actor = await fetchActor(recipient) as any
|
||||
|
||||
const body = JSON.stringify(message)
|
||||
|
||||
|
@ -122,7 +129,7 @@ export async function verify(req:Request, body:string) {
|
|||
}
|
||||
|
||||
// get the actor's public key
|
||||
const actor = await fetchActor(keyId);
|
||||
const actor = await fetchActor(keyId) as any
|
||||
if (!actor.publicKey) throw new Error("No public key found.")
|
||||
const key = forge.pki.publicKeyFromPem(actor.publicKey.publicKeyPem)
|
||||
|
||||
|
@ -150,3 +157,16 @@ export async function verify(req:Request, body:string) {
|
|||
|
||||
return actor.id;
|
||||
}
|
||||
|
||||
/* verifies that the request is authorized
|
||||
* (i.e. has a valid auth token) */
|
||||
export async function authorized(req:Request) {
|
||||
if(!TOKEN_ENDPOINT) return false
|
||||
const response = await fetch(TOKEN_ENDPOINT, { headers: {"authorization": req.headers.get("authorization") || ''} })
|
||||
if(!response.ok) return false
|
||||
|
||||
const json = await response.json() as any
|
||||
if(new URL(json.me).hostname !== new URL(ACTOR.id).hostname) return false
|
||||
|
||||
return true
|
||||
}
|
Loading…
Reference in a new issue