MOW-95: use instance config in Env instead of DB

pull/72/head
Sven Sauleau 2023-01-11 14:32:06 +00:00
rodzic 0d01476ca0
commit 26fbb26cec
28 zmienionych plików z 381 dodań i 580 usunięć

Wyświetl plik

@ -109,6 +109,39 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
if: ${{ env.tfstate_kv != '' }}
- name: download VAPID keys
uses: cloudflare/wrangler-action@2.0.0
with:
command: kv:key get --namespace-id=${{ env.tfstate_kv }} vapid_jwk | jq . > ./tf/vapid_jwk
apiToken: ${{ secrets.CF_API_TOKEN }}
preCommands: |
echo "*** pre commands ***"
apt-get update && apt-get -y install jq
echo "******"
postCommands: |
echo "*** post commands ***"
chmod 777 ./tf/vapid_jwk
echo "******"
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
continue-on-error: true
- name: generate VAPID keys if needed
run: |
if [ ! -s ./tf/vapid_jwk ]
then
node ./scripts/generate-vapid-keys.mjs > ./tf/vapid_jwk
echo "VAPID keys generated"
fi
- name: store VAPID keys state
uses: cloudflare/wrangler-action@2.0.0
with:
command: kv:key put --namespace-id=${{ env.tfstate_kv }} vapid_jwk --path=./tf/vapid_jwk
apiToken: ${{ secrets.CF_API_TOKEN }}
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
- name: Configure
run: terraform plan && terraform apply -auto-approve
continue-on-error: true

Wyświetl plik

@ -1,4 +1,5 @@
import * as actors from 'wildebeest/backend/src/activitypub/actors'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
import * as objects from 'wildebeest/backend/src/activitypub/objects'
@ -79,7 +80,14 @@ export function makeGetActorAsId(activity: Activity): Function {
}
}
export async function handle(domain: string, activity: Activity, db: D1Database, userKEK: string) {
export async function handle(
domain: string,
activity: Activity,
db: D1Database,
userKEK: string,
adminEmail: string,
vapidKeys: JWK
) {
// The `object` field of the activity is required to be an object, with an
// `id` and a `type` field.
const requireComplexObject = () => {
@ -183,7 +191,7 @@ export async function handle(domain: string, activity: Activity, db: D1Database,
const notifId = await createNotification(db, 'mention', person, fromActor, obj)
await Promise.all([
await addObjectInInbox(db, person, obj),
await sendMentionNotification(db, fromActor, person, notifId),
await sendMentionNotification(db, fromActor, person, notifId, adminEmail, vapidKeys),
])
}
@ -228,7 +236,7 @@ export async function handle(domain: string, activity: Activity, db: D1Database,
// Notify the user
const notifId = await insertFollowNotification(db, receiver, originalActor)
await sendFollowNotification(db, originalActor, receiver, notifId)
await sendFollowNotification(db, originalActor, receiver, notifId, adminEmail, vapidKeys)
} else {
console.warn(`actor ${objectId} not found`)
}
@ -282,7 +290,7 @@ export async function handle(domain: string, activity: Activity, db: D1Database,
// Store the reblog for counting
insertReblog(db, fromActor, obj),
sendReblogNotification(db, fromActor, targetActor, notifId),
sendReblogNotification(db, fromActor, targetActor, notifId, adminEmail, vapidKeys),
])
break
}
@ -312,7 +320,7 @@ export async function handle(domain: string, activity: Activity, db: D1Database,
insertLike(db, fromActor, obj),
])
await sendLikeNotification(db, fromActor, targetActor, notifId)
await sendLikeNotification(db, fromActor, targetActor, notifId, adminEmail, vapidKeys)
break
}

Wyświetl plik

