@evanp/activitypub-bot 0.8.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.
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/main-docker.yml +45 -0
- package/.github/workflows/tag-docker.yml +54 -0
- package/Dockerfile +23 -0
- package/LICENSE +661 -0
- package/README.md +82 -0
- package/bots/index.js +7 -0
- package/docs/activitypub.bot.drawio +110 -0
- package/index.js +23 -0
- package/lib/activitydistributor.js +263 -0
- package/lib/activityhandler.js +999 -0
- package/lib/activitypubclient.js +126 -0
- package/lib/activitystreams.js +41 -0
- package/lib/actorstorage.js +300 -0
- package/lib/app.js +173 -0
- package/lib/authorizer.js +133 -0
- package/lib/bot.js +44 -0
- package/lib/botcontext.js +520 -0
- package/lib/botdatastorage.js +87 -0
- package/lib/bots/donothing.js +11 -0
- package/lib/bots/ok.js +41 -0
- package/lib/digester.js +23 -0
- package/lib/httpsignature.js +195 -0
- package/lib/httpsignatureauthenticator.js +81 -0
- package/lib/keystorage.js +113 -0
- package/lib/microsyntax.js +140 -0
- package/lib/objectcache.js +48 -0
- package/lib/objectstorage.js +319 -0
- package/lib/remotekeystorage.js +116 -0
- package/lib/routes/collection.js +92 -0
- package/lib/routes/health.js +24 -0
- package/lib/routes/inbox.js +83 -0
- package/lib/routes/object.js +69 -0
- package/lib/routes/server.js +47 -0
- package/lib/routes/user.js +63 -0
- package/lib/routes/webfinger.js +36 -0
- package/lib/urlformatter.js +97 -0
- package/package.json +51 -0
- package/tests/activitydistributor.test.js +606 -0
- package/tests/activityhandler.test.js +2185 -0
- package/tests/activitypubclient.test.js +225 -0
- package/tests/actorstorage.test.js +261 -0
- package/tests/app.test.js +17 -0
- package/tests/authorizer.test.js +306 -0
- package/tests/bot.donothing.test.js +30 -0
- package/tests/bot.ok.test.js +101 -0
- package/tests/botcontext.test.js +674 -0
- package/tests/botdatastorage.test.js +87 -0
- package/tests/digester.test.js +56 -0
- package/tests/fixtures/bots.js +15 -0
- package/tests/httpsignature.test.js +200 -0
- package/tests/httpsignatureauthenticator.test.js +463 -0
- package/tests/keystorage.test.js +89 -0
- package/tests/microsyntax.test.js +122 -0
- package/tests/objectcache.test.js +133 -0
- package/tests/objectstorage.test.js +148 -0
- package/tests/remotekeystorage.test.js +76 -0
- package/tests/routes.actor.test.js +207 -0
- package/tests/routes.collection.test.js +434 -0
- package/tests/routes.health.test.js +41 -0
- package/tests/routes.inbox.test.js +135 -0
- package/tests/routes.object.test.js +519 -0
- package/tests/routes.server.test.js +69 -0
- package/tests/routes.webfinger.test.js +41 -0
- package/tests/urlformatter.test.js +164 -0
- package/tests/utils/digest.js +7 -0
- package/tests/utils/nock.js +276 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { describe, before, after, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { Sequelize } from 'sequelize'
|
|
4
|
+
import { KeyStorage } from '../lib/keystorage.js'
|
|
5
|
+
import { nockSetup, nockSignature, nockKeyRotate, nockFormat } from './utils/nock.js'
|
|
6
|
+
import { HTTPSignature } from '../lib/httpsignature.js'
|
|
7
|
+
import { HTTPSignatureAuthenticator } from '../lib/httpsignatureauthenticator.js'
|
|
8
|
+
import Logger from 'pino'
|
|
9
|
+
import { Digester } from '../lib/digester.js'
|
|
10
|
+
import { RemoteKeyStorage } from '../lib/remotekeystorage.js'
|
|
11
|
+
import { ActivityPubClient } from '../lib/activitypubclient.js'
|
|
12
|
+
import { UrlFormatter } from '../lib/urlformatter.js'
|
|
13
|
+
import as2 from '../lib/activitystreams.js'
|
|
14
|
+
|
|
15
|
+
describe('HTTPSignatureAuthenticator', async () => {
|
|
16
|
+
const domain = 'activitypubbot.example'
|
|
17
|
+
const origin = `https://${domain}`
|
|
18
|
+
let authenticator = null
|
|
19
|
+
let logger = null
|
|
20
|
+
let signer = null
|
|
21
|
+
let digester = null
|
|
22
|
+
let remoteKeyStorage = null
|
|
23
|
+
let connection = null
|
|
24
|
+
const next = (err) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
assert.fail(`Failed to authenticate: ${err.message}`)
|
|
27
|
+
} else {
|
|
28
|
+
assert.ok(true, 'Authenticated successfully')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const failNext = (err) => {
|
|
32
|
+
if (err) {
|
|
33
|
+
assert.ok(true, 'Failed successfully')
|
|
34
|
+
} else {
|
|
35
|
+
assert.fail('Passed through an incorrect request')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
before(async () => {
|
|
39
|
+
logger = Logger({
|
|
40
|
+
level: 'silent'
|
|
41
|
+
})
|
|
42
|
+
connection = new Sequelize('sqlite::memory:', { logging: false })
|
|
43
|
+
await connection.authenticate()
|
|
44
|
+
signer = new HTTPSignature(logger)
|
|
45
|
+
digester = new Digester(logger)
|
|
46
|
+
const formatter = new UrlFormatter(origin)
|
|
47
|
+
const keyStorage = new KeyStorage(connection, logger)
|
|
48
|
+
await keyStorage.initialize()
|
|
49
|
+
const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
|
|
50
|
+
remoteKeyStorage = new RemoteKeyStorage(client, connection, logger)
|
|
51
|
+
await remoteKeyStorage.initialize()
|
|
52
|
+
nockSetup('social.example')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
after(async () => {
|
|
56
|
+
await connection.close()
|
|
57
|
+
authenticator = null
|
|
58
|
+
digester = null
|
|
59
|
+
signer = null
|
|
60
|
+
remoteKeyStorage = null
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('can initialize', async () => {
|
|
64
|
+
authenticator = new HTTPSignatureAuthenticator(
|
|
65
|
+
remoteKeyStorage,
|
|
66
|
+
signer,
|
|
67
|
+
digester,
|
|
68
|
+
logger)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('can authenticate a valid GET request', async () => {
|
|
72
|
+
const username = 'test'
|
|
73
|
+
const date = new Date().toUTCString()
|
|
74
|
+
const signature = await nockSignature({
|
|
75
|
+
url: `${origin}/user/ok/outbox`,
|
|
76
|
+
date,
|
|
77
|
+
username
|
|
78
|
+
})
|
|
79
|
+
const headers = {
|
|
80
|
+
date,
|
|
81
|
+
signature,
|
|
82
|
+
host: URL.parse(origin).host
|
|
83
|
+
}
|
|
84
|
+
const method = 'GET'
|
|
85
|
+
const originalUrl = '/user/ok/outbox'
|
|
86
|
+
const res = {
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
const req = {
|
|
90
|
+
headers,
|
|
91
|
+
originalUrl,
|
|
92
|
+
method,
|
|
93
|
+
get: function (name) {
|
|
94
|
+
return this.headers[name.toLowerCase()]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await authenticator.authenticate(req, res, next)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('can authenticate a valid GET request with parameters', async () => {
|
|
101
|
+
const lname = 'ok'
|
|
102
|
+
const username = 'test'
|
|
103
|
+
const date = new Date().toUTCString()
|
|
104
|
+
const signature = await nockSignature({
|
|
105
|
+
url: `${origin}/.well-known/webfinger?resource=acct:${lname}@${domain}`,
|
|
106
|
+
date,
|
|
107
|
+
username
|
|
108
|
+
})
|
|
109
|
+
const headers = {
|
|
110
|
+
date,
|
|
111
|
+
signature,
|
|
112
|
+
host: URL.parse(origin).host
|
|
113
|
+
}
|
|
114
|
+
const method = 'GET'
|
|
115
|
+
const originalUrl = `/.well-known/webfinger?resource=acct:${lname}@${domain}`
|
|
116
|
+
const res = {
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
const req = {
|
|
120
|
+
headers,
|
|
121
|
+
originalUrl,
|
|
122
|
+
method,
|
|
123
|
+
get: function (name) {
|
|
124
|
+
return this.headers[name.toLowerCase()]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await authenticator.authenticate(req, res, next)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('can authenticate a valid GET request after key rotation', async () => {
|
|
131
|
+
const username = 'test2'
|
|
132
|
+
const date = new Date().toUTCString()
|
|
133
|
+
const signature = await nockSignature({
|
|
134
|
+
url: `${origin}/user/ok/outbox`,
|
|
135
|
+
date,
|
|
136
|
+
username
|
|
137
|
+
})
|
|
138
|
+
const headers = {
|
|
139
|
+
date,
|
|
140
|
+
signature,
|
|
141
|
+
host: URL.parse(origin).host
|
|
142
|
+
}
|
|
143
|
+
const method = 'GET'
|
|
144
|
+
const originalUrl = '/user/ok/outbox'
|
|
145
|
+
const res = {
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
const req = {
|
|
149
|
+
headers,
|
|
150
|
+
originalUrl,
|
|
151
|
+
method,
|
|
152
|
+
get: function (name) {
|
|
153
|
+
return this.headers[name.toLowerCase()]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await authenticator.authenticate(req, res, next)
|
|
157
|
+
await nockKeyRotate(username)
|
|
158
|
+
const date2 = new Date().toUTCString()
|
|
159
|
+
const signature2 = await nockSignature({
|
|
160
|
+
url: `${origin}/user/ok/outbox`,
|
|
161
|
+
date: date2,
|
|
162
|
+
username
|
|
163
|
+
})
|
|
164
|
+
const headers2 = {
|
|
165
|
+
date: date2,
|
|
166
|
+
signature: signature2,
|
|
167
|
+
host: URL.parse(origin).host
|
|
168
|
+
}
|
|
169
|
+
const req2 = {
|
|
170
|
+
headers: headers2,
|
|
171
|
+
originalUrl,
|
|
172
|
+
method,
|
|
173
|
+
get: function (name) {
|
|
174
|
+
return this.headers[name.toLowerCase()]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await authenticator.authenticate(req2, res, next)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('can authenticate a valid POST request', async () => {
|
|
181
|
+
const username = 'test3'
|
|
182
|
+
const type = 'Activity'
|
|
183
|
+
const activity = await as2.import({
|
|
184
|
+
id: nockFormat({ username, type }),
|
|
185
|
+
type
|
|
186
|
+
})
|
|
187
|
+
const rawBodyText = await activity.write()
|
|
188
|
+
const digest = await digester.digest(rawBodyText)
|
|
189
|
+
const date = new Date().toUTCString()
|
|
190
|
+
const method = 'POST'
|
|
191
|
+
const originalUrl = '/user/ok/inbox'
|
|
192
|
+
const signature = await nockSignature({
|
|
193
|
+
username,
|
|
194
|
+
url: `${origin}${originalUrl}`,
|
|
195
|
+
date,
|
|
196
|
+
digest,
|
|
197
|
+
method
|
|
198
|
+
})
|
|
199
|
+
const headers = {
|
|
200
|
+
date,
|
|
201
|
+
signature,
|
|
202
|
+
host: URL.parse(origin).host,
|
|
203
|
+
digest
|
|
204
|
+
}
|
|
205
|
+
const res = {
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
const req = {
|
|
209
|
+
headers,
|
|
210
|
+
originalUrl,
|
|
211
|
+
method,
|
|
212
|
+
get: function (name) {
|
|
213
|
+
return this.headers[name.toLowerCase()]
|
|
214
|
+
},
|
|
215
|
+
rawBodyText
|
|
216
|
+
}
|
|
217
|
+
await authenticator.authenticate(req, res, next)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('skips a request that is not signed', async () => {
|
|
221
|
+
const date = new Date().toUTCString()
|
|
222
|
+
const method = 'GET'
|
|
223
|
+
const originalUrl = '/user/ok/outbox'
|
|
224
|
+
const headers = {
|
|
225
|
+
date,
|
|
226
|
+
host: URL.parse(origin).host
|
|
227
|
+
}
|
|
228
|
+
const res = {
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
const req = {
|
|
232
|
+
headers,
|
|
233
|
+
originalUrl,
|
|
234
|
+
method,
|
|
235
|
+
get: function (name) {
|
|
236
|
+
return this.headers[name.toLowerCase()]
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
await authenticator.authenticate(req, res, next)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('can refuse a request signed with the wrong key', async () => {
|
|
243
|
+
const username = 'test'
|
|
244
|
+
const date = new Date().toUTCString()
|
|
245
|
+
const signature = await nockSignature({
|
|
246
|
+
url: `${origin}/user/ok/outbox`,
|
|
247
|
+
date,
|
|
248
|
+
username
|
|
249
|
+
})
|
|
250
|
+
const headers = {
|
|
251
|
+
date,
|
|
252
|
+
signature,
|
|
253
|
+
host: URL.parse(origin).host
|
|
254
|
+
}
|
|
255
|
+
const method = 'GET'
|
|
256
|
+
const originalUrl = '/user/ok/outbox'
|
|
257
|
+
const res = {
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
const req = {
|
|
261
|
+
headers,
|
|
262
|
+
originalUrl,
|
|
263
|
+
method,
|
|
264
|
+
get: function (name) {
|
|
265
|
+
return this.headers[name.toLowerCase()]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
await authenticator.authenticate(req, res, next)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('can refuse a request with a bad digest', async () => {
|
|
272
|
+
const username = 'test3'
|
|
273
|
+
const type = 'Activity'
|
|
274
|
+
const activity = await as2.import({
|
|
275
|
+
id: nockFormat({ username, type }),
|
|
276
|
+
type
|
|
277
|
+
})
|
|
278
|
+
const rawBodyText = await activity.write()
|
|
279
|
+
const digest = await digester.digest('This does not match the rawBodyText')
|
|
280
|
+
const date = new Date().toUTCString()
|
|
281
|
+
const method = 'POST'
|
|
282
|
+
const originalUrl = '/user/ok/inbox'
|
|
283
|
+
const signature = await nockSignature({
|
|
284
|
+
username,
|
|
285
|
+
url: `${origin}${originalUrl}`,
|
|
286
|
+
date,
|
|
287
|
+
digest,
|
|
288
|
+
method
|
|
289
|
+
})
|
|
290
|
+
const headers = {
|
|
291
|
+
date,
|
|
292
|
+
signature,
|
|
293
|
+
host: URL.parse(origin).host,
|
|
294
|
+
digest
|
|
295
|
+
}
|
|
296
|
+
const res = {
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
const req = {
|
|
300
|
+
headers,
|
|
301
|
+
originalUrl,
|
|
302
|
+
method,
|
|
303
|
+
get: function (name) {
|
|
304
|
+
return this.headers[name.toLowerCase()]
|
|
305
|
+
},
|
|
306
|
+
rawBodyText
|
|
307
|
+
}
|
|
308
|
+
await authenticator.authenticate(req, res, failNext)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('can refuse a request with a missing digest', async () => {
|
|
312
|
+
const username = 'test3'
|
|
313
|
+
const type = 'Activity'
|
|
314
|
+
const activity = await as2.import({
|
|
315
|
+
id: nockFormat({ username, type }),
|
|
316
|
+
type
|
|
317
|
+
})
|
|
318
|
+
const rawBodyText = await activity.write()
|
|
319
|
+
const date = new Date().toUTCString()
|
|
320
|
+
const method = 'POST'
|
|
321
|
+
const originalUrl = '/user/ok/inbox'
|
|
322
|
+
const signature = await nockSignature({
|
|
323
|
+
username,
|
|
324
|
+
url: `${origin}${originalUrl}`,
|
|
325
|
+
date,
|
|
326
|
+
method
|
|
327
|
+
})
|
|
328
|
+
const headers = {
|
|
329
|
+
date,
|
|
330
|
+
signature,
|
|
331
|
+
host: URL.parse(origin).host
|
|
332
|
+
}
|
|
333
|
+
const res = {
|
|
334
|
+
|
|
335
|
+
}
|
|
336
|
+
const req = {
|
|
337
|
+
headers,
|
|
338
|
+
originalUrl,
|
|
339
|
+
method,
|
|
340
|
+
get: function (name) {
|
|
341
|
+
return this.headers[name.toLowerCase()]
|
|
342
|
+
},
|
|
343
|
+
rawBodyText
|
|
344
|
+
}
|
|
345
|
+
await authenticator.authenticate(req, res, failNext)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('can refuse a request with a missing date', async () => {
|
|
349
|
+
const username = 'test'
|
|
350
|
+
const date = new Date().toUTCString()
|
|
351
|
+
const signature = await nockSignature({
|
|
352
|
+
url: `${origin}/user/ok/outbox`,
|
|
353
|
+
date,
|
|
354
|
+
username
|
|
355
|
+
})
|
|
356
|
+
const headers = {
|
|
357
|
+
signature,
|
|
358
|
+
host: URL.parse(origin).host
|
|
359
|
+
}
|
|
360
|
+
const method = 'GET'
|
|
361
|
+
const originalUrl = '/user/ok/outbox'
|
|
362
|
+
const res = {
|
|
363
|
+
|
|
364
|
+
}
|
|
365
|
+
const req = {
|
|
366
|
+
headers,
|
|
367
|
+
originalUrl,
|
|
368
|
+
method,
|
|
369
|
+
get: function (name) {
|
|
370
|
+
return this.headers[name.toLowerCase()]
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await authenticator.authenticate(req, res, failNext)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('can refuse a request with a badly formatted date', async () => {
|
|
377
|
+
const username = 'test'
|
|
378
|
+
const date = '3 Prairial CCXXXIII 14:00:35'
|
|
379
|
+
const signature = await nockSignature({
|
|
380
|
+
url: `${origin}/user/ok/outbox`,
|
|
381
|
+
date,
|
|
382
|
+
username
|
|
383
|
+
})
|
|
384
|
+
const headers = {
|
|
385
|
+
signature,
|
|
386
|
+
host: URL.parse(origin).host
|
|
387
|
+
}
|
|
388
|
+
const method = 'GET'
|
|
389
|
+
const originalUrl = '/user/ok/outbox'
|
|
390
|
+
const res = {
|
|
391
|
+
|
|
392
|
+
}
|
|
393
|
+
const req = {
|
|
394
|
+
headers,
|
|
395
|
+
originalUrl,
|
|
396
|
+
method,
|
|
397
|
+
get: function (name) {
|
|
398
|
+
return this.headers[name.toLowerCase()]
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
await authenticator.authenticate(req, res, failNext)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('can refuse a request with a past date outside of the skew window', async () => {
|
|
405
|
+
const username = 'test'
|
|
406
|
+
// 10 days ago
|
|
407
|
+
const date = (new Date(Date.now() - 10 * 24 * 60 * 60 * 1000)).toUTCString()
|
|
408
|
+
logger.debug(date)
|
|
409
|
+
const signature = await nockSignature({
|
|
410
|
+
url: `${origin}/user/ok/outbox`,
|
|
411
|
+
date,
|
|
412
|
+
username
|
|
413
|
+
})
|
|
414
|
+
const headers = {
|
|
415
|
+
signature,
|
|
416
|
+
host: URL.parse(origin).host
|
|
417
|
+
}
|
|
418
|
+
const method = 'GET'
|
|
419
|
+
const originalUrl = '/user/ok/outbox'
|
|
420
|
+
const res = {
|
|
421
|
+
|
|
422
|
+
}
|
|
423
|
+
const req = {
|
|
424
|
+
headers,
|
|
425
|
+
originalUrl,
|
|
426
|
+
method,
|
|
427
|
+
get: function (name) {
|
|
428
|
+
return this.headers[name.toLowerCase()]
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await authenticator.authenticate(req, res, failNext)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('can refuse a request with a future date outside of the skew window', async () => {
|
|
435
|
+
const username = 'test'
|
|
436
|
+
// 10 days ago
|
|
437
|
+
const date = (new Date(Date.now() + 10 * 24 * 60 * 60 * 1000)).toUTCString()
|
|
438
|
+
logger.debug(date)
|
|
439
|
+
const signature = await nockSignature({
|
|
440
|
+
url: `${origin}/user/ok/outbox`,
|
|
441
|
+
date,
|
|
442
|
+
username
|
|
443
|
+
})
|
|
444
|
+
const headers = {
|
|
445
|
+
signature,
|
|
446
|
+
host: URL.parse(origin).host
|
|
447
|
+
}
|
|
448
|
+
const method = 'GET'
|
|
449
|
+
const originalUrl = '/user/ok/outbox'
|
|
450
|
+
const res = {
|
|
451
|
+
|
|
452
|
+
}
|
|
453
|
+
const req = {
|
|
454
|
+
headers,
|
|
455
|
+
originalUrl,
|
|
456
|
+
method,
|
|
457
|
+
get: function (name) {
|
|
458
|
+
return this.headers[name.toLowerCase()]
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
await authenticator.authenticate(req, res, failNext)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, before, after, it } from 'node:test'
|
|
2
|
+
import { KeyStorage } from '../lib/keystorage.js'
|
|
3
|
+
import assert from 'node:assert'
|
|
4
|
+
import { Sequelize } from 'sequelize'
|
|
5
|
+
import Logger from 'pino'
|
|
6
|
+
|
|
7
|
+
describe('KeyStorage', async () => {
|
|
8
|
+
let connection = null
|
|
9
|
+
let storage = null
|
|
10
|
+
let logger = null
|
|
11
|
+
let firstPublicKey = null
|
|
12
|
+
let firstPrivateKey = null
|
|
13
|
+
let secondPublicKey = null
|
|
14
|
+
let secondPrivateKey = null
|
|
15
|
+
before(async () => {
|
|
16
|
+
connection = new Sequelize('sqlite::memory:', { logging: false })
|
|
17
|
+
await connection.authenticate()
|
|
18
|
+
logger = new Logger({
|
|
19
|
+
level: 'silent'
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
after(async () => {
|
|
23
|
+
await connection.close()
|
|
24
|
+
connection = null
|
|
25
|
+
logger = null
|
|
26
|
+
})
|
|
27
|
+
it('can initialize', async () => {
|
|
28
|
+
storage = new KeyStorage(connection, logger)
|
|
29
|
+
await storage.initialize()
|
|
30
|
+
})
|
|
31
|
+
it('can get a public key', async () => {
|
|
32
|
+
firstPublicKey = await storage.getPublicKey('test1')
|
|
33
|
+
assert.ok(firstPublicKey)
|
|
34
|
+
assert.equal(typeof firstPublicKey, 'string')
|
|
35
|
+
assert.match(firstPublicKey, /^-----BEGIN PUBLIC KEY-----\n/)
|
|
36
|
+
assert.match(firstPublicKey, /-----END PUBLIC KEY-----\n$/)
|
|
37
|
+
})
|
|
38
|
+
it('can get a public key again', async () => {
|
|
39
|
+
secondPublicKey = await storage.getPublicKey('test1')
|
|
40
|
+
assert.ok(secondPublicKey)
|
|
41
|
+
assert.equal(typeof secondPublicKey, 'string')
|
|
42
|
+
assert.match(secondPublicKey, /^-----BEGIN PUBLIC KEY-----\n/)
|
|
43
|
+
assert.match(secondPublicKey, /-----END PUBLIC KEY-----\n$/)
|
|
44
|
+
assert.equal(firstPublicKey, secondPublicKey)
|
|
45
|
+
})
|
|
46
|
+
it('can get a private key after getting a public key', async () => {
|
|
47
|
+
const privateKey = await storage.getPrivateKey('test1')
|
|
48
|
+
assert.ok(privateKey)
|
|
49
|
+
assert.equal(typeof privateKey, 'string')
|
|
50
|
+
assert.match(privateKey, /^-----BEGIN PRIVATE KEY-----\n/)
|
|
51
|
+
assert.match(privateKey, /-----END PRIVATE KEY-----\n$/)
|
|
52
|
+
})
|
|
53
|
+
it('can get a private key', async () => {
|
|
54
|
+
firstPrivateKey = await storage.getPrivateKey('test2')
|
|
55
|
+
assert.ok(firstPrivateKey)
|
|
56
|
+
assert.equal(typeof firstPrivateKey, 'string')
|
|
57
|
+
assert.match(firstPrivateKey, /^-----BEGIN PRIVATE KEY-----\n/)
|
|
58
|
+
assert.match(firstPrivateKey, /-----END PRIVATE KEY-----\n$/)
|
|
59
|
+
})
|
|
60
|
+
it('can get a private key again', async () => {
|
|
61
|
+
secondPrivateKey = await storage.getPrivateKey('test2')
|
|
62
|
+
assert.ok(secondPrivateKey)
|
|
63
|
+
assert.equal(typeof secondPrivateKey, 'string')
|
|
64
|
+
assert.match(secondPrivateKey, /^-----BEGIN PRIVATE KEY-----\n/)
|
|
65
|
+
assert.match(secondPrivateKey, /-----END PRIVATE KEY-----\n$/)
|
|
66
|
+
assert.equal(firstPrivateKey, secondPrivateKey)
|
|
67
|
+
})
|
|
68
|
+
it('can get a public key after getting a private key', async () => {
|
|
69
|
+
const publicKey = await storage.getPublicKey('test2')
|
|
70
|
+
assert.ok(publicKey)
|
|
71
|
+
assert.equal(typeof publicKey, 'string')
|
|
72
|
+
assert.match(publicKey, /^-----BEGIN PUBLIC KEY-----\n/)
|
|
73
|
+
assert.match(publicKey, /-----END PUBLIC KEY-----\n$/)
|
|
74
|
+
})
|
|
75
|
+
it('can get distinct public keys for distinct bots', async () => {
|
|
76
|
+
const publicKey = await storage.getPublicKey('test1')
|
|
77
|
+
const publicKey2 = await storage.getPublicKey('test2')
|
|
78
|
+
assert.ok(publicKey)
|
|
79
|
+
assert.ok(publicKey2)
|
|
80
|
+
assert.notEqual(publicKey, publicKey2)
|
|
81
|
+
})
|
|
82
|
+
it('can get distinct private keys for distinct bots', async () => {
|
|
83
|
+
const privateKey = await storage.getPrivateKey('test1')
|
|
84
|
+
const privateKey2 = await storage.getPrivateKey('test2')
|
|
85
|
+
assert.ok(privateKey)
|
|
86
|
+
assert.ok(privateKey2)
|
|
87
|
+
assert.notEqual(privateKey, privateKey2)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { Sequelize } from 'sequelize'
|
|
4
|
+
import { Transformer } from '../lib/microsyntax.js'
|
|
5
|
+
import { UrlFormatter } from '../lib/urlformatter.js'
|
|
6
|
+
import { KeyStorage } from '../lib/keystorage.js'
|
|
7
|
+
import { ActivityPubClient } from '../lib/activitypubclient.js'
|
|
8
|
+
import { nockSetup } from './utils/nock.js'
|
|
9
|
+
import { HTTPSignature } from '../lib/httpsignature.js'
|
|
10
|
+
import Logger from 'pino'
|
|
11
|
+
import { Digester } from '../lib/digester.js'
|
|
12
|
+
|
|
13
|
+
const AS2 = 'https://www.w3.org/ns/activitystreams#'
|
|
14
|
+
|
|
15
|
+
describe('microsyntax', async () => {
|
|
16
|
+
const tagNamespace = 'https://tags.example/tag/'
|
|
17
|
+
const origin = 'https://activitypubbot.example'
|
|
18
|
+
|
|
19
|
+
nockSetup('social.example')
|
|
20
|
+
|
|
21
|
+
const logger = Logger({
|
|
22
|
+
level: 'silent'
|
|
23
|
+
})
|
|
24
|
+
const digester = new Digester(logger)
|
|
25
|
+
const connection = new Sequelize('sqlite::memory:', { logging: false })
|
|
26
|
+
await connection.authenticate()
|
|
27
|
+
const keyStorage = new KeyStorage(connection, logger)
|
|
28
|
+
await keyStorage.initialize()
|
|
29
|
+
const formatter = new UrlFormatter(origin)
|
|
30
|
+
const signer = new HTTPSignature(logger)
|
|
31
|
+
const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
|
|
32
|
+
const transformer = new Transformer(tagNamespace, client)
|
|
33
|
+
|
|
34
|
+
it('has transformer', () => {
|
|
35
|
+
assert.ok(transformer)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('transform tagless text', async () => {
|
|
39
|
+
const text = 'Hello, world!'
|
|
40
|
+
const { html } = await transformer.transform(text)
|
|
41
|
+
it('has output', () => {
|
|
42
|
+
assert.ok(html)
|
|
43
|
+
})
|
|
44
|
+
it('is the same as input', () => {
|
|
45
|
+
assert.equal(html, `<p>${text}</p>`)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('transform hashtag', async () => {
|
|
50
|
+
const text = 'Hello, World! #greeting'
|
|
51
|
+
const { html, tag } = await transformer.transform(text)
|
|
52
|
+
it('has html output', () => {
|
|
53
|
+
assert.ok(html)
|
|
54
|
+
})
|
|
55
|
+
it('has tag', () => {
|
|
56
|
+
assert.ok(tag)
|
|
57
|
+
})
|
|
58
|
+
it('has correct html', () => {
|
|
59
|
+
assert.equal(html, '<p>Hello, World! <a href="https://tags.example/tag/greeting">#greeting</a></p>')
|
|
60
|
+
})
|
|
61
|
+
it('has correct tag', () => {
|
|
62
|
+
assert.equal(tag.length, 1)
|
|
63
|
+
assert.equal(tag[0].type, AS2 + 'Hashtag')
|
|
64
|
+
assert.equal(tag[0].name, '#greeting')
|
|
65
|
+
assert.equal(tag[0].href, 'https://tags.example/tag/greeting')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('transform url', async () => {
|
|
70
|
+
const text = 'Please visit https://example.com for more information.'
|
|
71
|
+
const { html, tag } = await transformer.transform(text)
|
|
72
|
+
it('has html output', () => {
|
|
73
|
+
assert.ok(html)
|
|
74
|
+
})
|
|
75
|
+
it('has tag', () => {
|
|
76
|
+
assert.ok(tag)
|
|
77
|
+
})
|
|
78
|
+
it('has correct html', () => {
|
|
79
|
+
assert.equal(html, '<p>Please visit <a href="https://example.com">https://example.com</a> for more information.</p>')
|
|
80
|
+
})
|
|
81
|
+
it('has correct tag', () => {
|
|
82
|
+
assert.equal(tag.length, 0)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('transform url with fragment', async () => {
|
|
87
|
+
const text = 'Please visit https://example.com#fragment for more information.'
|
|
88
|
+
const { html, tag } = await transformer.transform(text)
|
|
89
|
+
it('has html output', () => {
|
|
90
|
+
assert.ok(html)
|
|
91
|
+
})
|
|
92
|
+
it('has tag', () => {
|
|
93
|
+
assert.ok(tag)
|
|
94
|
+
})
|
|
95
|
+
it('has correct html', () => {
|
|
96
|
+
assert.equal(html, '<p>Please visit <a href="https://example.com#fragment">https://example.com#fragment</a> for more information.</p>')
|
|
97
|
+
})
|
|
98
|
+
it('has correct tag', () => {
|
|
99
|
+
assert.equal(tag.length, 0)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('transform full mention', async () => {
|
|
104
|
+
const text = 'Hello, @world@social.example !'
|
|
105
|
+
const { html, tag } = await transformer.transform(text)
|
|
106
|
+
it('has html output', () => {
|
|
107
|
+
assert.ok(html)
|
|
108
|
+
})
|
|
109
|
+
it('has tag', () => {
|
|
110
|
+
assert.ok(tag)
|
|
111
|
+
})
|
|
112
|
+
it('has correct html', () => {
|
|
113
|
+
assert.equal(html, '<p>Hello, <a href="https://social.example/profile/world">@world@social.example</a> !</p>')
|
|
114
|
+
})
|
|
115
|
+
it('has correct tag', () => {
|
|
116
|
+
assert.equal(tag.length, 1)
|
|
117
|
+
assert.equal(tag[0].type, 'Mention')
|
|
118
|
+
assert.equal(tag[0].name, '@world@social.example')
|
|
119
|
+
assert.equal(tag[0].href, 'https://social.example/profile/world')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|