@evanp/activitypub-bot 0.38.4 → 0.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ export class RateLimiter {
11
11
  assert.strictEqual(typeof connection, 'object')
12
12
  assert.strictEqual(typeof logger, 'object')
13
13
  this.#connection = connection
14
- this.#logger = logger
14
+ this.#logger = logger.child({ class: this.constructor.name })
15
15
  }
16
16
 
17
17
  async limit (host, maxWaitTime = 30000) {
@@ -15,7 +15,7 @@ export class RemoteObjectCache {
15
15
  assert.ok(connection)
16
16
  assert.ok(logger)
17
17
  this.#connection = connection
18
- this.#logger = logger
18
+ this.#logger = logger.child({ class: this.constructor.name })
19
19
  }
20
20
 
21
21
  async get (id, username) {
@@ -1,10 +1,10 @@
1
1
  import assert from 'node:assert'
2
2
 
3
3
  import express from 'express'
4
- import createHttpError from 'http-errors'
5
4
 
6
5
  import as2 from '../activitystreams.js'
7
6
  import BotMaker from '../botmaker.js'
7
+ import { ProblemDetailsError, PrincipalNotAuthorizedError } from '../errors.js'
8
8
 
9
9
  const collections = ['outbox', 'liked', 'followers', 'following']
10
10
  const router = express.Router()
@@ -40,7 +40,7 @@ function collectionHandler (collection) {
40
40
  const { actorStorage, bots } = req.app.locals
41
41
  const bot = await BotMaker.makeBot(bots, username)
42
42
  if (!bot) {
43
- return next(createHttpError(404, `User ${username} not found`))
43
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
44
44
  }
45
45
  const coll = await actorStorage.getCollection(username, collection)
46
46
  res.status(200)
@@ -57,20 +57,20 @@ function collectionPageHandler (collection) {
57
57
  try {
58
58
  pageNo = parseInt(n)
59
59
  } catch (err) {
60
- return next(createHttpError(400, `Invalid page ${n}`))
60
+ return next(new ProblemDetailsError(400, `Invalid page ${n}`))
61
61
  }
62
62
  const { actorStorage, bots, authorizer, objectStorage, formatter, client } = req.app.locals
63
63
  const bot = await BotMaker.makeBot(bots, username)
64
64
 
65
65
  if (!bot) {
66
- return next(createHttpError(404, `User ${username} not found`))
66
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
67
67
  }
68
68
 
69
69
  if (collection === 'inbox') {
70
- return next(createHttpError(403, `No access to ${collection} collection`))
70
+ return next(new PrincipalNotAuthorizedError(`No access to ${collection} collection`, { resource: req.url }))
71
71
  }
72
72
  if (!await actorStorage.hasPage(username, collection, parseInt(n))) {
73
- return next(createHttpError(404, `No such page ${n} for collection ${collection} for user ${username}`))
73
+ return next(new ProblemDetailsError(404, `No such page ${n} for collection ${collection} for user ${username}`))
74
74
  }
75
75
 
76
76
  let exported = null
@@ -113,7 +113,7 @@ function collectionPageHandler (collection) {
113
113
  { err: error, username, collection, n },
114
114
  'error loading collection page'
115
115
  )
116
- return next(createHttpError(500, 'Error loading collection page'))
116
+ return next(new ProblemDetailsError(500, 'Error loading collection page'))
117
117
  }
118
118
  res.status(200)
119
119
  res.type(as2.mediaType)
@@ -1,5 +1,5 @@
1
1
  import express from 'express'
2
- import createHttpError from 'http-errors'
2
+ import { ProblemDetailsError } from '../errors.js'
3
3
 
4
4
  const router = express.Router()
5
5
 
@@ -17,7 +17,7 @@ router.get('/readyz', async (req, res, next) => {
17
17
  res.type('text/plain')
18
18
  res.end('OK')
19
19
  } catch (err) {
20
- return next(createHttpError(503, 'Service Unavailable'))
20
+ return next(new ProblemDetailsError(503, 'Service Unavailable'))
21
21
  }
22
22
  })
23
23
 
@@ -1,10 +1,17 @@
1
1
  import http from 'node:http'
2
2
 
3
3
  import express from 'express'
4
- import createHttpError from 'http-errors'
5
4
 
6
5
  import as2 from '../activitystreams.js'
7
6
  import BotMaker from '../botmaker.js'
7
+ import {
8
+ ProblemDetailsError,
9
+ UnsupportedTypeError,
10
+ DuplicateDeliveryError,
11
+ PrincipalActorMismatchError,
12
+ ActorNotAuthorizedError,
13
+ PrincipalNotAuthorizedError
14
+ } from '../errors.js'
8
15
 
9
16
  const router = express.Router()
10
17
 
@@ -16,15 +23,15 @@ router.post('/user/:username/inbox', async (req, res, next) => {
16
23
 
17
24
  const bot = await BotMaker.makeBot(bots, username)
18
25
  if (!bot) {
19
- return next(createHttpError(404, `User ${username} not found`))
26
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
20
27
  }
21
28
 
22
29
  if (!subject) {
23
- return next(createHttpError(401, 'Unauthorized'))
30
+ return next(new ProblemDetailsError(401, 'Unauthorized'))
24
31
  }
25
32
 
26
33
  if (!req.body) {
27
- return next(createHttpError(400, 'No request body provided'))
34
+ return next(new ProblemDetailsError(400, 'No request body provided'))
28
35
  }
29
36
 
30
37
  let activity
@@ -34,37 +41,37 @@ router.post('/user/:username/inbox', async (req, res, next) => {
34
41
  } catch (err) {
35
42
  logger.warn({ reqId: req.id, err }, 'Failed to import activity')
36
43
  logger.debug({ reqId: req.id, body: req.body }, 'Request body')
37
- return next(createHttpError(400, 'Invalid request body'))
44
+ return next(new ProblemDetailsError(400, 'Invalid request body'))
38
45
  }
39
46
 
40
47
  if (!activity.isActivity()) {
41
- return next(createHttpError(400, 'Request body is not an activity'))
48
+ return next(new UnsupportedTypeError('Request body is not an activity', { objectType: activity.type }))
42
49
  }
43
50
 
44
51
  const actor = deliverer.getActor(activity)
45
52
 
46
53
  if (!actor) {
47
- return next(createHttpError(400, 'No actor found in activity'))
54
+ return next(new ProblemDetailsError(400, 'No actor found in activity'))
48
55
  }
49
56
 
50
57
  if (!actor.id) {
51
- return next(createHttpError(400, 'No actor id found in activity'))
58
+ return next(new ProblemDetailsError(400, 'No actor id found in activity'))
52
59
  }
53
60
 
54
61
  if (actor.id !== subject && !(await bot.actorOK(subject, activity))) {
55
- return next(createHttpError(403, `${subject} is not the actor ${actor.id}`))
62
+ return next(new PrincipalActorMismatchError(`${subject} is not the actor ${actor.id}`, { principal: subject, actor: actor.id }))
56
63
  }
57
64
 
58
65
  if (await actorStorage.isInCollection(username, 'blocked', actor)) {
59
- return next(createHttpError(403, 'Forbidden'))
66
+ return next(new ActorNotAuthorizedError('Blocked actor', { actor: actor.id, resource: req.url }))
60
67
  }
61
68
 
62
69
  if (!activity.id) {
63
- return next(createHttpError(400, 'No activity id found in activity'))
70
+ return next(new ProblemDetailsError(400, 'No activity id found in activity'))
64
71
  }
65
72
 
66
73
  if (await actorStorage.isInCollection(bot.username, 'inbox', activity)) {
67
- return next(createHttpError(400, 'Activity already delivered'))
74
+ return next(new DuplicateDeliveryError('Activity already delivered', { id: activity.id }))
68
75
  }
69
76
 
70
77
  logger.info(
@@ -84,11 +91,11 @@ router.post('/user/:username/inbox', async (req, res, next) => {
84
91
  })
85
92
 
86
93
  router.get('/user/:username/inbox', async (req, res, next) => {
87
- return next(createHttpError(403, 'No access to inbox collection'))
94
+ return next(new PrincipalNotAuthorizedError('No access to inbox collection', { resource: req.url }))
88
95
  })
89
96
 
90
97
  router.get('/user/:username/inbox/:n', async (req, res, next) => {
91
- return next(createHttpError(403, 'No access to inbox collection'))
98
+ return next(new PrincipalNotAuthorizedError('No access to inbox collection', { resource: req.url }))
92
99
  })
93
100
 
94
101
  export default router
@@ -1,7 +1,7 @@
1
1
  import express from 'express'
2
- import createHttpError from 'http-errors'
3
2
 
4
3
  import as2 from '../activitystreams.js'
4
+ import { ProblemDetailsError, PrincipalNotAuthorizedError } from '../errors.js'
5
5
 
6
6
  const router = express.Router()
7
7
 
@@ -10,17 +10,17 @@ export default router
10
10
  router.get('/user/:username/:type/:nanoid', async (req, res, next) => {
11
11
  const { username, type, nanoid } = req.params
12
12
  if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
13
- return next(createHttpError(400, `Invalid nanoid ${nanoid}`))
13
+ return next(new ProblemDetailsError(400, `Invalid nanoid ${nanoid}`))
14
14
  }
15
15
  const { objectStorage, formatter, authorizer } = req.app.locals
16
16
  const id = formatter.format({ username, type, nanoid })
17
17
  const object = await objectStorage.read(id)
18
18
  if (!object) {
19
- return next(createHttpError(404, `Object ${id} not found`))
19
+ return next(new ProblemDetailsError(404, `Object ${id} not found`))
20
20
  }
21
21
  const remote = (req.auth?.subject) ? await as2.import({ id: req.auth.subject }) : null
22
22
  if (!await authorizer.canRead(remote, object)) {
23
- return next(createHttpError(403, `Forbidden to read object ${id}`))
23
+ return next(new PrincipalNotAuthorizedError('Forbidden to read object', { id }))
24
24
  }
25
25
  res.status(200)
26
26
  res.type(as2.mediaType)
@@ -30,20 +30,20 @@ router.get('/user/:username/:type/:nanoid', async (req, res, next) => {
30
30
  router.get('/user/:username/:type/:nanoid/:collection', async (req, res, next) => {
31
31
  const { objectStorage, formatter, authorizer } = req.app.locals
32
32
  if (!['replies', 'likes', 'shares', 'thread'].includes(req.params.collection)) {
33
- return next(createHttpError(404, 'Not Found'))
33
+ return next(new ProblemDetailsError(404, 'Not Found'))
34
34
  }
35
35
  const { username, type, nanoid } = req.params
36
36
  if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
37
- return next(createHttpError(400, `Invalid nanoid ${nanoid}`))
37
+ return next(new ProblemDetailsError(400, `Invalid nanoid ${nanoid}`))
38
38
  }
39
39
  const id = formatter.format({ username, type, nanoid })
40
40
  const object = await objectStorage.read(id)
41
41
  if (!object) {
42
- return next(createHttpError(404, 'Not Found'))
42
+ return next(new ProblemDetailsError(404, 'Not Found'))
43
43
  }
44
44
  const remote = (req.auth?.subject) ? await as2.import({ id: req.auth.subject }) : null
45
45
  if (!await authorizer.canRead(remote, object)) {
46
- return next(createHttpError(403, 'Forbidden'))
46
+ return next(new ProblemDetailsError(403, 'Forbidden'))
47
47
  }
48
48
  const collection = await objectStorage.getCollection(id, req.params.collection)
49
49
  res.status(200)
@@ -57,23 +57,23 @@ router.get('/user/:username/:type/:nanoid/:collection/:n', async (req, res, next
57
57
  try {
58
58
  pageNo = parseInt(n)
59
59
  } catch (err) {
60
- return next(createHttpError(400, `Invalid page ${n}`))
60
+ return next(new ProblemDetailsError(400, `Invalid page ${n}`))
61
61
  }
62
62
  if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
63
- return next(createHttpError(400, `Invalid nanoid ${nanoid}`))
63
+ return next(new ProblemDetailsError(400, `Invalid nanoid ${nanoid}`))
64
64
  }
65
65
  const { objectStorage, formatter, authorizer } = req.app.locals
66
66
  if (!['replies', 'likes', 'shares', 'thread'].includes(req.params.collection)) {
67
- return next(createHttpError(404, 'Not Found'))
67
+ return next(new ProblemDetailsError(404, 'Not Found'))
68
68
  }
69
69
  const id = formatter.format({ username, type, nanoid })
70
70
  const object = await objectStorage.read(id)
71
71
  if (!object) {
72
- return next(createHttpError(404, 'Not Found'))
72
+ return next(new ProblemDetailsError(404, 'Not Found'))
73
73
  }
74
74
  const remote = (req.auth?.subject) ? await as2.import({ id: req.auth.subject }) : null
75
75
  if (!await authorizer.canRead(remote, object)) {
76
- return next(createHttpError(403, 'Forbidden'))
76
+ return next(new ProblemDetailsError(403, 'Forbidden'))
77
77
  }
78
78
  const collectionPage = await objectStorage.getCollectionPage(
79
79
  id,
@@ -1,5 +1,5 @@
1
1
  import express from 'express'
2
- import createHttpError from 'http-errors'
2
+ import { ProblemDetailsError } from '../errors.js'
3
3
  import BotMaker from '../botmaker.js'
4
4
 
5
5
  const router = express.Router()
@@ -9,7 +9,7 @@ router.get('/profile/:username', async (req, res, next) => {
9
9
  const { profileFileName, bots } = req.app.locals
10
10
  const bot = await BotMaker.makeBot(bots, username)
11
11
  if (!bot) {
12
- return next(createHttpError(404, `User ${username} not found`))
12
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
13
13
  }
14
14
  res.type('html')
15
15
  res.sendFile(profileFileName)
@@ -1,7 +1,7 @@
1
1
  import express from 'express'
2
2
 
3
3
  import as2 from '../activitystreams.js'
4
- import createHttpError from 'http-errors'
4
+ import { ProblemDetailsError } from '../errors.js'
5
5
 
6
6
  const router = express.Router()
7
7
 
@@ -17,7 +17,7 @@ router.post('/shared/proxy', async (req, res, next) => {
17
17
  const { id } = req.body
18
18
 
19
19
  if (!id) {
20
- return next(createHttpError(400, 'Missing id parameter'))
20
+ return next(new ProblemDetailsError(400, 'Missing id parameter'))
21
21
  }
22
22
 
23
23
  let url
@@ -25,11 +25,11 @@ router.post('/shared/proxy', async (req, res, next) => {
25
25
  try {
26
26
  url = new URL(id)
27
27
  } catch (error) {
28
- return next(createHttpError(400, 'id must be an URL'))
28
+ return next(new ProblemDetailsError(400, 'id must be an URL'))
29
29
  }
30
30
 
31
31
  if (url.protocol !== 'https:') {
32
- return next(createHttpError(400, 'id must be an https: URL'))
32
+ return next(new ProblemDetailsError(400, 'id must be an https: URL'))
33
33
  }
34
34
 
35
35
  let obj
@@ -38,7 +38,7 @@ router.post('/shared/proxy', async (req, res, next) => {
38
38
  obj = await client.get(id)
39
39
  } catch (err) {
40
40
  logger.warn({ reqId: req.id, err, id }, 'Error fetching object in proxy')
41
- return next(createHttpError(400, `Error fetching object ${id}`))
41
+ return next(new ProblemDetailsError(400, `Error fetching object ${id}`))
42
42
  }
43
43
 
44
44
  res.status(200)
@@ -1,9 +1,13 @@
1
1
  import http from 'node:http'
2
2
 
3
3
  import express from 'express'
4
- import createHttpError from 'http-errors'
5
4
 
6
5
  import as2 from '../activitystreams.js'
6
+ import {
7
+ ProblemDetailsError,
8
+ UnsupportedTypeError,
9
+ PrincipalActorMismatchError
10
+ } from '../errors.js'
7
11
 
8
12
  const router = express.Router()
9
13
 
@@ -28,11 +32,11 @@ router.post('/shared/inbox', async (req, res, next) => {
28
32
  const { subject } = req.auth
29
33
 
30
34
  if (!subject) {
31
- return next(createHttpError(401, 'Unauthorized'))
35
+ return next(new ProblemDetailsError(401, 'Unauthorized'))
32
36
  }
33
37
 
34
38
  if (!req.body) {
35
- return next(createHttpError(400, 'No request body provided'))
39
+ return next(new ProblemDetailsError(400, 'No request body provided'))
36
40
  }
37
41
 
38
42
  let activity
@@ -42,29 +46,29 @@ router.post('/shared/inbox', async (req, res, next) => {
42
46
  } catch (err) {
43
47
  logger.warn({ reqId: req.id, err }, 'Failed to import activity')
44
48
  logger.debug({ reqId: req.id, body: req.body }, 'Request body')
45
- return next(createHttpError(400, 'Invalid request body'))
49
+ return next(new ProblemDetailsError(400, 'Invalid request body'))
46
50
  }
47
51
 
48
52
  if (!activity.isActivity()) {
49
- return next(createHttpError(400, 'Request body is not an activity'))
53
+ return next(new UnsupportedTypeError('Request body is not an activity', { objectType: activity.type }))
50
54
  }
51
55
 
52
56
  const actor = deliverer.getActor(activity)
53
57
 
54
58
  if (!actor) {
55
- return next(createHttpError(400, 'No actor found in activity'))
59
+ return next(new ProblemDetailsError(400, 'No actor found in activity'))
56
60
  }
57
61
 
58
62
  if (!actor.id) {
59
- return next(createHttpError(400, 'No actor id found in activity'))
63
+ return next(new ProblemDetailsError(400, 'No actor id found in activity'))
60
64
  }
61
65
 
62
66
  if (actor.id !== subject && !(await actorOK(subject, activity, bots))) {
63
- return next(createHttpError(403, `${subject} is not the actor ${actor.id}`))
67
+ return next(new PrincipalActorMismatchError(`${subject} is not the actor ${actor.id}`, { principal: subject, actor: actor.id }))
64
68
  }
65
69
 
66
70
  if (!activity.id) {
67
- return next(createHttpError(400, 'No activity id found in activity'))
71
+ return next(new ProblemDetailsError(400, 'No activity id found in activity'))
68
72
  }
69
73
 
70
74
  logger.info(
@@ -1,7 +1,7 @@
1
1
  import { fileURLToPath } from 'node:url'
2
2
  import express from 'express'
3
3
  import as2 from '../activitystreams.js'
4
- import createHttpError from 'http-errors'
4
+ import { ProblemDetailsError } from '../errors.js'
5
5
  import BotMaker from '../botmaker.js'
6
6
 
7
7
  const router = express.Router()
@@ -28,7 +28,7 @@ router.get('/user/:username', async (req, res, next) => {
28
28
  const { actorStorage, keyStorage, formatter, bots, origin } = req.app.locals
29
29
  const bot = await BotMaker.makeBot(bots, username)
30
30
  if (!bot) {
31
- return next(createHttpError(404, `User ${username} not found`))
31
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
32
32
  }
33
33
  const publicKeyPem = await keyStorage.getPublicKey(username)
34
34
  const acct = formatter.acct(username)
@@ -73,7 +73,7 @@ router.get('/user/:username/publickey', async (req, res, next) => {
73
73
  const { formatter, keyStorage, bots } = req.app.locals
74
74
  const bot = await BotMaker.makeBot(bots, username)
75
75
  if (!bot) {
76
- return next(createHttpError(404, `User ${username} not found`))
76
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
77
77
  }
78
78
  const publicKeyPem = await keyStorage.getPublicKey(username)
79
79
  const publicKey = await as2.import({
@@ -100,14 +100,14 @@ router.get('/user/:username/icon', async (req, res, next) => {
100
100
  const { bots } = req.app.locals
101
101
  const bot = await BotMaker.makeBot(bots, username)
102
102
  if (!bot) {
103
- return next(createHttpError(404, `User ${username} not found`))
103
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
104
104
  }
105
105
  if (!bot.icon) {
106
- return next(createHttpError(404, `No icon for ${username} found`))
106
+ return next(new ProblemDetailsError(404, `No icon for ${username} found`))
107
107
  }
108
108
 
109
109
  if (typeof bot.icon !== 'object' || !(bot.icon instanceof URL)) {
110
- return next(createHttpError(500, 'Incorrect image format from bot'))
110
+ return next(new ProblemDetailsError(500, 'Incorrect image format from bot'))
111
111
  }
112
112
 
113
113
  if (bot.icon.protocol === 'file:') {
@@ -122,14 +122,14 @@ router.get('/user/:username/image', async (req, res, next) => {
122
122
  const { bots } = req.app.locals
123
123
  const bot = await BotMaker.makeBot(bots, username)
124
124
  if (!bot) {
125
- return next(createHttpError(404, `User ${username} not found`))
125
+ return next(new ProblemDetailsError(404, `User ${username} not found`))
126
126
  }
127
127
  if (!bot.image) {
128
- return next(createHttpError(404, `No image for ${username} found`))
128
+ return next(new ProblemDetailsError(404, `No image for ${username} found`))
129
129
  }
130
130
 
131
131
  if (typeof bot.image !== 'object' || !(bot.image instanceof URL)) {
132
- return next(createHttpError(500, 'Incorrect image format from bot'))
132
+ return next(new ProblemDetailsError(500, 'Incorrect image format from bot'))
133
133
  }
134
134
 
135
135
  if (bot.image.protocol === 'file:') {
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert'
2
2
 
3
3
  import { Router } from 'express'
4
- import createHttpError from 'http-errors'
4
+ import { ProblemDetailsError } from '../errors.js'
5
5
 
6
6
  import BotMaker from '../botmaker.js'
7
7
 
@@ -11,7 +11,7 @@ async function botWebfinger (username, req, res, next) {
11
11
  const { formatter, bots } = req.app.locals
12
12
  const bot = await BotMaker.makeBot(bots, username)
13
13
  if (!bot) {
14
- return next(createHttpError(404, `No such bot '${username}'`))
14
+ return next(new ProblemDetailsError(404, `No such bot '${username}'`))
15
15
  }
16
16
  res.status(200)
17
17
  res.type('application/jrd+json')
@@ -37,7 +37,7 @@ async function profileWebfinger (username, profileUrl, req, res, next) {
37
37
  const { formatter, bots } = req.app.locals
38
38
  const bot = await BotMaker.makeBot(bots, username)
39
39
  if (!bot) {
40
- return next(createHttpError(404, `No such bot '${username}'`))
40
+ return next(new ProblemDetailsError(404, `No such bot '${username}'`))
41
41
  }
42
42
  res.status(200)
43
43
  res.type('application/jrd+json')
@@ -57,7 +57,7 @@ async function httpsWebfinger (resource, req, res, next) {
57
57
  const { formatter } = req.app.locals
58
58
  assert.ok(formatter)
59
59
  if (!formatter.isLocal(resource)) {
60
- return next(createHttpError(400, 'Only local URLs'))
60
+ return next(new ProblemDetailsError(400, 'Only local URLs'))
61
61
  }
62
62
  const parts = formatter.unformat(resource)
63
63
  if (parts.username && !parts.type && !parts.collection) {
@@ -65,18 +65,18 @@ async function httpsWebfinger (resource, req, res, next) {
65
65
  } else if (parts.username && parts.type === 'profile') {
66
66
  return await profileWebfinger(parts.username, resource, req, res, next)
67
67
  } else {
68
- return next(createHttpError(400, `No webfinger lookup for url ${resource}`))
68
+ return next(new ProblemDetailsError(400, `No webfinger lookup for url ${resource}`))
69
69
  }
70
70
  }
71
71
 
72
72
  async function acctWebfinger (resource, req, res, next) {
73
73
  const [username, domain] = resource.substring(5).split('@')
74
74
  if (!username || !domain) {
75
- return next(createHttpError(400, `Invalid resource parameter ${resource}`))
75
+ return next(new ProblemDetailsError(400, `Invalid resource parameter ${resource}`))
76
76
  }
77
77
  const { host } = new URL(req.app.locals.origin)
78
78
  if (domain !== host) {
79
- return next(createHttpError(400, `Invalid domain ${domain} in resource parameter`))
79
+ return next(new ProblemDetailsError(400, `Invalid domain ${domain} in resource parameter`))
80
80
  }
81
81
  return await botWebfinger(username, req, res, next)
82
82
  }
@@ -84,7 +84,7 @@ async function acctWebfinger (resource, req, res, next) {
84
84
  router.get('/.well-known/webfinger', async (req, res, next) => {
85
85
  const { resource } = req.query
86
86
  if (!resource) {
87
- return next(createHttpError(400, 'resource parameter is required'))
87
+ return next(new ProblemDetailsError(400, 'resource parameter is required'))
88
88
  }
89
89
  const colon = resource.indexOf(':')
90
90
  const protocol = resource.slice(0, colon)
@@ -96,7 +96,7 @@ router.get('/.well-known/webfinger', async (req, res, next) => {
96
96
  return await httpsWebfinger(resource, req, res, next)
97
97
  default:
98
98
  return next(
99
- createHttpError(400, `Unsupported resource protocol '${protocol}'`)
99
+ new ProblemDetailsError(400, `Unsupported resource protocol '${protocol}'`)
100
100
  )
101
101
  }
102
102
  })
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert'
2
+
3
+ export class SignaturePolicyStorage {
4
+ static RFC9421 = 'rfc9421'
5
+ static DRAFT_CAVAGE_12 = 'draft-cavage-12'
6
+ static #policies = [
7
+ SignaturePolicyStorage.RFC9421,
8
+ SignaturePolicyStorage.DRAFT_CAVAGE_12
9
+ ]
10
+
11
+ static #EXPIRY_OFFSET = 30 * 24 * 60 * 60 * 1000
12
+
13
+ #connection
14
+ #logger
15
+ constructor (connection, logger) {
16
+ assert.ok(connection)
17
+ assert.strictEqual(typeof connection, 'object')
18
+ assert.ok(logger)
19
+ assert.strictEqual(typeof logger, 'object')
20
+ this.#connection = connection
21
+ this.#logger = logger.child({ class: this.constructor.name })
22
+ }
23
+
24
+ async get (origin) {
25
+ assert.ok(origin)
26
+ assert.strictEqual(typeof origin, 'string')
27
+ const [rows] = await this.#connection.query(
28
+ 'SELECT policy, expiry FROM signature_policy WHERE origin = ?',
29
+ { replacements: [origin] }
30
+ )
31
+ if (rows.length === 0) {
32
+ return null
33
+ }
34
+ const { policy, expiry } = rows[0]
35
+ return ((new Date(expiry)) > (new Date()))
36
+ ? policy
37
+ : null
38
+ }
39
+
40
+ async set (origin, policy) {
41
+ assert.ok(origin)
42
+ assert.strictEqual(typeof origin, 'string')
43
+ assert.ok(policy)
44
+ assert.strictEqual(typeof policy, 'string')
45
+ assert.ok(SignaturePolicyStorage.#policies.includes(policy))
46
+
47
+ const expiry = new Date(
48
+ Date.now() + SignaturePolicyStorage.#EXPIRY_OFFSET
49
+ )
50
+
51
+ await this.#connection.query(
52
+ `INSERT INTO signature_policy (origin, policy, expiry)
53
+ VALUES (?, ?, ?)
54
+ ON CONFLICT (origin) DO UPDATE
55
+ SET policy = EXCLUDED.policy,
56
+ expiry = EXCLUDED.expiry,
57
+ updated_at = CURRENT_TIMESTAMP`,
58
+ { replacements: [origin, policy, expiry] }
59
+ )
60
+ }
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.38.4",
3
+ "version": "0.39.1",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -32,7 +32,6 @@
32
32
  "@isaacs/ttlcache": "^2.1.4",
33
33
  "activitystrea.ms": "^3.3.0",
34
34
  "express": "^5.2.1",
35
- "http-errors": "^2.0.0",
36
35
  "humanhash": "^1.0.4",
37
36
  "lru-cache": "^11.1.0",
38
37
  "nanoid": "^5.1.5",
package/web/profile.html CHANGED
@@ -45,14 +45,18 @@
45
45
  const u = new URL(url)
46
46
  if (u.origin == page.origin) {
47
47
  return await fetch(u, options)
48
- } else {
48
+ }
49
+
50
+ try {
51
+ return await fetch(u, options)
52
+ } catch (err) {
49
53
  return await fetch('/shared/proxy', {
50
- method: "POST",
51
- headers: {
52
- "Content-Type": "application/x-www-form-urlencoded"
53
- },
54
- body: new URLSearchParams({ id: url }),
55
- });
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/x-www-form-urlencoded'
57
+ },
58
+ body: new URLSearchParams({ id: url })
59
+ })
56
60
  }
57
61
  }
58
62
 
@@ -69,4 +73,4 @@
69
73
  </script>
70
74
  </body>
71
75
 
72
- </html>
76
+ </html>