kopia lustrzana https://github.com/cloudflare/wildebeest
MOW-95: use instance config in Env instead of DB
rodzic
0d01476ca0
commit
26fbb26cec
|
@ -109,6 +109,39 @@ jobs:
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||||
if: ${{ env.tfstate_kv != '' }}
|
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
|
- name: Configure
|
||||||
run: terraform plan && terraform apply -auto-approve
|
run: terraform plan && terraform apply -auto-approve
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
|
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 { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
||||||
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
|
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import * as objects from 'wildebeest/backend/src/activitypub/objects'
|
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
|
// The `object` field of the activity is required to be an object, with an
|
||||||
// `id` and a `type` field.
|
// `id` and a `type` field.
|
||||||
const requireComplexObject = () => {
|
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)
|
const notifId = await createNotification(db, 'mention', person, fromActor, obj)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
await addObjectInInbox(db, person, obj),
|
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
|
// Notify the user
|
||||||
const notifId = await insertFollowNotification(db, receiver, originalActor)
|
const notifId = await insertFollowNotification(db, receiver, originalActor)
|
||||||
await sendFollowNotification(db, originalActor, receiver, notifId)
|
await sendFollowNotification(db, originalActor, receiver, notifId, adminEmail, vapidKeys)
|
||||||
} else {
|
} else {
|
||||||
console.warn(`actor ${objectId} not found`)
|
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
|
// Store the reblog for counting
|
||||||
insertReblog(db, fromActor, obj),
|
insertReblog(db, fromActor, obj),
|
||||||
|
|
||||||
sendReblogNotification(db, fromActor, targetActor, notifId),
|
sendReblogNotification(db, fromActor, targetActor, notifId, adminEmail, vapidKeys),
|
||||||
])
|
])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -312,7 +320,7 @@ export async function handle(domain: string, activity: Activity, db: D1Database,
|
||||||
insertLike(db, fromActor, obj),
|
insertLike(db, fromActor, obj),
|
||||||
])
|
])
|
||||||
|
|
||||||
await sendLikeNotification(db, fromActor, targetActor, notifId)
|
await sendLikeNotification(db, fromActor, targetActor, notifId, adminEmail, vapidKeys)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,53 +1,10 @@
|
||||||
export type InstanceConfig = {
|
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||||
title?: string
|
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
||||||
email?: string
|
|
||||||
description?: string
|
|
||||||
thumbnail?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_THUMBNAIL =
|
export const DEFAULT_THUMBNAIL =
|
||||||
'https://imagedelivery.net/NkfPDviynOyTAOI79ar_GQ/b24caf12-5230-48c4-0bf7-2f40063bd400/thumbnail'
|
'https://imagedelivery.net/NkfPDviynOyTAOI79ar_GQ/b24caf12-5230-48c4-0bf7-2f40063bd400/thumbnail'
|
||||||
|
|
||||||
export async function configure(db: D1Database, data: InstanceConfig) {
|
export function getVAPIDKeys(env: Env): JWK {
|
||||||
const sql = `
|
const value: JWK = JSON.parse(env.VAPID_JWK)
|
||||||
INSERT INTO instance_config
|
return value
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Object } from 'wildebeest/backend/src/activitypub/objects'
|
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 * as actors from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
|
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
|
||||||
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
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 { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import type { NotificationType, Notification } from 'wildebeest/backend/src/types/notification'
|
import type { NotificationType, Notification } from 'wildebeest/backend/src/types/notification'
|
||||||
import { getSubscriptionForAllClients } from 'wildebeest/backend/src/mastodon/subscription'
|
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(
|
export async function createNotification(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
|
@ -43,9 +42,14 @@ export async function insertFollowNotification(db: D1Database, actor: Actor, fro
|
||||||
return row.id
|
return row.id
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendFollowNotification(db: D1Database, follower: Actor, actor: Actor, notificationId: string) {
|
export async function sendFollowNotification(
|
||||||
const sub = await config.get(db, 'email')
|
db: D1Database,
|
||||||
|
follower: Actor,
|
||||||
|
actor: Actor,
|
||||||
|
notificationId: string,
|
||||||
|
adminEmail: string,
|
||||||
|
vapidKeys: JWK
|
||||||
|
) {
|
||||||
const data = {
|
const data = {
|
||||||
preferred_locale: 'en',
|
preferred_locale: 'en',
|
||||||
notification_type: 'follow',
|
notification_type: 'follow',
|
||||||
|
@ -58,16 +62,21 @@ export async function sendFollowNotification(db: D1Database, follower: Actor, ac
|
||||||
const message: WebPushMessage = {
|
const message: WebPushMessage = {
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
urgency: 'normal',
|
urgency: 'normal',
|
||||||
sub,
|
sub: adminEmail,
|
||||||
ttl: 60 * 24 * 7,
|
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) {
|
export async function sendLikeNotification(
|
||||||
const sub = await config.get(db, 'email')
|
db: D1Database,
|
||||||
|
fromActor: Actor,
|
||||||
|
actor: Actor,
|
||||||
|
notificationId: string,
|
||||||
|
adminEmail: string,
|
||||||
|
vapidKeys: JWK
|
||||||
|
) {
|
||||||
const data = {
|
const data = {
|
||||||
preferred_locale: 'en',
|
preferred_locale: 'en',
|
||||||
notification_type: 'favourite',
|
notification_type: 'favourite',
|
||||||
|
@ -80,16 +89,21 @@ export async function sendLikeNotification(db: D1Database, fromActor: Actor, act
|
||||||
const message: WebPushMessage = {
|
const message: WebPushMessage = {
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
urgency: 'normal',
|
urgency: 'normal',
|
||||||
sub,
|
sub: adminEmail,
|
||||||
ttl: 60 * 24 * 7,
|
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) {
|
export async function sendMentionNotification(
|
||||||
const sub = await config.get(db, 'email')
|
db: D1Database,
|
||||||
|
fromActor: Actor,
|
||||||
|
actor: Actor,
|
||||||
|
notificationId: string,
|
||||||
|
adminEmail: string,
|
||||||
|
vapidKeys: JWK
|
||||||
|
) {
|
||||||
const data = {
|
const data = {
|
||||||
preferred_locale: 'en',
|
preferred_locale: 'en',
|
||||||
notification_type: 'mention',
|
notification_type: 'mention',
|
||||||
|
@ -102,16 +116,21 @@ export async function sendMentionNotification(db: D1Database, fromActor: Actor,
|
||||||
const message: WebPushMessage = {
|
const message: WebPushMessage = {
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
urgency: 'normal',
|
urgency: 'normal',
|
||||||
sub,
|
sub: adminEmail,
|
||||||
ttl: 60 * 24 * 7,
|
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) {
|
export async function sendReblogNotification(
|
||||||
const sub = await config.get(db, 'email')
|
db: D1Database,
|
||||||
|
fromActor: Actor,
|
||||||
|
actor: Actor,
|
||||||
|
notificationId: string,
|
||||||
|
adminEmail: string,
|
||||||
|
vapidKeys: JWK
|
||||||
|
) {
|
||||||
const data = {
|
const data = {
|
||||||
preferred_locale: 'en',
|
preferred_locale: 'en',
|
||||||
notification_type: 'reblog',
|
notification_type: 'reblog',
|
||||||
|
@ -124,15 +143,14 @@ export async function sendReblogNotification(db: D1Database, fromActor: Actor, a
|
||||||
const message: WebPushMessage = {
|
const message: WebPushMessage = {
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
urgency: 'normal',
|
urgency: 'normal',
|
||||||
sub,
|
sub: adminEmail,
|
||||||
ttl: 60 * 24 * 7,
|
ttl: 60 * 24 * 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendNotification(db, actor, message)
|
return sendNotification(db, actor, message, vapidKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendNotification(db: D1Database, actor: Actor, message: WebPushMessage) {
|
async function sendNotification(db: D1Database, actor: Actor, message: WebPushMessage, vapidKeys: JWK) {
|
||||||
const vapidKeys = await getVAPIDKeys(db)
|
|
||||||
const subscriptions = await getSubscriptionForAllClients(db, actor)
|
const subscriptions = await getSubscriptionForAllClients(db, actor)
|
||||||
|
|
||||||
const promises = subscriptions.map(async (subscription) => {
|
const promises = subscriptions.map(async (subscription) => {
|
||||||
|
|
|
@ -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 {
|
export function VAPIDPublicKey(keys: JWK): string {
|
||||||
return b64ToUrlEncoded(exportPublicKeyPair(keys))
|
return b64ToUrlEncoded(exportPublicKeyPair(keys))
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,4 +10,10 @@ export interface Env {
|
||||||
DOMAIN: string
|
DOMAIN: string
|
||||||
ACCESS_AUD: string
|
ACCESS_AUD: string
|
||||||
ACCESS_AUTH_DOMAIN: string
|
ACCESS_AUTH_DOMAIN: string
|
||||||
|
|
||||||
|
// Configuration for the instance
|
||||||
|
INSTANCE_TITLE: string
|
||||||
|
ADMIN_EMAIL: string
|
||||||
|
INSTANCE_DESCR: string
|
||||||
|
VAPID_JWK: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { makeDB, isUrlValid } from './utils'
|
import { makeDB, isUrlValid } from './utils'
|
||||||
|
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
||||||
import { addFollowing } from 'wildebeest/backend/src/mastodon/follow'
|
import { addFollowing } from 'wildebeest/backend/src/mastodon/follow'
|
||||||
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
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 * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
|
||||||
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||||
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
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'
|
import * as ap_outbox_page from 'wildebeest/functions/ap/users/[id]/outbox/page'
|
||||||
|
|
||||||
const userKEK = 'test_kek5'
|
const userKEK = 'test_kek5'
|
||||||
|
const vapidKeys = {} as JWK
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
const domain = 'cloudflare.com'
|
const domain = 'cloudflare.com'
|
||||||
|
const adminEmail = 'admin@example.com'
|
||||||
|
|
||||||
describe('ActivityPub', () => {
|
describe('ActivityPub', () => {
|
||||||
test('fetch non-existant user by id', async () => {
|
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
|
const row = await db
|
||||||
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
|
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
|
||||||
|
@ -96,7 +98,7 @@ describe('ActivityPub', () => {
|
||||||
object: 'a',
|
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',
|
message: '`activity.object` must be of type object',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -114,7 +116,7 @@ describe('ActivityPub', () => {
|
||||||
object: 'a',
|
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',
|
message: '`activity.object` must be of type object',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -131,7 +133,7 @@ describe('ActivityPub', () => {
|
||||||
object: 'a',
|
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',
|
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',
|
message: 'object https://example.com/note2 does not exist',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -174,7 +176,7 @@ describe('ActivityPub', () => {
|
||||||
object: object,
|
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',
|
message: 'actorid mismatch when updating object',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -204,7 +206,7 @@ describe('ActivityPub', () => {
|
||||||
object: newObject,
|
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()
|
const updatedObject = await db.prepare('SELECT * FROM objects WHERE original_object_id=?').bind(object.id).first()
|
||||||
assert(updatedObject)
|
assert(updatedObject)
|
||||||
|
@ -276,8 +278,6 @@ describe('ActivityPub', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
await configure(db, { title: 'title', description: 'a', email: 'email' })
|
|
||||||
await generateVAPIDKeys(db)
|
|
||||||
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
|
||||||
const activity: any = {
|
const activity: any = {
|
||||||
|
@ -287,7 +287,7 @@ describe('ActivityPub', () => {
|
||||||
cc: [],
|
cc: [],
|
||||||
object: objectId,
|
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()
|
const object = await db.prepare('SELECT * FROM objects').bind(remoteActorId).first()
|
||||||
assert(object)
|
assert(object)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
|
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_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_following_page from 'wildebeest/functions/ap/users/[id]/following/page'
|
||||||
import * as ap_followers from 'wildebeest/functions/ap/users/[id]/followers'
|
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 userKEK = 'test_kek10'
|
||||||
const domain = 'cloudflare.com'
|
const domain = 'cloudflare.com'
|
||||||
|
const adminEmail = 'admin@example.com'
|
||||||
|
const vapidKeys = {} as JWK
|
||||||
|
|
||||||
describe('ActivityPub', () => {
|
describe('ActivityPub', () => {
|
||||||
describe('Follow', () => {
|
describe('Follow', () => {
|
||||||
|
@ -34,8 +36,6 @@ describe('ActivityPub', () => {
|
||||||
|
|
||||||
test('Receive follow with Accept reply', async () => {
|
test('Receive follow with Accept reply', async () => {
|
||||||
const db = await makeDB()
|
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 actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
|
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ describe('ActivityPub', () => {
|
||||||
object: actor.id.toString(),
|
object: actor.id.toString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await activityHandler.handle(domain, activity, db, userKEK)
|
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
|
||||||
|
|
||||||
const row = await db
|
const row = await db
|
||||||
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
|
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
|
||||||
|
@ -132,8 +132,6 @@ describe('ActivityPub', () => {
|
||||||
|
|
||||||
test('creates a notification', async () => {
|
test('creates a notification', async () => {
|
||||||
const db = await makeDB()
|
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 actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
|
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
|
||||||
|
|
||||||
|
@ -144,7 +142,7 @@ describe('ActivityPub', () => {
|
||||||
object: actor.id,
|
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()
|
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
|
||||||
assert.equal(entry.type, 'follow')
|
assert.equal(entry.type, 'follow')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { makeDB } from '../utils'
|
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 * as objects from 'wildebeest/backend/src/activitypub/objects'
|
||||||
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||||
import * as ap_inbox from 'wildebeest/functions/ap/users/[id]/inbox'
|
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 userKEK = 'test_kek9'
|
||||||
const domain = 'cloudflare.com'
|
const domain = 'cloudflare.com'
|
||||||
|
const adminEmail = 'admin@example.com'
|
||||||
|
const vapidKeys = {} as JWK
|
||||||
|
|
||||||
const kv_cache: any = {
|
const kv_cache: any = {
|
||||||
async put() {},
|
async put() {},
|
||||||
|
@ -20,14 +22,22 @@ describe('ActivityPub', () => {
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
|
|
||||||
const activity: any = {}
|
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)
|
assert.equal(res.status, 404)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('send Note to inbox stores in DB', async () => {
|
test('send Note to inbox stores in DB', async () => {
|
||||||
const db = await makeDB()
|
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 actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
|
||||||
const activity: any = {
|
const activity: any = {
|
||||||
|
@ -41,7 +51,17 @@ describe('ActivityPub', () => {
|
||||||
content: 'test note',
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db
|
const entry = await db
|
||||||
|
@ -81,7 +101,17 @@ describe('ActivityPub', () => {
|
||||||
content: 'test note',
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM outbox_objects WHERE actor_id=?').bind(remoteActorId).first()
|
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 () => {
|
test('local actor sends Note with mention create notification', async () => {
|
||||||
const db = await makeDB()
|
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 actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
||||||
|
|
||||||
|
@ -106,7 +134,17 @@ describe('ActivityPub', () => {
|
||||||
content: 'test note',
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
|
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
|
||||||
|
@ -132,8 +170,6 @@ describe('ActivityPub', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = await makeDB()
|
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 actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
|
|
||||||
const activity: any = {
|
const activity: any = {
|
||||||
|
@ -147,7 +183,17 @@ describe('ActivityPub', () => {
|
||||||
content: 'test note',
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM actors WHERE id=?').bind(actorB).first()
|
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 () => {
|
test('send Note records reply', async () => {
|
||||||
const db = await makeDB()
|
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 actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -171,7 +215,17 @@ describe('ActivityPub', () => {
|
||||||
content: 'post',
|
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)
|
assert.equal(res.status, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +241,17 @@ describe('ActivityPub', () => {
|
||||||
content: 'reply',
|
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)
|
assert.equal(res.status, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,8 +270,6 @@ describe('ActivityPub', () => {
|
||||||
describe('Announce', () => {
|
describe('Announce', () => {
|
||||||
test('records reblog in db', async () => {
|
test('records reblog in db', async () => {
|
||||||
const db = await makeDB()
|
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 actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
||||||
|
|
||||||
|
@ -218,7 +280,17 @@ describe('ActivityPub', () => {
|
||||||
actor: actorB.id,
|
actor: actorB.id,
|
||||||
object: note.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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM actor_reblogs').first()
|
const entry = await db.prepare('SELECT * FROM actor_reblogs').first()
|
||||||
|
@ -228,8 +300,6 @@ describe('ActivityPub', () => {
|
||||||
|
|
||||||
test('creates notification', async () => {
|
test('creates notification', async () => {
|
||||||
const db = await makeDB()
|
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 actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
||||||
|
|
||||||
|
@ -240,7 +310,17 @@ describe('ActivityPub', () => {
|
||||||
actor: actorB.id,
|
actor: actorB.id,
|
||||||
object: note.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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
|
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
|
||||||
|
@ -254,8 +334,6 @@ describe('ActivityPub', () => {
|
||||||
describe('Like', () => {
|
describe('Like', () => {
|
||||||
test('records like in db', async () => {
|
test('records like in db', async () => {
|
||||||
const db = await makeDB()
|
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 actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
||||||
|
|
||||||
|
@ -266,7 +344,17 @@ describe('ActivityPub', () => {
|
||||||
actor: actorB.id,
|
actor: actorB.id,
|
||||||
object: note.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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
|
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
|
||||||
|
@ -276,8 +364,6 @@ describe('ActivityPub', () => {
|
||||||
|
|
||||||
test('creates notification', async () => {
|
test('creates notification', async () => {
|
||||||
const db = await makeDB()
|
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 actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
||||||
|
|
||||||
|
@ -288,7 +374,17 @@ describe('ActivityPub', () => {
|
||||||
actor: actorB.id,
|
actor: actorB.id,
|
||||||
object: note.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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
|
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
|
||||||
|
@ -299,8 +395,6 @@ describe('ActivityPub', () => {
|
||||||
|
|
||||||
test('records like in db', async () => {
|
test('records like in db', async () => {
|
||||||
const db = await makeDB()
|
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 actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
|
||||||
|
|
||||||
|
@ -311,7 +405,17 @@ describe('ActivityPub', () => {
|
||||||
actor: actorB.id,
|
actor: actorB.id,
|
||||||
object: note.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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
|
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { strict as assert } from 'node:assert/strict'
|
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 v1_instance from 'wildebeest/functions/api/v1/instance'
|
||||||
import * as v2_instance from 'wildebeest/functions/api/v2/instance'
|
import * as v2_instance from 'wildebeest/functions/api/v2/instance'
|
||||||
import * as apps from 'wildebeest/functions/api/v1/apps'
|
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 { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { createSubscription } from '../src/mastodon/subscription'
|
import { createSubscription } from '../src/mastodon/subscription'
|
||||||
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
|
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
|
||||||
import { configure, generateVAPIDKeys } from 'wildebeest/backend/src/config'
|
|
||||||
|
|
||||||
const userKEK = 'test_kek'
|
const userKEK = 'test_kek'
|
||||||
const domain = 'cloudflare.com'
|
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('Mastodon APIs', () => {
|
||||||
describe('instance', () => {
|
describe('instance', () => {
|
||||||
test('return the instance infos v1', async () => {
|
test('return the instance infos v1', async () => {
|
||||||
const db = await makeDB()
|
const env = {
|
||||||
const data = {
|
INSTANCE_TITLE: 'a',
|
||||||
title: 'title',
|
ADMIN_EMAIL: 'b',
|
||||||
uri: 'uri',
|
INSTANCE_DESCR: 'c',
|
||||||
email: 'email',
|
} as Env
|
||||||
description: 'description',
|
|
||||||
}
|
|
||||||
await configure(db, data)
|
|
||||||
|
|
||||||
const res = await v1_instance.handleRequest(domain, db)
|
const res = await v1_instance.handleRequest(domain, env)
|
||||||
assert.equal(res.status, 200)
|
assert.equal(res.status, 200)
|
||||||
assertCORS(res)
|
assertCORS(res)
|
||||||
assertJSON(res)
|
assertJSON(res)
|
||||||
|
@ -35,55 +42,60 @@ describe('Mastodon APIs', () => {
|
||||||
const data = await res.json<any>()
|
const data = await res.json<any>()
|
||||||
assert.equal(data.rules.length, 0)
|
assert.equal(data.rules.length, 0)
|
||||||
assert.equal(data.uri, domain)
|
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 () => {
|
test('adds a short_description if missing v1', async () => {
|
||||||
const db = await makeDB()
|
const env = {
|
||||||
const data = {
|
INSTANCE_DESCR: 'c',
|
||||||
title: 'title',
|
} as Env
|
||||||
uri: 'uri',
|
|
||||||
email: 'email',
|
|
||||||
description: 'description',
|
|
||||||
}
|
|
||||||
await configure(db, data)
|
|
||||||
|
|
||||||
const res = await v1_instance.handleRequest(domain, db)
|
const res = await v1_instance.handleRequest(domain, env)
|
||||||
assert.equal(res.status, 200)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
{
|
{
|
||||||
const data = await res.json<any>()
|
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 () => {
|
test('return the instance infos v2', async () => {
|
||||||
const db = await makeDB()
|
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)
|
assert.equal(res.status, 200)
|
||||||
assertCORS(res)
|
assertCORS(res)
|
||||||
assertJSON(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', () => {
|
describe('apps', () => {
|
||||||
test('return the app infos', async () => {
|
test('return the app infos', async () => {
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
await generateVAPIDKeys(db)
|
const vapidKeys = await generateVAPIDKeys()
|
||||||
const request = new Request('https://example.com', {
|
const request = new Request('https://example.com', {
|
||||||
method: 'POST',
|
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"}',
|
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)
|
assert.equal(res.status, 200)
|
||||||
assertCORS(res)
|
assertCORS(res)
|
||||||
assertJSON(res)
|
assertJSON(res)
|
||||||
|
@ -100,11 +112,14 @@ describe('Mastodon APIs', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns 404 for GET request', async () => {
|
test('returns 404 for GET request', async () => {
|
||||||
|
const vapidKeys = await generateVAPIDKeys()
|
||||||
const request = new Request('https://example.com')
|
const request = new Request('https://example.com')
|
||||||
const ctx: any = {
|
const ctx: any = {
|
||||||
next: () => new Response(),
|
next: () => new Response(),
|
||||||
data: null,
|
data: null,
|
||||||
env: {},
|
env: {
|
||||||
|
VAPID_JWK: JSON.stringify(vapidKeys),
|
||||||
|
},
|
||||||
request,
|
request,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,8 +184,8 @@ describe('Mastodon APIs', () => {
|
||||||
test('create subscription', async () => {
|
test('create subscription', async () => {
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
const client = await createTestClient(db)
|
const client = await createTestClient(db)
|
||||||
await generateVAPIDKeys(db)
|
|
||||||
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
const vapidKeys = await generateVAPIDKeys()
|
||||||
|
|
||||||
const data: any = {
|
const data: any = {
|
||||||
subscription: {
|
subscription: {
|
||||||
|
@ -190,7 +205,7 @@ describe('Mastodon APIs', () => {
|
||||||
body: JSON.stringify(data),
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const row: any = await db.prepare('SELECT * FROM subscriptions').first()
|
const row: any = await db.prepare('SELECT * FROM subscriptions').first()
|
||||||
|
@ -202,8 +217,8 @@ describe('Mastodon APIs', () => {
|
||||||
test('create subscriptions only creates one', async () => {
|
test('create subscriptions only creates one', async () => {
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
const client = await createTestClient(db)
|
const client = await createTestClient(db)
|
||||||
await generateVAPIDKeys(db)
|
|
||||||
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
const vapidKeys = await generateVAPIDKeys()
|
||||||
|
|
||||||
const data: any = {
|
const data: any = {
|
||||||
subscription: {
|
subscription: {
|
||||||
|
@ -225,7 +240,7 @@ describe('Mastodon APIs', () => {
|
||||||
body: JSON.stringify(data),
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const { count } = await db.prepare('SELECT count(*) as count FROM subscriptions').first()
|
const { count } = await db.prepare('SELECT count(*) as count FROM subscriptions').first()
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { strict as assert } from 'node:assert/strict'
|
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 { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
||||||
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||||
import * as accounts_following from 'wildebeest/functions/api/v1/accounts/[id]/following'
|
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 () => {
|
test('get remote actor statuses', async () => {
|
||||||
const db = await makeDB()
|
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 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 () => {
|
test('get remote actor statuses ignoring object that fail to download', async () => {
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
await generateVAPIDKeys(db)
|
|
||||||
|
|
||||||
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
await createPublicNote(domain, db, 'my localnote status', actor)
|
await createPublicNote(domain, db, 'my localnote status', actor)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as notifications_get from 'wildebeest/functions/api/v1/notifications/[id]'
|
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 { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||||
import { createNotification, insertFollowNotification } from 'wildebeest/backend/src/mastodon/notification'
|
import { createNotification, insertFollowNotification } from 'wildebeest/backend/src/mastodon/notification'
|
||||||
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
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 { strict as assert } from 'node:assert/strict'
|
||||||
import { sendLikeNotification } from 'wildebeest/backend/src/mastodon/notification'
|
import { sendLikeNotification } from 'wildebeest/backend/src/mastodon/notification'
|
||||||
import { createSubscription } from 'wildebeest/backend/src/mastodon/subscription'
|
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 { arrayBufferToBase64 } from 'wildebeest/backend/src/utils/key-ops'
|
||||||
import { getNotifications } from 'wildebeest/backend/src/mastodon/notification'
|
import { getNotifications } from 'wildebeest/backend/src/mastodon/notification'
|
||||||
|
|
||||||
const userKEK = 'test_kek15'
|
const userKEK = 'test_kek15'
|
||||||
const domain = 'cloudflare.com'
|
const domain = 'cloudflare.com'
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||||
|
const vapidKeys = {} as JWK
|
||||||
|
|
||||||
function parseCryptoKey(s: string): any {
|
function parseCryptoKey(s: string): any {
|
||||||
const parts = s.split(';')
|
const parts = s.split(';')
|
||||||
|
@ -92,8 +93,6 @@ describe('Mastodon APIs', () => {
|
||||||
|
|
||||||
test('send like notification', async () => {
|
test('send like notification', async () => {
|
||||||
const db = await makeDB()
|
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, [
|
const clientKeys = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
|
||||||
'sign',
|
'sign',
|
||||||
|
@ -140,7 +139,7 @@ describe('Mastodon APIs', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const fromActor = await createPerson(domain, db, userKEK, 'from@cloudflare.com')
|
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -3,20 +3,12 @@ import * as statusesAPI from 'wildebeest/functions/api/v1/statuses'
|
||||||
import { statuses } from 'wildebeest/frontend/src/dummyData'
|
import { statuses } from 'wildebeest/frontend/src/dummyData'
|
||||||
import type { MastodonStatus } from 'wildebeest/frontend/src/types'
|
import type { MastodonStatus } from 'wildebeest/frontend/src/types'
|
||||||
import type { MastodonAccount } from 'wildebeest/backend/src/types'
|
import type { MastodonAccount } from 'wildebeest/backend/src/types'
|
||||||
import { configure } from 'wildebeest/backend/src/config'
|
|
||||||
|
|
||||||
const kek = 'test-kek'
|
const kek = 'test-kek'
|
||||||
/**
|
/**
|
||||||
* Run helper commands to initialize the database with actors, statuses, etc.
|
* Run helper commands to initialize the database with actors, statuses, etc.
|
||||||
*/
|
*/
|
||||||
export async function init(domain: string, db: D1Database) {
|
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[]) {
|
for (const status of statuses as MastodonStatus[]) {
|
||||||
const actor = await getOrCreatePerson(domain, db, status.account.username)
|
const actor = await getOrCreatePerson(domain, db, status.account.username)
|
||||||
await createStatus(db, actor, status.content)
|
await createStatus(db, actor, status.content)
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { component$, Slot, useContextProvider } from '@builder.io/qwik'
|
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 { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
||||||
import * as instance from 'wildebeest/functions/api/v1/instance'
|
import * as instance from 'wildebeest/functions/api/v1/instance'
|
||||||
import type { InstanceConfig } from 'wildebeest/backend/src/types/configs'
|
import type { InstanceConfig } from 'wildebeest/backend/src/types/configs'
|
||||||
|
@ -8,18 +9,24 @@ import { WildebeestLogo } from '~/components/MastodonLogo'
|
||||||
import { getCommitHash } from '~/utils/getCommitHash'
|
import { getCommitHash } from '~/utils/getCommitHash'
|
||||||
import { InstanceConfigContext } from '~/utils/instanceConfig'
|
import { InstanceConfigContext } from '~/utils/instanceConfig'
|
||||||
|
|
||||||
export const instanceLoader = loader$<{ DATABASE: D1Database }, Promise<InstanceConfig>>(
|
export const instanceLoader = loader$<
|
||||||
async ({ platform, redirect }) => {
|
{ DATABASE: D1Database; INSTANCE_TITLE: string; INSTANCE_DESCR: string; ADMIN_EMAIL: string },
|
||||||
const response = await instance.handleRequest('', platform.DATABASE)
|
Promise<InstanceConfig>
|
||||||
const results = await response.text()
|
>(async ({ platform, redirect }) => {
|
||||||
const json = JSON.parse(results) as InstanceConfig
|
const env = {
|
||||||
if (!json.title) {
|
INSTANCE_DESCR: platform.INSTANCE_DESCR,
|
||||||
// If there is no title set then we have not configured the instance
|
INSTANCE_TITLE: platform.INSTANCE_TITLE,
|
||||||
throw redirect(302, '/start-instance')
|
ADMIN_EMAIL: platform.ADMIN_EMAIL,
|
||||||
}
|
} as Env
|
||||||
return json
|
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$(() => {
|
export default component$(() => {
|
||||||
useContextProvider(InstanceConfigContext, instanceLoader.use().value)
|
useContextProvider(InstanceConfigContext, instanceLoader.use().value)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
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 * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
|
||||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
|
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 { generateDigestHeader } from 'wildebeest/backend/src/utils/http-signing-cavage'
|
||||||
import * as timeline from 'wildebeest/backend/src/mastodon/timeline'
|
import * as timeline from 'wildebeest/backend/src/mastodon/timeline'
|
||||||
import * as notification from 'wildebeest/backend/src/mastodon/notification'
|
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 }) => {
|
export const onRequest: PagesFunction<Env, any> = async ({ params, request, env, waitUntil }) => {
|
||||||
const parsedSignature = parseRequest(request)
|
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 activity: Activity = JSON.parse(body)
|
||||||
const domain = new URL(request.url).hostname
|
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(
|
export async function handleRequest(
|
||||||
|
@ -39,7 +51,9 @@ export async function handleRequest(
|
||||||
id: string,
|
id: string,
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
userKEK: string,
|
userKEK: string,
|
||||||
waitUntil: (p: Promise<any>) => void
|
waitUntil: (p: Promise<any>) => void,
|
||||||
|
adminEmail: string,
|
||||||
|
vapidKeys: JWK
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const handle = parseHandle(id)
|
const handle = parseHandle(id)
|
||||||
|
|
||||||
|
@ -53,7 +67,7 @@ export async function handleRequest(
|
||||||
return new Response('', { status: 404 })
|
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
|
// Assuming we received new posts or a like, pregenerate the user's timelines
|
||||||
// and notifications.
|
// and notifications.
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { ContextData } from 'wildebeest/backend/src/types/context'
|
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 { Env } from 'wildebeest/backend/src/types/env'
|
||||||
import { createClient } from 'wildebeest/backend/src/mastodon/client'
|
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 = {
|
type AppsPost = {
|
||||||
redirect_uris: string
|
redirect_uris: string
|
||||||
|
@ -11,10 +13,10 @@ type AppsPost = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request, env }) => {
|
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') {
|
if (request.method !== 'POST') {
|
||||||
return new Response('', { status: 400 })
|
return new Response('', { status: 400 })
|
||||||
}
|
}
|
||||||
|
@ -22,7 +24,7 @@ export async function handleRequest(db: D1Database, request: Request) {
|
||||||
const body = await request.json<AppsPost>()
|
const body = await request.json<AppsPost>()
|
||||||
|
|
||||||
const client = await createClient(db, body.client_name, body.redirect_uris, body.website, body.scopes)
|
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 = {
|
const res = {
|
||||||
name: body.client_name,
|
name: body.client_name,
|
||||||
|
|
|
@ -1,46 +1,37 @@
|
||||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||||
|
import { DEFAULT_THUMBNAIL } from 'wildebeest/backend/src/config'
|
||||||
|
|
||||||
const INSTANCE_VERSION = '4.0.2'
|
const INSTANCE_VERSION = '4.0.2'
|
||||||
|
|
||||||
export const onRequest: PagesFunction<Env, any> = async ({ env, request }) => {
|
export const onRequest: PagesFunction<Env, any> = async ({ env, request }) => {
|
||||||
const domain = new URL(request.url).hostname
|
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 = {
|
const headers = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Headers': 'content-type, authorization',
|
'Access-Control-Allow-Headers': 'content-type, authorization',
|
||||||
'content-type': 'application/json; charset=utf-8',
|
'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 = {}
|
const res: any = {}
|
||||||
if (results) {
|
|
||||||
for (let i = 0, len = results.length; i < len; i++) {
|
res.thumbnail = DEFAULT_THUMBNAIL
|
||||||
const row: any = results[i]
|
|
||||||
res[row.key] = row.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registration is disabled because unsupported by Wildebeest. Users
|
// Registration is disabled because unsupported by Wildebeest. Users
|
||||||
// should go through the login flow and authenticate with Access.
|
// should go through the login flow and authenticate with Access.
|
||||||
// The documentation is incorrect and registrations is a boolean.
|
// The documentation is incorrect and registrations is a boolean.
|
||||||
res.registrations = false
|
res.registrations = false
|
||||||
|
|
||||||
res.version = INSTANCE_VERSION
|
res.version = INSTANCE_VERSION
|
||||||
res.rules = []
|
res.rules = []
|
||||||
res.uri = domain
|
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 })
|
return new Response(JSON.stringify(res), { headers })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import { getClientById } from 'wildebeest/backend/src/mastodon/client'
|
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 type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { createSubscription, getSubscription } from 'wildebeest/backend/src/mastodon/subscription'
|
import { createSubscription, getSubscription } from 'wildebeest/backend/src/mastodon/subscription'
|
||||||
import type { CreateRequest } from 'wildebeest/backend/src/mastodon/subscription'
|
import type { CreateRequest } from 'wildebeest/backend/src/mastodon/subscription'
|
||||||
import { ContextData } from 'wildebeest/backend/src/types/context'
|
import { ContextData } from 'wildebeest/backend/src/types/context'
|
||||||
import { Env } from 'wildebeest/backend/src/types/env'
|
import { Env } from 'wildebeest/backend/src/types/env'
|
||||||
import * as errors from 'wildebeest/backend/src/errors'
|
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 }) => {
|
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
|
||||||
return handleGetRequest(env.DATABASE, request, data.connectedActor, data.clientId)
|
return handleGetRequest(env.DATABASE, request, data.connectedActor, data.clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
|
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 = {
|
const headers = {
|
||||||
|
@ -52,7 +54,13 @@ export async function handleGetRequest(db: D1Database, request: Request, connect
|
||||||
return new Response(JSON.stringify(res), { headers })
|
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)
|
const client = await getClientById(db, clientId)
|
||||||
if (client === null) {
|
if (client === null) {
|
||||||
return errors.clientUnknown()
|
return errors.clientUnknown()
|
||||||
|
@ -66,7 +74,7 @@ export async function handlePostRequest(db: D1Database, request: Request, connec
|
||||||
subscription = await createSubscription(db, connectedActor, client, data)
|
subscription = await createSubscription(db, connectedActor, client, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const vapidKey = VAPIDPublicKey(await getVAPIDKeys(db))
|
const vapidKey = VAPIDPublicKey(vapidKeys)
|
||||||
|
|
||||||
const res = {
|
const res = {
|
||||||
id: 4,
|
id: 4,
|
||||||
|
|
|
@ -1,44 +1,29 @@
|
||||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
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'
|
import type { InstanceConfigV2 } from 'wildebeest/backend/src/types/configs'
|
||||||
|
|
||||||
const INSTANCE_VERSION = '4.0.2'
|
const INSTANCE_VERSION = '4.0.2'
|
||||||
|
|
||||||
export const onRequest: PagesFunction<Env, any> = async ({ env, request }) => {
|
export const onRequest: PagesFunction<Env, any> = async ({ env, request }) => {
|
||||||
const domain = new URL(request.url).hostname
|
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 = {
|
const headers = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Headers': 'content-type, authorization',
|
'Access-Control-Allow-Headers': 'content-type, authorization',
|
||||||
'content-type': 'application/json; charset=utf-8',
|
'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 = {
|
const res: InstanceConfigV2 = {
|
||||||
domain,
|
domain,
|
||||||
title: config.title,
|
title: env.INSTANCE_TITLE,
|
||||||
version: INSTANCE_VERSION,
|
version: INSTANCE_VERSION,
|
||||||
source_url: 'https://github.com/cloudflare/wildebeest',
|
source_url: 'https://github.com/cloudflare/wildebeest',
|
||||||
description: config.description,
|
description: env.INSTANCE_DESCR,
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: config.thumbnail,
|
url: DEFAULT_THUMBNAIL,
|
||||||
},
|
},
|
||||||
languages: ['en'],
|
languages: ['en'],
|
||||||
registrations: {
|
registrations: {
|
||||||
|
@ -47,7 +32,7 @@ export async function handleRequest(domain: string, db: D1Database) {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
email: config.email,
|
email: env.ADMIN_EMAIL,
|
||||||
},
|
},
|
||||||
rules: [],
|
rules: [],
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }))
|
|
||||||
}
|
|
|
@ -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 })
|
|
||||||
}
|
|
|
@ -139,11 +139,6 @@ CREATE TABLE IF NOT EXISTS subscriptions (
|
||||||
FOREIGN KEY(client_id) REFERENCES clients(id)
|
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 (
|
CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5 (
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -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));
|
|
@ -98,6 +98,7 @@ resource "cloudflare_pages_project" "wildebeest_pages_project" {
|
||||||
INSTANCE_TITLE = var.wd_instance_title
|
INSTANCE_TITLE = var.wd_instance_title
|
||||||
ADMIN_EMAIL = var.wd_admin_email
|
ADMIN_EMAIL = var.wd_admin_email
|
||||||
INSTANCE_DESCR = var.wd_instance_description
|
INSTANCE_DESCR = var.wd_instance_description
|
||||||
|
VAPID_JWK = sensitive(file("${path.module}/vapid_jwk"))
|
||||||
}
|
}
|
||||||
kv_namespaces = {
|
kv_namespaces = {
|
||||||
KV_CACHE = sensitive(cloudflare_workers_kv_namespace.wildebeest_cache.id)
|
KV_CACHE = sensitive(cloudflare_workers_kv_namespace.wildebeest_cache.id)
|
||||||
|
|
Ładowanie…
Reference in New Issue