@evanp/activitypub-bot 0.8.0 → 0.11.0

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.
Files changed (56) hide show
  1. package/.github/workflows/main.yml +34 -0
  2. package/.github/workflows/{tag-docker.yml → tag.yml} +57 -5
  3. package/.nvmrc +1 -0
  4. package/Dockerfile +11 -16
  5. package/README.md +262 -12
  6. package/activitypub-bot.js +68 -0
  7. package/lib/activitydeliverer.js +260 -0
  8. package/lib/activityhandler.js +14 -0
  9. package/lib/activitypubclient.js +52 -1
  10. package/lib/activitystreams.js +31 -0
  11. package/lib/actorstorage.js +18 -28
  12. package/lib/app.js +18 -7
  13. package/lib/bot.js +7 -0
  14. package/lib/botcontext.js +62 -0
  15. package/lib/botdatastorage.js +0 -13
  16. package/lib/botfactory.js +24 -0
  17. package/lib/botmaker.js +23 -0
  18. package/lib/keystorage.js +7 -24
  19. package/lib/migrations/001-initial.js +107 -0
  20. package/lib/migrations/index.js +28 -0
  21. package/lib/objectcache.js +4 -1
  22. package/lib/objectstorage.js +0 -36
  23. package/lib/remotekeystorage.js +0 -24
  24. package/lib/routes/collection.js +6 -2
  25. package/lib/routes/inbox.js +7 -20
  26. package/lib/routes/sharedinbox.js +54 -0
  27. package/lib/routes/user.js +11 -5
  28. package/lib/routes/webfinger.js +11 -2
  29. package/lib/urlformatter.js +8 -0
  30. package/package.json +18 -11
  31. package/tests/activitydistributor.test.js +3 -3
  32. package/tests/activityhandler.test.js +96 -5
  33. package/tests/activitypubclient.test.js +115 -130
  34. package/tests/actorstorage.test.js +26 -4
  35. package/tests/authorizer.test.js +3 -8
  36. package/tests/botcontext.test.js +109 -63
  37. package/tests/botdatastorage.test.js +3 -2
  38. package/tests/botfactory.provincebotfactory.test.js +430 -0
  39. package/tests/fixtures/bots.js +13 -1
  40. package/tests/fixtures/eventloggingbot.js +57 -0
  41. package/tests/fixtures/provincebotfactory.js +53 -0
  42. package/tests/httpsignature.test.js +3 -4
  43. package/tests/httpsignatureauthenticator.test.js +3 -3
  44. package/tests/keystorage.test.js +37 -2
  45. package/tests/microsyntax.test.js +3 -2
  46. package/tests/objectstorage.test.js +4 -3
  47. package/tests/remotekeystorage.test.js +10 -8
  48. package/tests/routes.actor.test.js +7 -0
  49. package/tests/routes.collection.test.js +0 -1
  50. package/tests/routes.inbox.test.js +1 -0
  51. package/tests/routes.object.test.js +44 -38
  52. package/tests/routes.sharedinbox.test.js +473 -0
  53. package/tests/routes.webfinger.test.js +27 -0
  54. package/tests/utils/nock.js +250 -27
  55. package/.github/workflows/main-docker.yml +0 -45
  56. package/index.js +0 -23
@@ -0,0 +1,23 @@
1
+ import assert from 'node:assert'
2
+
3
+ export default class BotMaker {
4
+ static async makeBot (bots, username) {
5
+ assert.ok(bots)
6
+ assert.ok(typeof bots === 'object')
7
+ assert.ok(username)
8
+ assert.ok(typeof username === 'string')
9
+
10
+ let bot
11
+
12
+ if (username in bots) {
13
+ bot = bots[username]
14
+ } else if ('*' in bots) {
15
+ const factory = bots['*']
16
+ if (await factory.canCreate(username)) {
17
+ bot = await factory.create(username)
18
+ }
19
+ }
20
+
21
+ return bot
22
+ }
23
+ }
package/lib/keystorage.js CHANGED
@@ -17,27 +17,6 @@ export class KeyStorage {
17
17
  this.#hasher = new HumanHasher()
18
18
  }
19
19
 
