@evanp/activitypub-bot 0.52.2 → 0.53.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.
- package/CHANGELOG.md +28 -1
- package/lib/app.js +2 -0
- package/lib/microsyntax.js +48 -31
- package/lib/routes/webfinger.js +12 -3
- package/lib/urlformatter.js +5 -5
- package/package.json +2 -2
- package/.cache/standard/v17/.cache_1b54s7r +0 -1
- package/.claude/settings.json +0 -25
- package/.claude/settings.local.json +0 -10
- package/.vscode/settings.json +0 -17
- package/.vscode/tasks.json +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,31 @@ and this project adheres to
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.53.1] - 2026-10-09
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- Update proxy-addr from 2.0.7 to 2.0.8 to address a security vulnerability
|
|
17
|
+
(Dependabot #284).
|
|
18
|
+
|
|
19
|
+
## [0.53.0] - 2026-10-08
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Upgrade activitypub-webfinger to 0.3.0 and add BotContext tests for forward
|
|
24
|
+
and reverse discovery with non-ASCII usernames and domains (#282).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Linkify mentions with non-ASCII usernames and domains or ASCII punctuation
|
|
29
|
+
in usernames, preserving HTML escaping and URL fragments; add regression
|
|
30
|
+
tests for Unicode handles and username punctuation (#282).
|
|
31
|
+
- Normalize server origins to support Unicode domains; add regression tests
|
|
32
|
+
for discovery, actor and collection endpoints, and OKBot replies (#282).
|
|
33
|
+
- Support Unicode local usernames by encoding URL paths and acct: URIs and
|
|
34
|
+
decoding usernames during URL parsing and WebFinger lookup; return 400 for
|
|
35
|
+
malformed WebFinger username escapes and add regression tests (#282).
|
|
36
|
+
|
|
12
37
|
## [0.52.2] - 2026-10-05
|
|
13
38
|
|
|
14
39
|
### Fixed
|
|
@@ -1695,7 +1720,9 @@ Hot patch on the `stable` branch.
|
|
|
1695
1720
|
- Docker multi-platform build workflow.
|
|
1696
1721
|
- Dependabot configuration.
|
|
1697
1722
|
|
|
1698
|
-
[Unreleased]: https://github.com/evanp/activitypub-bot/compare/v0.
|
|
1723
|
+
[Unreleased]: https://github.com/evanp/activitypub-bot/compare/v0.53.1...HEAD
|
|
1724
|
+
[0.53.1]: https://github.com/evanp/activitypub-bot/compare/v0.53.0...v0.53.1
|
|
1725
|
+
[0.53.0]: https://github.com/evanp/activitypub-bot/compare/v0.52.2...v0.53.0
|
|
1699
1726
|
[0.52.2]: https://github.com/evanp/activitypub-bot/compare/v0.52.1...v0.52.2
|
|
1700
1727
|
[0.52.1]: https://github.com/evanp/activitypub-bot/compare/v0.52.0...v0.52.1
|
|
1701
1728
|
[0.52.0]: https://github.com/evanp/activitypub-bot/compare/v0.51.4...v0.52.0
|
package/lib/app.js
CHANGED
|
@@ -72,6 +72,8 @@ function createWorkers (logger, count, WorkerClass, ...args) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent', deliveryWorkerCount = 2, distributionWorkerCount = 8, fanoutWorkerCount = 4, intakeWorkerCount = 2, indexFileName = DEFAULT_INDEX_FILENAME, profileFileName = DEFAULT_PROFILE_FILENAME, allowPrivateNetworkRequests = false, redisUrl, trustProxy, domainBlockFileName }) {
|
|
75
|
+
// normalize the origin, including Unicode -> punycode
|
|
76
|
+
origin = (new URL(origin)).origin
|
|
75
77
|
const logger = Logger({
|
|
76
78
|
level: logLevel
|
|
77
79
|
})
|
package/lib/microsyntax.js
CHANGED
|
@@ -30,8 +30,8 @@ export class Transformer {
|
|
|
30
30
|
let html = this.#escape(text)
|
|
31
31
|
let tag = [];
|
|
32
32
|
({ html, tag } = this.#replaceUrls(html, tag));
|
|
33
|
-
({ html, tag } = this.#
|
|
34
|
-
({ html, tag } =
|
|
33
|
+
({ html, tag } = await this.#replaceMentions(html, tag));
|
|
34
|
+
({ html, tag } = this.#replaceHashtags(html, tag))
|
|
35
35
|
html = `<p>${html}</p>`
|
|
36
36
|
return { html, tag }
|
|
37
37
|
}
|
|
@@ -67,36 +67,19 @@ export class Transformer {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
async #replaceMentions (html, tag) {
|
|
70
|
-
const self = this
|
|
71
|
-
const webfinger = /@[a-zA-Z0-9_]+([a-zA-Z0-9_.-]+[a-zA-Z0-9_]+)?@[a-zA-Z0-9_.-]+/g
|
|
72
70
|
const segments = this.#segment(html)
|
|
73
71
|
for (const i in segments) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
segments[i] = await this.#replaceAsync(segments[i], webfinger, async (match) => {
|
|
77
|
-
const href = await self.#homePage(match.slice(1))
|
|
78
|
-
if (!href) return match
|
|
79
|
-
let url
|
|
80
|
-
try {
|
|
81
|
-
url = new URL(href)
|
|
82
|
-
} catch (err) {
|
|
83
|
-
return match
|
|
84
|
-
}
|
|
85
|
-
if (url.protocol !== 'https:') {
|
|
86
|
-
return match
|
|
87
|
-
}
|
|
88
|
-
tag.push({ type: 'Mention', name: match, href: url.href })
|
|
89
|
-
const escaped = this.#escape(url.href)
|
|
90
|
-
const escapedMatch = this.#escape(match)
|
|
91
|
-
return `<a href="${escaped}">${escapedMatch}</a>`
|
|
92
|
-
})
|
|
72
|
+
if (this.#isLink(segments[i])) continue
|
|
73
|
+
segments[i] = await this.#replaceMention(this.#unescape(segments[i]), tag)
|
|
93
74
|
}
|
|
94
75
|
return { html: segments.join(''), tag }
|
|
95
76
|
}
|
|
96
77
|
|
|
97
78
|
async #homePage (webfinger) {
|
|
98
79
|
if (!this.#client) return null
|
|
99
|
-
const
|
|
80
|
+
const delimiter = webfinger.lastIndexOf('@')
|
|
81
|
+
const username = webfinger.slice(0, delimiter)
|
|
82
|
+
const domain = webfinger.slice(delimiter + 1)
|
|
100
83
|
if (domain.toLowerCase() === this.#formatter.hostname) {
|
|
101
84
|
return this.#formatter.format({ username, type: 'profile' })
|
|
102
85
|
}
|
|
@@ -136,18 +119,43 @@ export class Transformer {
|
|
|
136
119
|
return html.split(/(<[^>]+>[^<]+<\/[^>]+>)/)
|
|
137
120
|
}
|
|
138
121
|
|
|
139
|
-
async #
|
|
122
|
+
async #transformMention (match, tag) {
|
|
123
|
+
const escapedMatch = this.#escape(match)
|
|
124
|
+
const href = await this.#homePage(match.slice(1))
|
|
125
|
+
if (!href) return escapedMatch
|
|
126
|
+
let url
|
|
127
|
+
try {
|
|
128
|
+
url = new URL(href)
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return escapedMatch
|
|
131
|
+
}
|
|
132
|
+
if (url.protocol !== 'https:') {
|
|
133
|
+
return escapedMatch
|
|
134
|
+
}
|
|
135
|
+
tag.push({ type: 'Mention', name: match, href: url.href })
|
|
136
|
+
const escaped = this.#escape(url.href)
|
|
137
|
+
return `<a href="${escaped}">${escapedMatch}</a>`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async #replaceMention (str, tag) {
|
|
141
|
+
const webfinger = /(@[\x21-\x7E\p{L}\p{Nd}\p{M}]+@[\p{L}\p{Nd}](?:[\p{L}\p{Nd}\p{M}-]*[\p{L}\p{Nd}\p{M}])?(?:\.[\p{L}\p{Nd}](?:[\p{L}\p{Nd}\p{M}-]*[\p{L}\p{Nd}\p{M}])?)*)/ug
|
|
142
|
+
|
|
143
|
+
const parts = str.split(webfinger)
|
|
144
|
+
|
|
140
145
|
const promises = []
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
promises
|
|
144
|
-
|
|
146
|
+
|
|
147
|
+
for (const i in parts) {
|
|
148
|
+
promises[i] = (i % 2 === 0)
|
|
149
|
+
? new Promise((resolve, reject) => {
|
|
150
|
+
resolve(this.#escape(parts[i]))
|
|
151
|
+
})
|
|
152
|
+
: this.#transformMention(parts[i], tag)
|
|
153
|
+
}
|
|
145
154
|
|
|
146
155
|
// Wait for all async replacements to resolve
|
|
147
156
|
const replacements = await Promise.all(promises)
|
|
148
157
|
|
|
149
|
-
|
|
150
|
-
return str.replace(regex, () => replacements.shift())
|
|
158
|
+
return replacements.join('')
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
#escape (text) {
|
|
@@ -158,4 +166,13 @@ export class Transformer {
|
|
|
158
166
|
.replace(/"/g, '"')
|
|
159
167
|
.replace(/'/g, ''')
|
|
160
168
|
}
|
|
169
|
+
|
|
170
|
+
#unescape (text) {
|
|
171
|
+
return text
|
|
172
|
+
.replace(/</g, '<')
|
|
173
|
+
.replace(/>/g, '>')
|
|
174
|
+
.replace(/"/g, '"')
|
|
175
|
+
.replace(/'/g, '\'')
|
|
176
|
+
.replace(/&/g, '&')
|
|
177
|
+
}
|
|
161
178
|
}
|
package/lib/routes/webfinger.js
CHANGED
|
@@ -70,15 +70,24 @@ async function httpsWebfinger (resource, req, res, next) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function acctWebfinger (resource, req, res, next) {
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const address = resource.substring(5)
|
|
74
|
+
const delimiter = address.lastIndexOf('@')
|
|
75
|
+
if (delimiter <= 0 || delimiter === address.length - 1) {
|
|
75
76
|
return next(new ProblemDetailsError(400, `Invalid resource parameter ${resource}`))
|
|
76
77
|
}
|
|
78
|
+
const username = address.slice(0, delimiter)
|
|
79
|
+
const domain = address.slice(delimiter + 1)
|
|
77
80
|
const { host } = new URL(req.app.locals.origin)
|
|
78
81
|
if (domain !== host) {
|
|
79
82
|
return next(new ProblemDetailsError(400, `Invalid domain ${domain} in resource parameter`))
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
let decoded
|
|
85
|
+
try {
|
|
86
|
+
decoded = decodeURIComponent(username)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return next(new ProblemDetailsError(400, `Invalid percent-encoded username ${username} in resource parameter`))
|
|
89
|
+
}
|
|
90
|
+
return await botWebfinger(decoded, req, res, next)
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
router.get('/.well-known/webfinger', async (req, res, next) => {
|
package/lib/urlformatter.js
CHANGED
|
@@ -16,9 +16,9 @@ export class UrlFormatter {
|
|
|
16
16
|
if (server) {
|
|
17
17
|
return this.format({ username: this.#hostname, type, nanoid, collection, page, server: false })
|
|
18
18
|
} else if (username && type && type === 'profile') {
|
|
19
|
-
return `${this.#origin}/profile/${username}`
|
|
19
|
+
return `${this.#origin}/profile/${encodeURIComponent(username)}`
|
|
20
20
|
} else if (username) {
|
|
21
|
-
base = `${this.#origin}/user/${username}`
|
|
21
|
+
base = `${this.#origin}/user/${encodeURIComponent(username)}`
|
|
22
22
|
} else {
|
|
23
23
|
throw new Error('Cannot format URL without username or server')
|
|
24
24
|
}
|
|
@@ -67,11 +67,11 @@ export class UrlFormatter {
|
|
|
67
67
|
}
|
|
68
68
|
let pathParts = parsed.pathname.slice(1).split('/')
|
|
69
69
|
if (pathParts.length > 0 && pathParts[0] === 'user') {
|
|
70
|
-
parts.username = pathParts[1]
|
|
70
|
+
parts.username = decodeURIComponent(pathParts[1])
|
|
71
71
|
pathParts = pathParts.slice(2)
|
|
72
72
|
parts.server = (parts.username === this.#hostname)
|
|
73
73
|
} else if (pathParts.length > 0 && pathParts[0] === 'profile') {
|
|
74
|
-
parts.username = pathParts[1]
|
|
74
|
+
parts.username = decodeURIComponent(pathParts[1])
|
|
75
75
|
parts.type = 'profile'
|
|
76
76
|
return parts
|
|
77
77
|
}
|
|
@@ -108,7 +108,7 @@ export class UrlFormatter {
|
|
|
108
108
|
|
|
109
109
|
acct (username = null) {
|
|
110
110
|
return (username)
|
|
111
|
-
? `acct:${username}@${this.#hostname}`
|
|
111
|
+
? `acct:${encodeURIComponent(username)}@${this.#hostname}`
|
|
112
112
|
: `acct:${this.#hostname}@${this.#hostname}`
|
|
113
113
|
}
|
|
114
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evanp/activitypub-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.1",
|
|
4
4
|
"description": "server-side ActivityPub bot framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@isaacs/ttlcache": "^2.1.4",
|
|
34
34
|
"@socialwebfoundation/ap-components": "^1.4.2",
|
|
35
|
-
"activitypub-webfinger": "^0.
|
|
35
|
+
"activitypub-webfinger": "^0.3.0",
|
|
36
36
|
"activitystrea.ms": "^4.0.1",
|
|
37
37
|
"csv-parse": "^7.0.0",
|
|
38
38
|
"express": "^5.2.1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[{"/Users/evanp/Documents/Code/activitypub-bot/tests/activitydeliverer.test.js":"1"},{"size":1743,"mtime":1770107423341,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","suppressedMessages":"6","errorCount":18,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},"eeiv40","/Users/evanp/Documents/Code/activitypub-bot/tests/activitydeliverer.test.js",["7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"],[],{"ruleId":"25","severity":2,"message":"26","line":20,"column":14,"nodeType":"27","messageId":"28","endLine":20,"endColumn":20},{"ruleId":"25","severity":2,"message":"29","line":22,"column":5,"nodeType":"27","messageId":"28","endLine":22,"endColumn":15},{"ruleId":"25","severity":2,"message":"29","line":23,"column":11,"nodeType":"27","messageId":"28","endLine":23,"endColumn":21},{"ruleId":"25","severity":2,"message":"30","line":24,"column":11,"nodeType":"27","messageId":"28","endLine":24,"endColumn":24},{"ruleId":"25","severity":2,"message":"29","line":24,"column":25,"nodeType":"27","messageId":"28","endLine":24,"endColumn":35},{"ruleId":"25","severity":2,"message":"31","line":25,"column":24,"nodeType":"27","messageId":"28","endLine":25,"endColumn":36},{"ruleId":"25","severity":2,"message":"29","line":25,"column":37,"nodeType":"27","messageId":"28","endLine":25,"endColumn":47},{"ruleId":"25","severity":2,"message":"32","line":26,"column":5,"nodeType":"27","messageId":"28","endLine":26,"endColumn":18},{"ruleId":"25","severity":2,"message":"29","line":26,"column":39,"nodeType":"27","messageId":"28","endLine":26,"endColumn":49},{"ruleId":"25","severity":2,"message":"33","line":27,"column":5,"nodeType":"27","messageId":"28","endLine":27,"endColumn":15},{"ruleId":"25","severity":2,"message":"29","line":27,"column":33,"nodeType":"27","messageId":"28","endLine":27,"endColumn":43},{"ruleId":"25","severity":2,"message":"34","line":28,"column":24,"nodeType":"27","messageId":"28","endLine":28,"endColumn":37},{"ruleId":"25","severity":2,"message":"35","line":29,"column":26,"nodeType":"27","messageId":"28","endLine":29,"endColumn":34},{"ruleId":"25","severity":2,"message":"33","line":30,"column":36,"nodeType":"27","messageId":"28","endLine":30,"endColumn":46},{"ruleId":"25","severity":2,"message":"32","line":33,"column":7,"nodeType":"27","messageId":"28","endLine":33,"endColumn":20},{"ruleId":"25","severity":2,"message":"36","line":34,"column":7,"nodeType":"27","messageId":"28","endLine":34,"endColumn":18},{"ruleId":"25","severity":2,"message":"37","line":36,"column":7,"nodeType":"27","messageId":"28","endLine":36,"endColumn":12},{"ruleId":"25","severity":2,"message":"38","line":37,"column":7,"nodeType":"27","messageId":"28","endLine":37,"endColumn":12},"no-undef","'Logger' is not defined.","Identifier","undef","'connection' is not defined.","'runMigrations' is not defined.","'ActorStorage' is not defined.","'objectStorage' is not defined.","'keyStorage' is not defined.","'HTTPSignature' is not defined.","'Digester' is not defined.","'distributor' is not defined.","'cache' is not defined.","'authz' is not defined."]
|
package/.claude/settings.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(kubectl get *)",
|
|
5
|
-
"Bash(kubectl top *)",
|
|
6
|
-
"Read(//private/tmp/heap-snapshots/**)",
|
|
7
|
-
"Bash(node --max-old-space-size=8192 analyze.mjs snap1-30min-mppzz.heapsnapshot)",
|
|
8
|
-
"Bash(tee analyze1.out)",
|
|
9
|
-
"Bash(node --max-old-space-size=8192 fr-owners.mjs snap1-30min-mppzz.heapsnapshot)",
|
|
10
|
-
"Bash(node --max-old-space-size=8192 fr-trace.mjs snap1-30min-mppzz.heapsnapshot 123343)",
|
|
11
|
-
"Bash(node --max-old-space-size=10240 diff.mjs snap1-30min-mppzz.heapsnapshot snap2-92min-mppzz.heapsnapshot)",
|
|
12
|
-
"Bash(tee diff.out)",
|
|
13
|
-
"Bash(mkdir -p /Users/evanp/Documents/Code/aode-relay-test/relay /Users/evanp/Documents/Code/aode-relay-test/bots && ls /Users/evanp/Documents/Code/aode-relay-test/)",
|
|
14
|
-
"Bash(docker builder *)",
|
|
15
|
-
"Bash(docker image *)",
|
|
16
|
-
"Bash(docker container *)",
|
|
17
|
-
"Bash(docker run *)"
|
|
18
|
-
],
|
|
19
|
-
"additionalDirectories": [
|
|
20
|
-
"/tmp/heap-snapshots",
|
|
21
|
-
"/private/tmp/heap-snapshots",
|
|
22
|
-
"/Users/evanp/Documents/Code/aode-relay-test"
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|
package/.vscode/settings.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"editor.formatOnSave": false,
|
|
3
|
-
"editor.formatOnSaveMode": "modificationsIfAvailable",
|
|
4
|
-
"editor.codeActionsOnSave": {
|
|
5
|
-
"source.fixAll.eslint": "explicit"
|
|
6
|
-
},
|
|
7
|
-
"javascript.format.enable": false,
|
|
8
|
-
"standard.enable": false,
|
|
9
|
-
"standard.autoFixOnSave": false,
|
|
10
|
-
"eslint.format.enable": true,
|
|
11
|
-
"eslint.validate": [
|
|
12
|
-
"javascript"
|
|
13
|
-
],
|
|
14
|
-
"[javascript]": {
|
|
15
|
-
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
|
16
|
-
}
|
|
17
|
-
}
|