@ -1,53 +1,10 @@
export type InstanceConfig = {
title?: string
email?: string
description?: string
thumbnail?: string
}
import type { Env } from 'wildebeest/backend/src/types/env'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
const DEFAULT_THUMBNAIL =
export const DEFAULT_THUMBNAIL =
'https://imagedelivery.net/NkfPDviynOyTAOI79ar_GQ/b24caf12-5230-48c4-0bf7-2f40063bd400/thumbnail'
export async function configure(db: D1Database, data: InstanceConfig) {
const sql = `
INSERT INTO instance_config
VALUES ('title', ?),
('email', ?),
('thumbnail', ?),
('description', ?);
`
const { success, error } = await db
.prepare(sql)
.bind(data.title, data.email, DEFAULT_THUMBNAIL, data.description)
.run()
if (!success) {
throw new Error('SQL error: ' + error)
}
}
export async function generateVAPIDKeys(db: D1Database) {
const keyPair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
'sign',
'verify',
])) as CryptoKeyPair
const jwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
const sql = `
INSERT INTO instance_config
VALUES ('vapid_jwk', ?);
`
const { success, error } = await db.prepare(sql).bind(JSON.stringify(jwk)).run()
if (!success) {
throw new Error('SQL error: ' + error)
}
}
export async function get(db: D1Database, name: string): Promise<string> {
const row: { value: string } = await db.prepare('SELECT value FROM instance_config WHERE key = ?').bind(name).first()
if (!row) {
throw new Error(`configuration not found: ${name}`)
}
return row.value
export function getVAPIDKeys(env: Env): JWK {
const value: JWK = JSON.parse(env.VAPID_JWK)
return value
}

Wyświetl plik

@ -1,4 +1,5 @@
import type { Object } from 'wildebeest/backend/src/activitypub/objects'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import * as actors from 'wildebeest/backend/src/activitypub/actors'
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
@ -9,8 +10,6 @@ import { WebPushResult } from 'wildebeest/backend/src/webpush/webpushinfos'
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
import type { NotificationType, Notification } from 'wildebeest/backend/src/types/notification'
import { getSubscriptionForAllClients } from 'wildebeest/backend/src/mastodon/subscription'
import { getVAPIDKeys } from 'wildebeest/backend/src/mastodon/subscription'
import * as config from 'wildebeest/backend/src/config'
export async function createNotification(
db: D1Database,
@ -43,9 +42,14 @@ export async function insertFollowNotification(db: D1Database, actor: Actor, fro
return row.id
}
export async function sendFollowNotification(db: D1Database, follower: Actor, actor: Actor, notificationId: string) {
const sub = await config.get(db, 'email')
export async function sendFollowNotification(
db: D1Database,
follower: Actor,
actor: Actor,
notificationId: string,
adminEmail: string,
vapidKeys: JWK
) {
const data = {
preferred_locale: 'en',
notification_type: 'follow',
@ -58,16 +62,21 @@ export async function sendFollowNotification(db: D1Database, follower: Actor, ac
const message: WebPushMessage = {
data: JSON.stringify(data),
urgency: 'normal',
sub,
sub: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message)
return sendNotification(db, actor, message, vapidKeys)
}
export async function sendLikeNotification(db: D1Database, fromActor: Actor, actor: Actor, notificationId: string) {
const sub = await config.get(db, 'email')
export async function sendLikeNotification(
db: D1Database,
fromActor: Actor,
actor: Actor,
notificationId: string,
adminEmail: string,
vapidKeys: JWK
) {
const data = {
preferred_locale: 'en',
notification_type: 'favourite',
@ -80,16 +89,21 @@ export async function sendLikeNotification(db: D1Database, fromActor: Actor, act
const message: WebPushMessage = {
data: JSON.stringify(data),
urgency: 'normal',
sub,
sub: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message)
return sendNotification(db, actor, message, vapidKeys)
}
export async function sendMentionNotification(db: D1Database, fromActor: Actor, actor: Actor, notificationId: string) {
const sub = await config.get(db, 'email')
export async function sendMentionNotification(
db: D1Database,
fromActor: Actor,
actor: Actor,
notificationId: string,
adminEmail: string,
vapidKeys: JWK
) {
const data = {
preferred_locale: 'en',
notification_type: 'mention',
@ -102,16 +116,21 @@ export async function sendMentionNotification(db: D1Database, fromActor: Actor,
const message: WebPushMessage = {
data: JSON.stringify(data),
urgency: 'normal',
sub,
sub: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message)
return sendNotification(db, actor, message, vapidKeys)
}
export async function sendReblogNotification(db: D1Database, fromActor: Actor, actor: Actor, notificationId: string) {
const sub = await config.get(db, 'email')
export async function sendReblogNotification(
db: D1Database,
fromActor: Actor,
actor: Actor,
notificationId: string,
adminEmail: string,
vapidKeys: JWK
) {
const data = {
preferred_locale: 'en',
notification_type: 'reblog',
@ -124,15 +143,14 @@ export async function sendReblogNotification(db: D1Database, fromActor: Actor, a
const message: WebPushMessage = {
data: JSON.stringify(data),
urgency: 'normal',
sub,
sub: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message)
return sendNotification(db, actor, message, vapidKeys)
}
async function sendNotification(db: D1Database, actor: Actor, message: WebPushMessage) {
const vapidKeys = await getVAPIDKeys(db)
async function sendNotification(db: D1Database, actor: Actor, message: WebPushMessage, vapidKeys: JWK) {
const subscriptions = await getSubscriptionForAllClients(db, actor)
const promises = subscriptions.map(async (subscription) => {

Wyświetl plik

@ -124,15 +124,6 @@ function subscriptionFromRow(row: any): Subscription {
}
}
export async function getVAPIDKeys(db: D1Database): Promise<JWK> {
const row: any = await db.prepare("SELECT value FROM instance_config WHERE key = 'vapid_jwk'").first()
if (!row) {
throw new Error('missing VAPID keys')
}
const value: JWK = JSON.parse(row.value)
return value
}
export function VAPIDPublicKey(keys: JWK): string {
return b64ToUrlEncoded(exportPublicKeyPair(keys))
}

Wyświetl plik

@ -10,4 +10,10 @@ export interface Env {
DOMAIN: string
ACCESS_AUD: string
ACCESS_AUTH_DOMAIN: string
// Configuration for the instance
INSTANCE_TITLE: string
ADMIN_EMAIL: string
INSTANCE_DESCR: string
VAPID_JWK: string
}

Wyświetl plik

@ -1,7 +1,7 @@
import { makeDB, isUrlValid } from './utils'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import { addFollowing } from 'wildebeest/backend/src/mastodon/follow'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { configure, generateVAPIDKeys } from 'wildebeest/backend/src/config'
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
@ -13,8 +13,10 @@ import * as ap_outbox from 'wildebeest/functions/ap/users/[id]/outbox'
import * as ap_outbox_page from 'wildebeest/functions/ap/users/[id]/outbox/page'
const userKEK = 'test_kek5'
const vapidKeys = {} as JWK
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const domain = 'cloudflare.com'
const adminEmail = 'admin@example.com'
describe('ActivityPub', () => {
test('fetch non-existant user by id', async () => {
@ -74,7 +76,7 @@ describe('ActivityPub', () => {
},
}
await activityHandler.handle(domain, activity, db, userKEK)
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const row = await db
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
@ -96,7 +98,7 @@ describe('ActivityPub', () => {
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK), {
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
@ -114,7 +116,7 @@ describe('ActivityPub', () => {
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK), {
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
@ -131,7 +133,7 @@ describe('ActivityPub', () => {
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK), {
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
@ -150,7 +152,7 @@ describe('ActivityPub', () => {
},
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK), {
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: 'object https://example.com/note2 does not exist',
})
})
@ -174,7 +176,7 @@ describe('ActivityPub', () => {
object: object,
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK), {
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: 'actorid mismatch when updating object',
})
})
@ -204,7 +206,7 @@ describe('ActivityPub', () => {
object: newObject,
}
await activityHandler.handle(domain, activity, db, userKEK)
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const updatedObject = await db.prepare('SELECT * FROM objects WHERE original_object_id=?').bind(object.id).first()
assert(updatedObject)
@ -276,8 +278,6 @@ describe('ActivityPub', () => {
}
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity: any = {
@ -287,7 +287,7 @@ describe('ActivityPub', () => {
cc: [],
object: objectId,
}
await activityHandler.handle(domain, activity, db, userKEK)
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const object = await db.prepare('SELECT * FROM objects').bind(remoteActorId).first()
assert(object)

Wyświetl plik

@ -1,5 +1,5 @@
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import { configure, generateVAPIDKeys } from 'wildebeest/backend/src/config'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import * as ap_followers_page from 'wildebeest/functions/ap/users/[id]/followers/page'
import * as ap_following_page from 'wildebeest/functions/ap/users/[id]/following/page'
import * as ap_followers from 'wildebeest/functions/ap/users/[id]/followers'
@ -11,6 +11,8 @@ import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
const userKEK = 'test_kek10'
const domain = 'cloudflare.com'
const adminEmail = 'admin@example.com'
const vapidKeys = {} as JWK
describe('ActivityPub', () => {
describe('Follow', () => {
@ -34,8 +36,6 @@ describe('ActivityPub', () => {
test('Receive follow with Accept reply', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
@ -46,7 +46,7 @@ describe('ActivityPub', () => {
object: actor.id.toString(),
}
await activityHandler.handle(domain, activity, db, userKEK)
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const row = await db
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
@ -132,8 +132,6 @@ describe('ActivityPub', () => {
test('creates a notification', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
@ -144,7 +142,7 @@ describe('ActivityPub', () => {
object: actor.id,
}
await activityHandler.handle(domain, activity, db, userKEK)
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
assert.equal(entry.type, 'follow')

Wyświetl plik

@ -1,5 +1,5 @@
import { makeDB } from '../utils'
import { generateVAPIDKeys, configure } from 'wildebeest/backend/src/config'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import * as objects from 'wildebeest/backend/src/activitypub/objects'
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
import * as ap_inbox from 'wildebeest/functions/ap/users/[id]/inbox'
@ -8,6 +8,8 @@ import { strict as assert } from 'node:assert/strict'
const userKEK = 'test_kek9'
const domain = 'cloudflare.com'
const adminEmail = 'admin@example.com'
const vapidKeys = {} as JWK
const kv_cache: any = {
async put() {},
@ -20,14 +22,22 @@ describe('ActivityPub', () => {
const db = await makeDB()
const activity: any = {}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'sven', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 404)
})
test('send Note to inbox stores in DB', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity: any = {
@ -41,7 +51,17 @@ describe('ActivityPub', () => {
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'sven', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db
@ -81,7 +101,17 @@ describe('ActivityPub', () => {
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'sven', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM outbox_objects WHERE actor_id=?').bind(remoteActorId).first()
@ -90,8 +120,6 @@ describe('ActivityPub', () => {
test('local actor sends Note with mention create notification', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
@ -106,7 +134,17 @@ describe('ActivityPub', () => {
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'a', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
@ -132,8 +170,6 @@ describe('ActivityPub', () => {
}
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const activity: any = {
@ -147,7 +183,17 @@ describe('ActivityPub', () => {
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'a', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actors WHERE id=?').bind(actorB).first()
@ -156,8 +202,6 @@ describe('ActivityPub', () => {
test('send Note records reply', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
{
@ -171,7 +215,17 @@ describe('ActivityPub', () => {
content: 'post',
},
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'sven', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
}
@ -187,7 +241,17 @@ describe('ActivityPub', () => {
content: 'reply',
},
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'sven', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
}
@ -206,8 +270,6 @@ describe('ActivityPub', () => {
describe('Announce', () => {
test('records reblog in db', async () => {
const db = await makeDB()
await generateVAPIDKeys(db)
await configure(db, { title: 'title', description: 'a', email: 'email' })
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
@ -218,7 +280,17 @@ describe('ActivityPub', () => {
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'a', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_reblogs').first()
@ -228,8 +300,6 @@ describe('ActivityPub', () => {
test('creates notification', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
@ -240,7 +310,17 @@ describe('ActivityPub', () => {
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'a', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
@ -254,8 +334,6 @@ describe('ActivityPub', () => {
describe('Like', () => {
test('records like in db', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
@ -266,7 +344,17 @@ describe('ActivityPub', () => {
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'a', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
@ -276,8 +364,6 @@ describe('ActivityPub', () => {
test('creates notification', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
@ -288,7 +374,17 @@ describe('ActivityPub', () => {
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'a', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
@ -299,8 +395,6 @@ describe('ActivityPub', () => {
test('records like in db', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
@ -311,7 +405,17 @@ describe('ActivityPub', () => {
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(domain, db, kv_cache, 'a', activity, userKEK, waitUntil)
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()

Wyświetl plik

@ -1,4 +1,6 @@
import { strict as assert } from 'node:assert/strict'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import type { Env } from 'wildebeest/backend/src/types/env'
import * as v1_instance from 'wildebeest/functions/api/v1/instance'
import * as v2_instance from 'wildebeest/functions/api/v2/instance'
import * as apps from 'wildebeest/functions/api/v1/apps'
@ -9,24 +11,29 @@ import { makeDB, assertCORS, assertJSON, assertCache, createTestClient } from '.
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { createSubscription } from '../src/mastodon/subscription'
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
import { configure, generateVAPIDKeys } from 'wildebeest/backend/src/config'
const userKEK = 'test_kek'
const domain = 'cloudflare.com'
async function generateVAPIDKeys(): Promise<JWK> {
const keyPair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
'sign',
'verify',
])) as CryptoKeyPair
const jwk = (await crypto.subtle.exportKey('jwk', keyPair.privateKey)) as JWK
return jwk
}
describe('Mastodon APIs', () => {
describe('instance', () => {
test('return the instance infos v1', async () => {
const db = await makeDB()
const data = {
title: 'title',
uri: 'uri',
email: 'email',
description: 'description',
}
await configure(db, data)
const env = {
INSTANCE_TITLE: 'a',
ADMIN_EMAIL: 'b',
INSTANCE_DESCR: 'c',
} as Env
const res = await v1_instance.handleRequest(domain, db)
const res = await v1_instance.handleRequest(domain, env)
assert.equal(res.status, 200)
assertCORS(res)
assertJSON(res)
@ -35,55 +42,60 @@ describe('Mastodon APIs', () => {
const data = await res.json<any>()
assert.equal(data.rules.length, 0)
assert.equal(data.uri, domain)
assert.equal(data.title, 'a')
assert.equal(data.email, 'b')
assert.equal(data.description, 'c')
}
})
test('adds a short_description if missing v1', async () => {
const db = await makeDB()
const data = {
title: 'title',
uri: 'uri',
email: 'email',
description: 'description',
}
await configure(db, data)
const env = {
INSTANCE_DESCR: 'c',
} as Env
const res = await v1_instance.handleRequest(domain, db)
const res = await v1_instance.handleRequest(domain, env)
assert.equal(res.status, 200)
{
const data = await res.json<any>()
assert.equal(data.short_description, 'description')
assert.equal(data.short_description, 'c')
}
})
test('return the instance infos v2', async () => {
const db = await makeDB()
const data = {
title: 'title',
uri: 'uri',
email: 'email',
description: 'description',
}
await configure(db, data)
const res = await v2_instance.handleRequest(domain, db)
const env = {
INSTANCE_TITLE: 'a',
ADMIN_EMAIL: 'b',
INSTANCE_DESCR: 'c',
} as Env
const res = await v2_instance.handleRequest(domain, db, env)
assert.equal(res.status, 200)
assertCORS(res)
assertJSON(res)
{
const data = await res.json<any>()
assert.equal(data.rules.length, 0)
assert.equal(data.domain, domain)
assert.equal(data.title, 'a')
assert.equal(data.contact.email, 'b')
assert.equal(data.description, 'c')
}
})
})
describe('apps', () => {
test('return the app infos', async () => {
const db = await makeDB()
await generateVAPIDKeys(db)
const vapidKeys = await generateVAPIDKeys()
const request = new Request('https://example.com', {
method: 'POST',
body: '{"redirect_uris":"mastodon://joinmastodon.org/oauth","website":"https://app.joinmastodon.org/ios","client_name":"Mastodon for iOS","scopes":"read write follow push"}',
})
const res = await apps.handleRequest(db, request)
const res = await apps.handleRequest(db, request, vapidKeys)
assert.equal(res.status, 200)
assertCORS(res)
assertJSON(res)
@ -100,11 +112,14 @@ describe('Mastodon APIs', () => {
})
test('returns 404 for GET request', async () => {
const vapidKeys = await generateVAPIDKeys()
const request = new Request('https://example.com')
const ctx: any = {
next: () => new Response(),
data: null,
env: {},
env: {
VAPID_JWK: JSON.stringify(vapidKeys),
},
request,
}
@ -169,8 +184,8 @@ describe('Mastodon APIs', () => {
test('create subscription', async () => {
const db = await makeDB()
const client = await createTestClient(db)
await generateVAPIDKeys(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const vapidKeys = await generateVAPIDKeys()
const data: any = {
subscription: {
@ -190,7 +205,7 @@ describe('Mastodon APIs', () => {
body: JSON.stringify(data),
})
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id)
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 200)
const row: any = await db.prepare('SELECT * FROM subscriptions').first()
@ -202,8 +217,8 @@ describe('Mastodon APIs', () => {
test('create subscriptions only creates one', async () => {
const db = await makeDB()
const client = await createTestClient(db)
await generateVAPIDKeys(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const vapidKeys = await generateVAPIDKeys()
const data: any = {
subscription: {
@ -225,7 +240,7 @@ describe('Mastodon APIs', () => {
body: JSON.stringify(data),
})
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id)
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 200)
const { count } = await db.prepare('SELECT count(*) as count FROM subscriptions').first()

Wyświetl plik

@ -1,5 +1,4 @@
import { strict as assert } from 'node:assert/strict'
import { configure, generateVAPIDKeys } from 'wildebeest/backend/src/config'
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
import * as accounts_following from 'wildebeest/functions/api/v1/accounts/[id]/following'
@ -418,8 +417,6 @@ describe('Mastodon APIs', () => {
test('get remote actor statuses', async () => {
const db = await makeDB()
await configure(db, { title: 'title', description: 'a', email: 'email' })
await generateVAPIDKeys(db)
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
@ -508,7 +505,6 @@ describe('Mastodon APIs', () => {
test('get remote actor statuses ignoring object that fail to download', async () => {
const db = await makeDB()
await generateVAPIDKeys(db)
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
await createPublicNote(domain, db, 'my localnote status', actor)

Wyświetl plik

@ -1,4 +1,5 @@
import * as notifications_get from 'wildebeest/functions/api/v1/notifications/[id]'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
import { createNotification, insertFollowNotification } from 'wildebeest/backend/src/mastodon/notification'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
@ -7,13 +8,13 @@ import { makeDB, assertJSON, createTestClient } from '../utils'
import { strict as assert } from 'node:assert/strict'
import { sendLikeNotification } from 'wildebeest/backend/src/mastodon/notification'
import { createSubscription } from 'wildebeest/backend/src/mastodon/subscription'
import { generateVAPIDKeys, configure } from 'wildebeest/backend/src/config'
import { arrayBufferToBase64 } from 'wildebeest/backend/src/utils/key-ops'
import { getNotifications } from 'wildebeest/backend/src/mastodon/notification'
const userKEK = 'test_kek15'
const domain = 'cloudflare.com'
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const vapidKeys = {} as JWK
function parseCryptoKey(s: string): any {
const parts = s.split(';')
@ -92,8 +93,6 @@ describe('Mastodon APIs', () => {
test('send like notification', async () => {
const db = await makeDB()
await generateVAPIDKeys(db)
await configure(db, { title: 'title', description: 'a', email: 'email' })
const clientKeys = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
'sign',
@ -140,7 +139,7 @@ describe('Mastodon APIs', () => {
})
const fromActor = await createPerson(domain, db, userKEK, 'from@cloudflare.com')
await sendLikeNotification(db, fromActor, actor, 'notifid')
await sendLikeNotification(db, fromActor, actor, 'notifid', 'admin@example.com', vapidKeys)
})
})
})

Wyświetl plik

@ -1,48 +0,0 @@
import * as startInstance from 'wildebeest/functions/start-instance'
import { TEST_JWT, ACCESS_CERTS } from './test-data'
import { strict as assert } from 'node:assert/strict'
import { makeDB } from './utils'
const accessDomain = 'access.com'
const accessAud = 'abcd'
describe('Wildebeest', () => {
globalThis.fetch = async (input: RequestInfo) => {
if (input === 'https://' + accessDomain + '/cdn-cgi/access/certs') {
return new Response(JSON.stringify(ACCESS_CERTS))
}
if (input === 'https://' + accessDomain + '/cdn-cgi/access/get-identity') {
return new Response(
JSON.stringify({
email: 'some@cloudflare.com',
})
)
}
throw new Error('unexpected request to ' + input)
}
test('start instance should generate a VAPID key and store a JWK', async () => {
const db = await makeDB()
const body = JSON.stringify({
title: 'title',
description: 'description',
email: 'email',
})
const headers = {
cookie: 'CF_Authorization=' + TEST_JWT,
}
const req = new Request('https://example.com', { method: 'POST', body, headers })
const res = await startInstance.handlePostRequest(req, db, accessDomain, accessAud)
assert.equal(res.status, 201)
const { value } = await db.prepare("SELECT value FROM instance_config WHERE key = 'vapid_jwk'").first()
const jwk = JSON.parse(value)
assert.equal(jwk.key_ops.length, 1)
assert.equal(jwk.key_ops[0], 'sign')
assert.equal(jwk.crv, 'P-256')
})
})

Wyświetl plik

@ -3,20 +3,12 @@ import * as statusesAPI from 'wildebeest/functions/api/v1/statuses'
import { statuses } from 'wildebeest/frontend/src/dummyData'
import type { MastodonStatus } from 'wildebeest/frontend/src/types'
import type { MastodonAccount } from 'wildebeest/backend/src/types'
import { configure } from 'wildebeest/backend/src/config'
const kek = 'test-kek'
/**
* Run helper commands to initialize the database with actors, statuses, etc.
*/
export async function init(domain: string, db: D1Database) {
configure(db, {
title: 'Wildebeest',
email: '',
description: 'Wildebeest dev instance',
thumbnail: '/assets/wildebeest-logo.png',
})
for (const status of statuses as MastodonStatus[]) {
const actor = await getOrCreatePerson(domain, db, status.account.username)
await createStatus(db, actor, status.content)

Wyświetl plik

@ -1,73 +0,0 @@
import { $, component$, useStore, useClientEffect$, useSignal } from '@builder.io/qwik'
import { DocumentHead } from '@builder.io/qwik-city'
import { WildebeestLogo } from '~/components/MastodonLogo'
import { useDomain } from '~/utils/useDomain'
import Step1 from './step-1'
import { type InstanceConfig, testInstance } from './utils'
export default component$(() => {
const domain = useDomain()
const loading = useSignal(true)
const instanceConfigured = useSignal(false)
const instanceConfig = useStore<InstanceConfig>({
title: `${domain} Wildebeest`,
email: `admin@${domain}`,
description: 'My personal Wildebeest instance (powered by Cloudflare)',
})
useClientEffect$(async () => {
if (await testInstance()) {
instanceConfigured.value = true
}
loading.value = false
})
const getStepToShow = () => {
if (loading.value) return 'loading'
if (!instanceConfigured.value) return 'step-1'
return 'all-good'
}
const stepToShow = getStepToShow()
const setLoading = $((value: boolean) => {
loading.value = value
})
const setInstanceConfigured = $((value: boolean) => {
instanceConfigured.value = value
})
return (
<div class="flex flex-col p-5 items-center max-w-lg mx-auto">
<h1 class="text-center mt-7 mb-9 flex items-center">
<WildebeestLogo size="large" />
</h1>
{stepToShow.startsWith('step-') && (
<div class="text-center">
<p class="mb-1">Welcome to Wildebeest...</p>
<p class="mb-5"> Your instance hasn't been configured yet.</p>
</div>
)}
{stepToShow === 'loading' && <p>Loading...</p>}
{stepToShow === 'step-1' && (
<Step1 instanceConfig={instanceConfig} setLoading={setLoading} setInstanceConfigured={setInstanceConfigured} />
)}
{stepToShow === 'all-good' && <p class="text-center">All good, your instance is ready.</p>}
</div>
)
})
export const head: DocumentHead = () => {
return {
title: 'Wildebeest Start Instance',
meta: [
{
name: 'description',
content: 'Wildebeest Instance Setup page',
},
],
}
}

Wyświetl plik

@ -1,80 +0,0 @@
import { component$, QRL } from '@builder.io/qwik'
import { configure, type InstanceConfig, testInstance } from './utils'
interface Props {
instanceConfig: InstanceConfig
setLoading: QRL<(loading: boolean) => void>
setInstanceConfigured: QRL<(configured: boolean) => void>
}
export default component$<Props>(({ instanceConfig, setLoading, setInstanceConfigured }) => {
return (
<>
<h2 class="mb-5">Configure your instance</h2>
<div class="flex flex-col mb-6 w-full max-w-md">
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-title">
Title
</label>
<div class="flex justify-center items-center flex-wrap gap-1">
<input
id="start-instance-title"
name="title"
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 invalid:border-red-400 flex-1 w-full"
value={instanceConfig.title}
onInput$={(ev) => (instanceConfig.title = (ev.target as HTMLInputElement).value)}
/>
</div>
</div>
<div class="flex flex-col mb-6 w-full max-w-md">
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-email">
Administrator email
</label>
<div class="flex justify-center items-center flex-wrap gap-1">
<input
id="start-instance-email"
name="email"
type="email"
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 invalid:border-red-400 flex-1 w-full"
value={instanceConfig.email}
onInput$={(ev) => (instanceConfig.email = (ev.target as HTMLInputElement).value)}
/>
</div>
</div>
<div class="flex flex-col mb-6 w-full max-w-md">
<label class="mb-2 max-w-max text-semi text-sm" for="start-instance-description">
Description
</label>
<div class="flex justify-center items-center flex-wrap gap-1">
<input
id="start-instance-description"
name="description"
class="bg-black text-white p-3 rounded outline-none border border-black hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 invalid:border-red-400 flex-1 w-full"
value={instanceConfig.description}
onInput$={(ev) => (instanceConfig.description = (ev.target as HTMLInputElement).value)}
/>
</div>
</div>
<button
type="submit"
class="mb-9 bg-wildebeest-vibrant-500 hover:bg-wildebeest-vibrant-600 p-3 text-white text-uppercase border-wildebeest-vibrant-500 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-600 focus:border-wildebeest-vibrant-600"
preventdefault:click
onClick$={async () => {
setLoading(true)
await configure(instanceConfig)
if (await testInstance()) {
setInstanceConfigured(true)
}
setLoading(false)
}}
>
Configure and start your instance
</button>
</>
)
})

Wyświetl plik

@ -1,21 +0,0 @@
export type InstanceConfig = {
title?: string
email?: string
description?: string
}
export async function configure(data: InstanceConfig) {
const res = await fetch('/start-instance', {
method: 'POST',
body: JSON.stringify(data),
})
if (!res.ok) {
throw new Error('/start-instance returned: ' + res.status)
}
}
export async function testInstance(): Promise<boolean> {
const res = await fetch('/api/v1/instance')
const data = await res.json<{ title?: string }>()
return !!data.title
}

Wyświetl plik

@ -1,4 +1,5 @@
import { component$, Slot, useContextProvider } from '@builder.io/qwik'
import type { Env } from 'wildebeest/backend/src/types/env'
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
import * as instance from 'wildebeest/functions/api/v1/instance'
import type { InstanceConfig } from 'wildebeest/backend/src/types/configs'
@ -8,18 +9,24 @@ import { WildebeestLogo } from '~/components/MastodonLogo'
import { getCommitHash } from '~/utils/getCommitHash'
import { InstanceConfigContext } from '~/utils/instanceConfig'
export const instanceLoader = loader$<{ DATABASE: D1Database }, Promise<InstanceConfig>>(
async ({ platform, redirect }) => {
const response = await instance.handleRequest('', platform.DATABASE)
const results = await response.text()
const json = JSON.parse(results) as InstanceConfig
if (!json.title) {
// If there is no title set then we have not configured the instance
throw redirect(302, '/start-instance')
}
return json
export const instanceLoader = loader$<
{ DATABASE: D1Database; INSTANCE_TITLE: string; INSTANCE_DESCR: string; ADMIN_EMAIL: string },
Promise<InstanceConfig>
>(async ({ platform, redirect }) => {
const env = {
INSTANCE_DESCR: platform.INSTANCE_DESCR,
INSTANCE_TITLE: platform.INSTANCE_TITLE,
ADMIN_EMAIL: platform.ADMIN_EMAIL,
} as Env
const response = await instance.handleRequest('', platform.DATABASE, env)
const results = await response.text()
const json = JSON.parse(results) as InstanceConfig
if (!json.title) {
// If there is no title set then we have not configured the instance
throw redirect(302, '/start-instance')
}
)
return json
})
export default component$(() => {
useContextProvider(InstanceConfigContext, instanceLoader.use().value)

Wyświetl plik

@ -1,4 +1,5 @@
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import type { Env } from 'wildebeest/backend/src/types/env'
import * as actors from 'wildebeest/backend/src/activitypub/actors'
@ -9,6 +10,7 @@ import { fetchKey, verifySignature } from 'wildebeest/backend/src/utils/httpsigj
import { generateDigestHeader } from 'wildebeest/backend/src/utils/http-signing-cavage'
import * as timeline from 'wildebeest/backend/src/mastodon/timeline'
import * as notification from 'wildebeest/backend/src/mastodon/notification'
import { getVAPIDKeys } from 'wildebeest/backend/src/config'
export const onRequest: PagesFunction<Env, any> = async ({ params, request, env, waitUntil }) => {
const parsedSignature = parseRequest(request)
@ -29,7 +31,17 @@ export const onRequest: PagesFunction<Env, any> = async ({ params, request, env,
const activity: Activity = JSON.parse(body)
const domain = new URL(request.url).hostname
return handleRequest(domain, env.DATABASE, env.KV_CACHE, params.id as string, activity, env.userKEK, waitUntil)
return handleRequest(
domain,
env.DATABASE,
env.KV_CACHE,
params.id as string,
activity,
env.userKEK,
waitUntil,
env.ADMIN_EMAIL,
getVAPIDKeys(env)
)
}
export async function handleRequest(
@ -39,7 +51,9 @@ export async function handleRequest(
id: string,
activity: Activity,
userKEK: string,
waitUntil: (p: Promise<any>) => void
waitUntil: (p: Promise<any>) => void,
adminEmail: string,
vapidKeys: JWK
): Promise<Response> {
const handle = parseHandle(id)
@ -53,7 +67,7 @@ export async function handleRequest(
return new Response('', { status: 404 })
}
await activityHandler.handle(domain, activity, db, userKEK)
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
// Assuming we received new posts or a like, pregenerate the user's timelines
// and notifications.

Wyświetl plik

@ -1,7 +1,9 @@
import { ContextData } from 'wildebeest/backend/src/types/context'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import { Env } from 'wildebeest/backend/src/types/env'
import { createClient } from 'wildebeest/backend/src/mastodon/client'
import { getVAPIDKeys, VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
import { getVAPIDKeys } from 'wildebeest/backend/src/config'
type AppsPost = {
redirect_uris: string
@ -11,10 +13,10 @@ type AppsPost = {
}
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request, env }) => {
return handleRequest(env.DATABASE, request)
return handleRequest(env.DATABASE, request, getVAPIDKeys(env))
}
export async function handleRequest(db: D1Database, request: Request) {
export async function handleRequest(db: D1Database, request: Request, vapidKeys: JWK) {
if (request.method !== 'POST') {
return new Response('', { status: 400 })
}
@ -22,7 +24,7 @@ export async function handleRequest(db: D1Database, request: Request) {
const body = await request.json<AppsPost>()
const client = await createClient(db, body.client_name, body.redirect_uris, body.website, body.scopes)
const vapidKey = VAPIDPublicKey(await getVAPIDKeys(db))
const vapidKey = VAPIDPublicKey(vapidKeys)
const res = {
name: body.client_name,

Wyświetl plik

@ -1,46 +1,37 @@
import type { Env } from 'wildebeest/backend/src/types/env'
import { DEFAULT_THUMBNAIL } from 'wildebeest/backend/src/config'
const INSTANCE_VERSION = '4.0.2'
export const onRequest: PagesFunction<Env, any> = async ({ env, request }) => {
const domain = new URL(request.url).hostname
return handleRequest(domain, env.DATABASE)
return handleRequest(domain, env)
}
export async function handleRequest(domain: string, db: D1Database) {
export async function handleRequest(domain: string, env: Env) {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'content-type, authorization',
'content-type': 'application/json; charset=utf-8',
}
const query = `
SELECT * FROM instance_config WHERE key IN ('title', 'description', 'email', 'short_description', 'thumbnail')
`
const { results, error, success } = await db.prepare(query).all()
if (!success) {
throw new Error('SQL error: ' + error)
}
const res: any = {}
if (results) {
for (let i = 0, len = results.length; i < len; i++) {
const row: any = results[i]
res[row.key] = row.value
}
}
res.thumbnail = DEFAULT_THUMBNAIL
// Registration is disabled because unsupported by Wildebeest. Users
// should go through the login flow and authenticate with Access.
// The documentation is incorrect and registrations is a boolean.
res.registrations = false
res.version = INSTANCE_VERSION
res.rules = []
res.uri = domain
res.title = env.INSTANCE_TITLE
res.email = env.ADMIN_EMAIL
res.description = env.INSTANCE_DESCR
if (!res.short_description) {
res.short_description = res.description
}
res.short_description = res.description
return new Response(JSON.stringify(res), { headers })
}

Wyświetl plik

@ -1,18 +1,20 @@
import { getClientById } from 'wildebeest/backend/src/mastodon/client'
import { getVAPIDKeys } from 'wildebeest/backend/src/config'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
import { createSubscription, getSubscription } from 'wildebeest/backend/src/mastodon/subscription'
import type { CreateRequest } from 'wildebeest/backend/src/mastodon/subscription'
import { ContextData } from 'wildebeest/backend/src/types/context'
import { Env } from 'wildebeest/backend/src/types/env'
import * as errors from 'wildebeest/backend/src/errors'
import { getVAPIDKeys, VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
return handleGetRequest(env.DATABASE, request, data.connectedActor, data.clientId)
}
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
return handlePostRequest(env.DATABASE, request, data.connectedActor, data.clientId)
return handlePostRequest(env.DATABASE, request, data.connectedActor, data.clientId, getVAPIDKeys(env))
}
const headers = {
@ -52,7 +54,13 @@ export async function handleGetRequest(db: D1Database, request: Request, connect
return new Response(JSON.stringify(res), { headers })
}
export async function handlePostRequest(db: D1Database, request: Request, connectedActor: Actor, clientId: string) {
export async function handlePostRequest(
db: D1Database,
request: Request,
connectedActor: Actor,
clientId: string,
vapidKeys: JWK
) {
const client = await getClientById(db, clientId)
if (client === null) {
return errors.clientUnknown()
@ -66,7 +74,7 @@ export async function handlePostRequest(db: D1Database, request: Request, connec
subscription = await createSubscription(db, connectedActor, client, data)
}
const vapidKey = VAPIDPublicKey(await getVAPIDKeys(db))
const vapidKey = VAPIDPublicKey(vapidKeys)
const res = {
id: 4,

Wyświetl plik

@ -1,44 +1,29 @@
import type { Env } from 'wildebeest/backend/src/types/env'
import { DEFAULT_THUMBNAIL } from 'wildebeest/backend/src/config'
import type { InstanceConfigV2 } from 'wildebeest/backend/src/types/configs'
const INSTANCE_VERSION = '4.0.2'
export const onRequest: PagesFunction<Env, any> = async ({ env, request }) => {
const domain = new URL(request.url).hostname
return handleRequest(domain, env.DATABASE)
return handleRequest(domain, env.DATABASE, env)
}
export async function handleRequest(domain: string, db: D1Database) {
export async function handleRequest(domain: string, db: D1Database, env: Env) {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'content-type, authorization',
'content-type': 'application/json; charset=utf-8',
}
const query = `
SELECT * FROM instance_config WHERE key IN ('title', 'description', 'email', 'short_description', 'thumbnail')
`
const { results, error, success } = await db.prepare(query).all()
if (!success) {
throw new Error('SQL error: ' + error)
}
const config: any = {}
if (results) {
for (let i = 0, len = results.length; i < len; i++) {
const row: any = results[i]
config[row.key] = row.value
}
}
const res: InstanceConfigV2 = {
domain,
title: config.title,
title: env.INSTANCE_TITLE,
version: INSTANCE_VERSION,
source_url: 'https://github.com/cloudflare/wildebeest',
description: config.description,
description: env.INSTANCE_DESCR,
thumbnail: {
url: config.thumbnail,
url: DEFAULT_THUMBNAIL,
},
languages: ['en'],
registrations: {
@ -47,7 +32,7 @@ export async function handleRequest(domain: string, db: D1Database) {
enabled: false,
},
contact: {
email: config.email,
email: env.ADMIN_EMAIL,
},
rules: [],
}

Wyświetl plik

@ -1,47 +0,0 @@
import type { Env } from 'wildebeest/backend/src/types/env'
import * as access from 'wildebeest/backend/src/access'
import * as errors from 'wildebeest/backend/src/errors'
import { parse } from 'cookie'
import type { ContextData } from 'wildebeest/backend/src/types/context'
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ request, env }) => {
return handleGetRequest(env, request)
}
// Route to test if Access has been configured properly
export async function handleGetRequest(env: Env, request: Request): Promise<Response> {
const db = env.DATABASE
const query = `
SELECT * FROM instance_config WHERE key IN ('accessDomain', 'accessAud')
`
const { results, error, success } = await db.prepare(query).all()
if (!success) {
throw new Error('SQL error: ' + error)
}
const data: any = {}
if (results) {
for (let i = 0, len = results.length; i < len; i++) {
const row: any = results[i]
data[row.key] = row.value
}
}
const cookie = parse(request.headers.get('Cookie') || '')
const jwt = cookie['CF_Authorization']
if (!jwt) {
return errors.notAuthorized('missing authorization')
}
const domain = env.ACCESS_AUTH_DOMAIN
const validator = access.generateValidator({ jwt, domain, aud: env.ACCESS_AUD })
await validator(request)
const identity = await access.getIdentity({ jwt, domain })
if (!identity) {
return errors.notAuthorized('failed to load identity')
}
return new Response(JSON.stringify({ email: identity.email }))
}

Wyświetl plik

@ -1,56 +0,0 @@
// First screen to configure and start the instance
import type { Env } from 'wildebeest/backend/src/types/env'
import * as errors from 'wildebeest/backend/src/errors'
import * as access from 'wildebeest/backend/src/access'
import { parse } from 'cookie'
import type { InstanceConfig } from 'wildebeest/backend/src/config'
import * as config from 'wildebeest/backend/src/config'
export const onRequestPost: PagesFunction<Env, any> = async ({ request, env }) => {
return handlePostRequest(request, env.DATABASE, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD)
}
export const onRequestGet: PagesFunction<Env, any> = async (ctx) => {
const { request, env } = ctx
const cookie = parse(request.headers.get('Cookie') || '')
const jwt = cookie['CF_Authorization']
if (!jwt) {
const url = access.generateLoginURL({
redirectURL: new URL('/start-instance', 'https://' + env.DOMAIN),
domain: env.ACCESS_AUTH_DOMAIN,
aud: env.ACCESS_AUD,
})
return Response.redirect(url)
}
const frontend = await import('../frontend/server/entry.cloudflare-pages')
return frontend.onRequest(ctx)
}
export async function handlePostRequest(
request: Request,
db: D1Database,
accessDomain: string,
accessAud: string
): Promise<Response> {
const data = await request.json<InstanceConfig>()
const cookie = parse(request.headers.get('Cookie') || '')
const jwt = cookie['CF_Authorization']
if (!jwt) {
return new Response('', { status: 401 })
}
const validator = access.generateValidator({ jwt, domain: accessDomain, aud: accessAud })
await validator(request)
const identity = await access.getIdentity({ jwt, domain: accessDomain })
if (!identity) {
return errors.notAuthorized('failed to load identity')
}
await config.configure(db, data)
await config.generateVAPIDKeys(db)
return new Response('', { status: 201 })
}

Wyświetl plik

@ -139,11 +139,6 @@ CREATE TABLE IF NOT EXISTS subscriptions (
FOREIGN KEY(client_id) REFERENCES clients(id)
);
CREATE TABLE IF NOT EXISTS instance_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5 (
type,
name,

Wyświetl plik

@ -0,0 +1,6 @@
import { webcrypto } from 'node:crypto'
const key = await webcrypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
const serverKey = await webcrypto.subtle.exportKey("jwk", key.privateKey);
console.log(JSON.stringify(serverKey));

Wyświetl plik

@ -98,6 +98,7 @@ resource "cloudflare_pages_project" "wildebeest_pages_project" {
INSTANCE_TITLE = var.wd_instance_title
ADMIN_EMAIL = var.wd_admin_email
INSTANCE_DESCR = var.wd_instance_description
VAPID_JWK = sensitive(file("${path.module}/vapid_jwk"))
}
kv_namespaces = {
KV_CACHE = sensitive(cloudflare_workers_kv_namespace.wildebeest_cache.id)