20
- async initialize () {
21
- await this.#connection.query(`
22
- CREATE TABLE IF NOT EXISTS new_keys (
23
- username varchar(512) PRIMARY KEY,
24
- public_key TEXT,
25
- private_key TEXT
26
- )
27
- `)
28
- try {
29
- await this.#connection.query(`
30
- INSERT OR IGNORE INTO new_keys (username, public_key, private_key)
31
- SELECT bot_id, public_key, private_key
32
- FROM keys
33
- `)
34
- } catch (error) {
35
- this.#logger.debug(
36
- { error, method: 'KeyStorage.initialize' },
37
- 'failed to copy keys to new_keys table')
38
- }
39
- }
40
-
41
20
  async getPublicKey (username) {
42
21
  this.#logger.debug(
43
22
  { username, method: 'KeyStorage.getPublicKey' },
@@ -57,9 +36,13 @@ export class KeyStorage {
57
36
  async #getKeys (username) {
58
37
  let privateKey
59
38
  let publicKey
60
- const [result] = await this.#connection.query(`
61
- SELECT public_key, private_key FROM new_keys WHERE username = ?
62
- `, { replacements: [username] })
39
+ const qry = (username)
40
+ ? 'SELECT public_key, private_key FROM new_keys WHERE username = ?'
41
+ : 'SELECT public_key, private_key FROM new_keys WHERE username IS NULL'
42
+ const [result] = await this.#connection.query(
43
+ qry,
44
+ { replacements: [username] }
45
+ )
63
46
  if (result.length > 0) {
64
47
  this.#logger.debug(
65
48
  { username, method: 'KeyStorage.#getKeys' },
@@ -0,0 +1,107 @@
1
+ export const id = '001-initial'
2
+
3
+ export async function up (connection) {
4
+ await connection.query(`
5
+ CREATE TABLE IF NOT EXISTS actorcollection (
6
+ username varchar(512) NOT NULL,
7
+ property varchar(512) NOT NULL,
8
+ first INTEGER NOT NULL,
9
+ totalItems INTEGER NOT NULL DEFAULT 0,
10
+ createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
+ updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
+ PRIMARY KEY (username, property)
13
+ );
14
+ `)
15
+ await connection.query(`
16
+ CREATE TABLE IF NOT EXISTS actorcollectionpage (
17
+ username varchar(512) NOT NULL,
18
+ property varchar(512) NOT NULL,
19
+ item varchar(512) NOT NULL,
20
+ page INTEGER NOT NULL,
21
+ createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
22
+ PRIMARY KEY (username, property, item)
23
+ );
24
+ `)
25
+ await connection.query(
26
+ `CREATE INDEX IF NOT EXISTS actorcollectionpage_username_property_page
27
+ ON actorcollectionpage (username, property, page);`
28
+ )
29
+
30
+ await connection.query(`
31
+ CREATE TABLE IF NOT EXISTS objects (
32
+ id VARCHAR(512) PRIMARY KEY,
33
+ data TEXT NOT NULL,
34
+ createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
35
+ updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
36
+ )
37
+ `)
38
+ await connection.query(`
39
+ CREATE TABLE IF NOT EXISTS collections (
40
+ id VARCHAR(512) NOT NULL,
41
+ property VARCHAR(512) NOT NULL,
42
+ first INTEGER NOT NULL,
43
+ createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
44
+ updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
45
+ PRIMARY KEY (id, property)
46
+ )
47
+ `)
48
+ await connection.query(`
49
+ CREATE TABLE IF NOT EXISTS pages (
50
+ id VARCHAR(512) NOT NULL,
51
+ property VARCHAR(64) NOT NULL,
52
+ item VARCHAR(512) NOT NULL,
53
+ page INTEGER NOT NULL,
54
+ createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
+ PRIMARY KEY (id, property, item)
56
+ )
57
+ `)
58
+ await connection.query(
59
+ `CREATE INDEX IF NOT EXISTS pages_username_property_page
60
+ ON pages (id, property, page);`
61
+ )
62
+
63
+ await connection.query(`
64
+ CREATE TABLE IF NOT EXISTS botdata (
65
+ username VARCHAR(512) not null,
66
+ key VARCHAR(512) not null,
67
+ value TEXT not null,
68
+ createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
69
+ updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
70
+ PRIMARY KEY (username, key)
71
+ )
72
+ `)
73
+
74
+ await connection.query(`
75
+ CREATE TABLE IF NOT EXISTS new_keys (
76
+ username varchar(512) PRIMARY KEY,
77
+ public_key TEXT,
78
+ private_key TEXT
79
+ )
80
+ `)
81
+ try {
82
+ await connection.query(`
83
+ INSERT OR IGNORE INTO new_keys (username, public_key, private_key)
84
+ SELECT bot_id, public_key, private_key
85
+ FROM keys
86
+ `)
87
+ } catch {
88
+ }
89
+
90
+ await connection.query(`
91
+ CREATE TABLE IF NOT EXISTS new_remotekeys (
92
+ id VARCHAR(512) PRIMARY KEY,
93
+ owner VARCHAR(512) NOT NULL,
94
+ publicKeyPem TEXT NOT NULL,
95
+ createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
96
+ updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
97
+ )
98
+ `)
99
+ try {
100
+ await connection.query(`
101
+ INSERT OR IGNORE INTO new_remotekeys (id, owner, publicKeyPem)
102
+ SELECT id, owner, publicKeyPem
103
+ FROM remotekeys
104
+ `)
105
+ } catch {
106
+ }
107
+ }
@@ -0,0 +1,28 @@
1
+ import { id as initialId, up as initialUp } from './001-initial.js'
2
+
3
+ const migrations = [
4
+ { id: initialId, up: initialUp }
5
+ ]
6
+
7
+ export async function runMigrations (connection) {
8
+ await connection.query(`
9
+ CREATE TABLE IF NOT EXISTS migrations (
10
+ id VARCHAR(255) PRIMARY KEY,
11
+ ranAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
12
+ )
13
+ `)
14
+
15
+ const [rows] = await connection.query('SELECT id FROM migrations')
16
+ const applied = new Set(rows.map((row) => row.id))
17
+
18
+ for (const migration of migrations) {
19
+ if (applied.has(migration.id)) {
20
+ continue
21
+ }
22
+ await migration.up(connection)
23
+ await connection.query(
24
+ 'INSERT INTO migrations (id) VALUES (?)',
25
+ { replacements: [migration.id] }
26
+ )
27
+ }
28
+ }
@@ -1,4 +1,7 @@
1
- import TTLCache from '@isaacs/ttlcache'
1
+ import * as ttlcachePkg from '@isaacs/ttlcache'
2
+
3
+ const TTLCache =
4
+ ttlcachePkg.TTLCache ?? ttlcachePkg.default ?? ttlcachePkg
2
5
 
3
6
  export class ObjectCache {
4
7
  #objects = null
@@ -17,42 +17,6 @@ export class ObjectStorage {
17
17
  this.#connection = connection
18
18
  }
19
19
 
20
- async initialize () {
21
- await this.#connection.query(`
22
- CREATE TABLE IF NOT EXISTS objects (
23
- id VARCHAR(512) PRIMARY KEY,
24
- data TEXT NOT NULL,
25
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
26
- updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
27
- )
28
- `)
29
- await this.#connection.query(`
30
- CREATE TABLE IF NOT EXISTS collections (
31
- id VARCHAR(512) NOT NULL,
32
- property VARCHAR(512) NOT NULL,
33
- first INTEGER NOT NULL,
34
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
35
- updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
- PRIMARY KEY (id, property)
37
- )
38
- `)
39
- await this.#connection.query(`
40
- CREATE TABLE IF NOT EXISTS pages (
41
- id VARCHAR(512) NOT NULL,
42
- property VARCHAR(64) NOT NULL,
43
- item VARCHAR(512) NOT NULL,
44
- page INTEGER NOT NULL,
45
- createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
46
- PRIMARY KEY (id, property, item)
47
- )
48
- `)
49
-
50
- await this.#connection.query(
51
- `CREATE INDEX IF NOT EXISTS pages_username_property_page
52
- ON pages (id, property, page);`
53
- )
54
- }
55
-
56
20
  async create (object) {
57
21
  assert.ok(this.#connection, 'ObjectStorage not initialized')
58
22
  assert.ok(object, 'object is required')
@@ -10,30 +10,6 @@ export class RemoteKeyStorage {
10
10
  this.#logger = logger.child({ class: this.constructor.name })
11
11
  }
12
12
 
13
- async initialize () {
14
- await this.#connection.query(
15
- `CREATE TABLE IF NOT EXISTS new_remotekeys (
16
- id VARCHAR(512) PRIMARY KEY,
17
- owner VARCHAR(512) NOT NULL,
18
- publicKeyPem TEXT NOT NULL,
19
- createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
20
- updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
21
- )`
22
- )
23
- try {
24
- await this.#connection.query(
25
- `INSERT OR IGNORE INTO new_remotekeys (id, owner, publicKeyPem)
26
- SELECT id, owner, publicKeyPem
27
- FROM remotekeys`
28
- )
29
- } catch (error) {
30
- this.#logger.debug(
31
- { error, method: 'RemoteKeyStorage.initialize' },
32
- 'failed to copy remotekeys to new_remotekeys table'
33
- )
34
- }
35
- }
36
-
37
13
  async getPublicKey (id, useCache = true) {
38
14
  this.debug(`getPublicKey(${id})`)
39
15
  if (useCache) {
@@ -1,6 +1,7 @@
1
1
  import express from 'express'
2
2
  import as2 from '../activitystreams.js'
3
3
  import createHttpError from 'http-errors'
4
+ import BotMaker from '../botmaker.js'
4
5
 
5
6
  const router = express.Router()
6
7
 
@@ -18,7 +19,8 @@ async function filterAsync (array, asyncPredicate) {
18
19
  router.get('/user/:username/:collection', async (req, res, next) => {
19
20
  const { username, collection } = req.params
20
21
  const { actorStorage, bots } = req.app.locals
21
- if (!(username in bots)) {
22
+ const bot = await BotMaker.makeBot(bots, username)
23
+ if (!bot) {
22
24
  return next(createHttpError(404, `User ${username} not found`))
23
25
  }
24
26
  if (collection === 'inbox') {
@@ -37,10 +39,12 @@ router.get('/user/:username/:collection', async (req, res, next) => {
37
39
  router.get('/user/:username/:collection/:n(\\d+)', async (req, res, next) => {
38
40
  const { username, collection, n } = req.params
39
41
  const { actorStorage, bots, authorizer, objectStorage, formatter, client } = req.app.locals
42
+ const bot = await BotMaker.makeBot(bots, username)
40
43
 
41
- if (!(username in bots)) {
44
+ if (!bot) {
42
45
  return next(createHttpError(404, `User ${username} not found`))
43
46
  }
47
+
44
48
  if (collection === 'inbox') {
45
49
  return next(createHttpError(403, `No access to ${collection} collection`))
46
50
  }
@@ -1,26 +1,19 @@
1
1
  import express from 'express'
2
2
  import as2 from '../activitystreams.js'
3
3
  import createHttpError from 'http-errors'
4
+ import BotMaker from '../botmaker.js'
4
5
 
5
6
  const router = express.Router()
6
7
 
7
- function isActivity (object) {
8
- return true
9
- }
10
-
11
- function getActor (activity) {
12
- return activity.actor?.first
13
- }
14
-
15
8
  router.post('/user/:username/inbox', async (req, res, next) => {
16
9
  const { username } = req.params
17
- const { bots, actorStorage, activityHandler } = req.app.locals
10
+ const { bots, deliverer, actorStorage } = req.app.locals
18
11
  const { subject } = req.auth
19
12
  const { logger } = req.app.locals
20
13
 
21
- const bot = bots[username]
14
+ const bot = await BotMaker.makeBot(bots, username)
22
15
  if (!bot) {
23
- return next(createHttpError(404, 'Not Found'))
16
+ return next(createHttpError(404, `User ${username} not found`))
24
17
  }
25
18
 
26
19
  if (!subject) {
@@ -41,11 +34,11 @@ router.post('/user/:username/inbox', async (req, res, next) => {
41
34
  return next(createHttpError(400, 'Invalid request body'))
42
35
  }
43
36
 
44
- if (!isActivity(activity)) {
37
+ if (!deliverer.isActivity(activity)) {
45
38
  return next(createHttpError(400, 'Request body is not an activity'))
46
39
  }
47
40
 
48
- const actor = getActor(activity)
41
+ const actor = deliverer.getActor(activity)
49
42
 
50
43
  if (!actor) {
51
44
  return next(createHttpError(400, 'No actor found in activity'))
@@ -67,13 +60,7 @@ router.post('/user/:username/inbox', async (req, res, next) => {
67
60
  return next(createHttpError(400, 'Activity already delivered'))
68
61
  }
69
62
 
70
- try {
71
- await activityHandler.handleActivity(bot, activity)
72
- } catch (err) {
73
- return next(err)
74
- }
75
-
76
- await actorStorage.addToCollection(bot.username, 'inbox', activity)
63
+ await deliverer.deliverTo(activity, bot)
77
64
 
78
65
  res.status(200)
79
66
  res.type('text/plain')
@@ -0,0 +1,54 @@
1
+ import express from 'express'
2
+ import as2 from '../activitystreams.js'
3
+ import createHttpError from 'http-errors'
4
+
5
+ const router = express.Router()
6
+
7
+ router.post('/shared/inbox', async (req, res, next) => {
8
+ const { bots, deliverer, logger } = req.app.locals
9
+ const { subject } = req.auth
10
+
11
+ if (!subject) {
12
+ return next(createHttpError(401, 'Unauthorized'))
13
+ }
14
+
15
+ if (!req.body) {
16
+ return next(createHttpError(400, 'No request body provided'))
17
+ }
18
+
19
+ let activity
20
+
21
+ try {
22
+ activity = await as2.import(req.body)
23
+ } catch (err) {
24
+ logger.warn('Failed to import activity', err)
25
+ logger.debug('Request body', req.body)
26
+ return next(createHttpError(400, 'Invalid request body'))
27
+ }
28
+
29
+ if (!deliverer.isActivity(activity)) {
30
+ return next(createHttpError(400, 'Request body is not an activity'))
31
+ }
32
+
33
+ const actor = deliverer.getActor(activity)
34
+
35
+ if (!actor) {
36
+ return next(createHttpError(400, 'No actor found in activity'))
37
+ }
38
+
39
+ if (!actor.id) {
40
+ return next(createHttpError(400, 'No actor id found in activity'))
41
+ }
42
+
43
+ if (actor.id !== subject) {
44
+ return next(createHttpError(403, `${subject} is not the actor ${actor.id}`))
45
+ }
46
+
47
+ await deliverer.deliverToAll(activity, bots)
48
+
49
+ res.status(200)
50
+ res.type('text/plain')
51
+ res.send('OK')
52
+ })
53
+
54
+ export default router
@@ -1,13 +1,15 @@
1
1
  import express from 'express'
2
2
  import as2 from '../activitystreams.js'
3
3
  import createHttpError from 'http-errors'
4
+ import BotMaker from '../botmaker.js'
4
5
 
5
6
  const router = express.Router()
6
7
 
7
8
  router.get('/user/:username', async (req, res, next) => {
8
9
  const { username } = req.params
9
- const { actorStorage, keyStorage, formatter, bots } = req.app.locals
10
- if (!(username in bots)) {
10
+ const { actorStorage, keyStorage, formatter, bots, origin } = req.app.locals
11
+ const bot = await BotMaker.makeBot(bots, username)
12
+ if (!bot) {
11
13
  return next(createHttpError(404, `User ${username} not found`))
12
14
  }
13
15
  const publicKeyPem = await keyStorage.getPublicKey(username)
@@ -16,14 +18,17 @@ router.get('/user/:username', async (req, res, next) => {
16
18
  'https://www.w3.org/ns/activitystreams',
17
19
  'https://w3id.org/security/v1'
18
20
  ],
19
- name: bots[username].fullname,
20
- summary: bots[username].description,
21
+ name: bot.fullname,
22
+ summary: bot.description,
21
23
  publicKey: {
22
24
  publicKeyPem,
23
25
  id: formatter.format({ username, type: 'publickey' }),
24
26
  owner: formatter.format({ username }),
25
27
  type: 'CryptographicKey',
26
28
  to: 'as:Public'
29
+ },
30
+ endpoints: {
31
+ sharedInbox: `${origin}/shared/inbox`
27
32
  }
28
33
  })
29
34
  res.status(200)
@@ -37,7 +42,8 @@ router.get('/user/:username', async (req, res, next) => {
37
42
  router.get('/user/:username/publickey', async (req, res, next) => {
38
43
  const { username } = req.params
39
44
  const { formatter, keyStorage, bots } = req.app.locals
40
- if (!(username in bots)) {
45
+ const bot = await BotMaker.makeBot(bots, username)
46
+ if (!bot) {
41
47
  return next(createHttpError(404, `User ${username} not found`))
42
48
  }
43
49
  const publicKeyPem = await keyStorage.getPublicKey(username)
@@ -3,11 +3,14 @@ import createHttpError from 'http-errors'
3
3
 
4
4
  const router = Router()
5
5
 
6
- router.get('/.well-known/webfinger', (req, res, next) => {
6
+ router.get('/.well-known/webfinger', async (req, res, next) => {
7
7
  const { resource } = req.query
8
8
  if (!resource) {
9
9
  return next(createHttpError(400, 'resource parameter is required'))
10
10
  }
11
+ if (resource.substring(0, 5) !== 'acct:') {
12
+ return next(createHttpError(400, 'only acct: protocol supported'))
13
+ }
11
14
  const [username, domain] = resource.substring(5).split('@')
12
15
  if (!username || !domain) {
13
16
  return next(createHttpError(400, 'Invalid resource parameter'))
@@ -17,7 +20,13 @@ router.get('/.well-known/webfinger', (req, res, next) => {
17
20
  return next(createHttpError(400, 'Invalid domain in resource parameter'))
18
21
  }
19
22
  if (!(username in req.app.locals.bots)) {
20
- return next(createHttpError(404, 'Bot not found'))
23
+ if ('*' in req.app.locals.bots) {
24
+ if (!await req.app.locals.bots['*'].canCreate(username)) {
25
+ return next(createHttpError(404, 'Bot not found'))
26
+ }
27
+ } else {
28
+ return next(createHttpError(404, 'Bot not found'))
29
+ }
21
30
  }
22
31
  res.status(200)
23
32
  res.type('application/jrd+json')
@@ -94,4 +94,12 @@ export class UrlFormatter {
94
94
  }
95
95
  return parts
96
96
  }
97
+
98
+ isActor (url) {
99
+ if (!this.isLocal(url)) {
100
+ return false
101
+ }
102
+ const parts = this.unformat(url)
103
+ return (parts.username && !parts.type && !parts.collection)
104
+ }
97
105
  }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.8.0",
3
+ "version": "0.11.0",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
- "main": "index.js",
6
+ "main": "activitypub-bot.js",
7
+ "bin": {
8
+ "activitypub-bot": "./activitypub-bot.js"
9
+ },
7
10
  "scripts": {
8
11
  "test": "NODE_ENV=test node --test",
9
- "start": "node index.js"
12
+ "start": "npx activitypub-bot"
10
13
  },
11
14
  "repository": {
12
15
  "type": "git",
@@ -25,27 +28,31 @@
25
28
  },
26
29
  "homepage": "https://github.com/evanp/activitypub-bot#readme",
27
30
  "dependencies": {
28
- "@isaacs/ttlcache": "^1.4.1",
31
+ "@isaacs/ttlcache": "^2.1.4",
29
32
  "activitystrea.ms": "3.2",
30
- "express": "^4.18.2",
33
+ "express": "^4.22.1",
31
34
  "http-errors": "^2.0.0",
32
35
  "humanhash": "^1.0.4",
33
36
  "lru-cache": "^11.1.0",
34
37
  "nanoid": "^5.1.5",
35
38
  "node-fetch": "^3.3.2",
36
- "p-queue": "^8.1.0",
37
- "pino": "^9.7.0",
38
- "pino-http": "^10.4.0",
39
+ "p-queue": "^9.1.0",
40
+ "pino": "^10.1.1",
41
+ "pino-http": "^11.0.0",
39
42
  "sequelize": "^6.37.7"
40
43
  },
41
44
  "devDependencies": {
42
45
  "nock": "^14.0.5",
43
46
  "standard": "^17.1.2",
44
- "supertest": "^7.1.1"
47
+ "supertest": "^7.1.4"
45
48
  },
46
49
  "optionalDependencies": {
47
50
  "mysql2": "^3.9.1",
48
- "pg": "^8.16.0",
51
+ "pg": "^8.16.3",
49
52
  "sqlite3": "^5.1.7"
50
- }
53
+ },
54
+ "engines": {
55
+ "node": "^20 || ^22 || ^24 || ^25"
56
+ },
57
+ "engineStrict": true
51
58
  }
@@ -11,6 +11,7 @@ import { ActivityDistributor } from '../lib/activitydistributor.js'
11
11
  import Logger from 'pino'
12
12
  import { HTTPSignature } from '../lib/httpsignature.js'
13
13
  import { Digester } from '../lib/digester.js'
14
+ import { runMigrations } from '../lib/migrations/index.js'
14
15
 
15
16
  const makeActor = (domain, username, shared = true) =>
16
17
  as2.import({
@@ -63,12 +64,11 @@ describe('ActivityDistributor', () => {
63
64
  before(async () => {
64
65
  logger = Logger({ level: 'silent' })
65
66
  formatter = new UrlFormatter('https://activitypubbot.example')
66
- connection = new Sequelize('sqlite::memory:', { logging: false })
67
+ connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
67
68
  await connection.authenticate()
69
+ await runMigrations(connection)
68
70
  actorStorage = new ActorStorage(connection, formatter)
69
- await actorStorage.initialize()
70
71
  keyStorage = new KeyStorage(connection, logger)
71
- await keyStorage.initialize()
72
72
  const signer = new HTTPSignature(logger)
73
73
  const digester = new Digester(logger)
74
74
  client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)