@bitsocial/wordfilter-challenge 0.2.0 → 0.3.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/AGENTS.md +8 -1
- package/README.md +100 -15
- package/dist/apply-wordfilters.js +17 -1
- package/dist/apply-wordfilters.js.map +1 -1
- package/dist/wordfilter-challenge.js +23 -12
- package/dist/wordfilter-challenge.js.map +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -10,7 +10,9 @@ Reference source files for this repository:
|
|
|
10
10
|
Workflow:
|
|
11
11
|
|
|
12
12
|
- Before every commit, run `npm run typecheck` and ensure it passes with no errors.
|
|
13
|
-
- Before every commit, run `npm test` and ensure it passes.
|
|
13
|
+
- Before every commit, run `npm test` (vitest) and ensure it passes.
|
|
14
|
+
- Never push to a PR branch without first running `npm run typecheck` and `npm test` locally and seeing
|
|
15
|
+
them pass. CI runs the same checks, but a red PR is a round trip that a local run avoids.
|
|
14
16
|
|
|
15
17
|
Conventions specific to this challenge:
|
|
16
18
|
|
|
@@ -33,6 +35,11 @@ Conventions specific to this challenge:
|
|
|
33
35
|
startup rather than the offending edit.
|
|
34
36
|
- Absent publication fields pass. `publication-match` treats a missing property as a failure, which would
|
|
35
37
|
reject every vote. Do not copy that.
|
|
38
|
+
- Test fixtures are typed against pkc-js's own challenge-time publication types
|
|
39
|
+
(`GetChallengeArgsInput["challengeRequestMessage"][K]`) through the `build*` helpers in
|
|
40
|
+
`test/wordfilter-challenge.test.ts`. Do not cast a publication through `as unknown as`: that is how a
|
|
41
|
+
`content` on a commentModeration, a field no client can send, once passed as a test. Each publication
|
|
42
|
+
type gets its own test, with the shape it actually has on the wire.
|
|
36
43
|
|
|
37
44
|
Releasing:
|
|
38
45
|
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ So the signal a client keys off is an option key that names a **contract** rathe
|
|
|
31
31
|
| Public option | Required | Meaning |
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| `wordfilter/v1/rules` | yes | JSON array of `{ src, dst }`. Its presence is what identifies the contract |
|
|
34
|
-
| `wordfilter/v1/fieldNames` | no | JSON array of dot-notation paths. Defaults to
|
|
34
|
+
| `wordfilter/v1/fieldNames` | no | JSON array of dot-notation paths, each starting with the publication type. Defaults to the list under [Default fields](#default-fields) |
|
|
35
35
|
|
|
36
36
|
This package is one implementation. Anything that publishes `wordfilter/v1/rules` with the semantics below claims the contract, and a client written against it keeps working with implementations that did not exist when the client was written. Keying on a package name instead, whether through a hardcoded `@bitsocial/wordfilter-challenge` or a generic `wordfilters` option, would enshrine one implementation in every UI and lock out every fork, competitor and in-house variant that behaves identically. See [issue #1](https://github.com/bitsocialnet/wordfilter-challenge/issues/1).
|
|
37
37
|
|
|
@@ -39,6 +39,21 @@ The namespace covers exactly what a publishing client must read, and nothing els
|
|
|
39
39
|
|
|
40
40
|
`wordfilter/v2/rules` would be a different key. A client that understands both reads both, and a community can publish both during a transition without either side guessing.
|
|
41
41
|
|
|
42
|
+
### Default fields
|
|
43
|
+
|
|
44
|
+
When a community does not publish `wordfilter/v1/fieldNames`, both sides check this list. Every path starts with the publication type, followed by the field's path inside that publication exactly as it is on the wire.
|
|
45
|
+
|
|
46
|
+
| Publication type | Default paths |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `comment` | `comment.content`, `comment.title`, `comment.author.displayName` |
|
|
49
|
+
| `commentEdit` | `commentEdit.content`, `commentEdit.reason`, `commentEdit.author.displayName` |
|
|
50
|
+
| `vote` | `vote.author.displayName` |
|
|
51
|
+
|
|
52
|
+
The defaults cover what an ordinary author publishes. Moderator and owner text is not in them: a moderation's reason and a community edit's title and description can be filtered by naming their paths in `wordfilter/v1/fieldNames`, as `commentModeration.commentModeration.reason`, `communityEdit.communityEdit.title` and `communityEdit.communityEdit.description`. The doubled segment is the wire shape, not a typo: the publication type is `commentModeration`, and pkc-js puts the moderation's fields under a `commentModeration` property of it; likewise for `communityEdit`.
|
|
53
|
+
|
|
54
|
+
Naming the publication type in every path is deliberate. `content` on a comment and `content` on a comment edit are the same field today, but nothing guarantees they stay that way, and a bare `content` could not tell them apart. A path whose first segment is not one of the five publication types is rejected by `validateChallengeSettings`, and by `getChallenge` for a config that predates that check, because it would match nothing on any request and the wordfilter would have quietly stopped filtering.
|
|
55
|
+
|
|
56
|
+
|
|
42
57
|
## For UI client developers
|
|
43
58
|
|
|
44
59
|
This is the section that matters if you are building a frontend that publishes to communities using this contract.
|
|
@@ -59,7 +74,15 @@ function getWordfilterConfigs(community) {
|
|
|
59
74
|
const rules = JSON.parse(challenge.publicOptions["wordfilter/v1/rules"]);
|
|
60
75
|
const fieldNames = challenge.publicOptions["wordfilter/v1/fieldNames"]
|
|
61
76
|
? JSON.parse(challenge.publicOptions["wordfilter/v1/fieldNames"])
|
|
62
|
-
: [
|
|
77
|
+
: [
|
|
78
|
+
"comment.content",
|
|
79
|
+
"comment.title",
|
|
80
|
+
"comment.author.displayName",
|
|
81
|
+
"commentEdit.content",
|
|
82
|
+
"commentEdit.reason",
|
|
83
|
+
"commentEdit.author.displayName",
|
|
84
|
+
"vote.author.displayName"
|
|
85
|
+
];
|
|
63
86
|
// JSON.parse is happy to hand back null, an object or an array of numbers, and
|
|
64
87
|
// applyWordfilters would throw on any of them. Treat the wrong shape like bad JSON.
|
|
65
88
|
if (!Array.isArray(rules) || !rules.every(isRule)) return undefined;
|
|
@@ -122,17 +145,65 @@ const configs = getWordfilterConfigs(community);
|
|
|
122
145
|
const rulesFor = (fieldName) =>
|
|
123
146
|
configs.filter(({ fieldNames }) => fieldNames.includes(fieldName)).flatMap(({ rules }) => rules);
|
|
124
147
|
|
|
125
|
-
if (content) content = applyWordfilters(content, rulesFor("content"));
|
|
126
|
-
if (title) title = applyWordfilters(title, rulesFor("title"));
|
|
127
|
-
if (displayName) displayName = applyWordfilters(displayName, rulesFor("author.displayName"));
|
|
148
|
+
if (content) content = applyWordfilters(content, rulesFor("comment.content"));
|
|
149
|
+
if (title) title = applyWordfilters(title, rulesFor("comment.title"));
|
|
150
|
+
if (displayName) displayName = applyWordfilters(displayName, rulesFor("comment.author.displayName"));
|
|
128
151
|
|
|
129
152
|
const comment = await pkc.createComment({ content, title, author: { displayName }, communityAddress, signer });
|
|
130
153
|
await comment.publish();
|
|
131
154
|
```
|
|
132
155
|
|
|
133
|
-
|
|
156
|
+
The challenge runs on every publication type, not only new comments. The [default field list](#default-fields) also names the text of a comment edit and the display name on a vote, so the same `rulesFor` call applies before each of these, with the path prefixed by the publication type:
|
|
134
157
|
|
|
135
|
-
|
|
158
|
+
```js
|
|
159
|
+
// Comment edit: content and reason are top-level on the edit, displayName is under author.
|
|
160
|
+
const edit = await pkc.createCommentEdit({
|
|
161
|
+
commentCid,
|
|
162
|
+
content: applyWordfilters(newContent, rulesFor("commentEdit.content")),
|
|
163
|
+
reason: applyWordfilters(reason, rulesFor("commentEdit.reason")),
|
|
164
|
+
author: { displayName: applyWordfilters(displayName, rulesFor("commentEdit.author.displayName")) },
|
|
165
|
+
communityAddress,
|
|
166
|
+
signer
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Vote: the only text on it is the display name.
|
|
170
|
+
const vote = await pkc.createVote({
|
|
171
|
+
commentCid,
|
|
172
|
+
vote: 1,
|
|
173
|
+
author: { displayName: applyWordfilters(displayName, rulesFor("vote.author.displayName")) },
|
|
174
|
+
communityAddress,
|
|
175
|
+
signer
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Those paths are the defaults, so they are the minimum. If a community publishes other paths in `wordfilter/v1/fieldNames`, run the same call on whatever your client puts at those paths; the community checks exactly the configured paths and nothing else, so an unfiltered custom field is a rejection just like an unfiltered display name. The paths a community is most likely to add are moderator and owner text, which `rulesFor` returns empty for unless the community names them:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
// Comment moderation: the moderator's reason lives under the publication's commentModeration property,
|
|
184
|
+
// hence the doubled segment.
|
|
185
|
+
const moderation = await pkc.createCommentModeration({
|
|
186
|
+
commentCid,
|
|
187
|
+
commentModeration: {
|
|
188
|
+
removed: true,
|
|
189
|
+
reason: applyWordfilters(reason, rulesFor("commentModeration.commentModeration.reason"))
|
|
190
|
+
},
|
|
191
|
+
communityAddress,
|
|
192
|
+
signer
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Community edit: title and description live under the publication's communityEdit property.
|
|
196
|
+
const communityEdit = await pkc.createCommunityEdit({
|
|
197
|
+
communityEdit: {
|
|
198
|
+
title: applyWordfilters(title, rulesFor("communityEdit.communityEdit.title")),
|
|
199
|
+
description: applyWordfilters(description, rulesFor("communityEdit.communityEdit.description"))
|
|
200
|
+
},
|
|
201
|
+
communityAddress,
|
|
202
|
+
signer
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The `wordfilter/v1/fieldNames` entries are dot-notation paths, the same convention `publication-match` uses for `propertyName`, resolved against the challenge request's publication map rather than the publication itself. So the first segment names the publication type, and a path is simply absent, and passes, on a request carrying a different type: `commentEdit.content` never sees a new comment, and `comment.content` never sees an edit.
|
|
136
207
|
|
|
137
208
|
Two challenges on the same community whose rules undo each other, `foo → bar` in one and `bar → foo` in another, cannot be caught by either challenge's `validateChallengeSettings`, which sees only its own settings. The merged call then either throws `did not stabilise` or settles on text that one of the challenges still rejects, and the community returns that challenge's `error`. Both are the right outcome: no text containing either word can satisfy both challenges, so the board is misconfigured and the owner has to fix it. Handle it the same way as any other rejection, in step 5.
|
|
138
209
|
|
|
@@ -207,9 +278,13 @@ await community.edit({
|
|
|
207
278
|
{ src: "spamword", dst: "" }
|
|
208
279
|
]),
|
|
209
280
|
"wordfilter/v1/fieldNames": JSON.stringify([
|
|
210
|
-
"content",
|
|
211
|
-
"title",
|
|
212
|
-
"author.displayName"
|
|
281
|
+
"comment.content",
|
|
282
|
+
"comment.title",
|
|
283
|
+
"comment.author.displayName",
|
|
284
|
+
"commentEdit.content",
|
|
285
|
+
"commentEdit.reason",
|
|
286
|
+
"commentEdit.author.displayName",
|
|
287
|
+
"vote.author.displayName"
|
|
213
288
|
]),
|
|
214
289
|
error: "This board replaces certain words. Please repost with the replacements applied."
|
|
215
290
|
},
|
|
@@ -227,10 +302,10 @@ await community.edit({
|
|
|
227
302
|
| Option | Required | Must be in `publicOptions` | Description |
|
|
228
303
|
|---|---|---|---|
|
|
229
304
|
| `wordfilter/v1/rules` | yes | yes | JSON array of `{ src, dst }`, applied in array order, cascading |
|
|
230
|
-
| `wordfilter/v1/fieldNames` | no | when set | JSON array of dot-notation paths. Defaults to
|
|
305
|
+
| `wordfilter/v1/fieldNames` | no | when set | JSON array of dot-notation paths, each starting with the publication type. Defaults to the list under [Default fields](#default-fields) |
|
|
231
306
|
| `error` | no | owner's call | Message shown to the author when a publication is rejected |
|
|
232
307
|
|
|
233
|
-
An option has to be public when a client cannot satisfy the challenge without reading it. That covers `wordfilter/v1/rules` always, and `wordfilter/v1/fieldNames` whenever the owner sets it: a client filtering the
|
|
308
|
+
An option has to be public when a client cannot satisfy the challenge without reading it. That covers `wordfilter/v1/rules` always, and `wordfilter/v1/fieldNames` whenever the owner sets it: a client filtering the default fields cannot satisfy a community checking a different set, and nothing in the published record would explain the rejections. Those two are also exactly the options the contract namespaces, for the same reason. `error` is returned in the rejection itself, so publishing it is transparency rather than a requirement, and it keeps its plain name.
|
|
234
309
|
|
|
235
310
|
### Validation
|
|
236
311
|
|
|
@@ -242,15 +317,15 @@ An option has to be public when a client cannot satisfy the challenge without re
|
|
|
242
317
|
- the same `src` in more than one rule
|
|
243
318
|
- any `dst` containing any `src`
|
|
244
319
|
- more than 64 rules, or a `src` or `dst` longer than 128 characters
|
|
245
|
-
- `wordfilter/v1/fieldNames`, when set, unparseable, not an array of non-empty strings, or missing from `publicOptions`
|
|
320
|
+
- `wordfilter/v1/fieldNames`, when set, unparseable, not an array of non-empty strings, containing a path whose first segment is not a publication type (`comment`, `vote`, `commentEdit`, `commentModeration`, `communityEdit`), or missing from `publicOptions`
|
|
246
321
|
|
|
247
322
|
The `src === dst`, duplicate `src` and `dst`-contains-`src` checks all compare case-insensitively, because matching is case-insensitive: `{src: "LOL", dst: "lol"}` replaces nothing just as surely as `{src: "lol", dst: "lol"}` does.
|
|
248
323
|
|
|
249
324
|
The `dst` containing `src` rule is what guarantees the client's loop terminates. Without it a rule like `lol` becomes `lolol` produces output that always contains a filtered word, making every post permanently unpublishable.
|
|
250
325
|
|
|
251
|
-
## Migrating from 0.1.x
|
|
326
|
+
## Migrating from 0.1.x and 0.2.0
|
|
252
327
|
|
|
253
|
-
0.1.x used bare `wordfilters` and `fieldNames` option names, which made the package rather than the contract the thing clients keyed off. Rename both, in `options` and in `publicOptions`:
|
|
328
|
+
0.2.0 already uses the `wordfilter/v1` option keys; only the paths inside `wordfilter/v1/fieldNames` change for it, see below. 0.1.x used bare `wordfilters` and `fieldNames` option names, which made the package rather than the contract the thing clients keyed off. Rename both, in `options` and in `publicOptions`:
|
|
254
329
|
|
|
255
330
|
| 0.1.x | Now |
|
|
256
331
|
|---|---|
|
|
@@ -258,6 +333,16 @@ The `dst` containing `src` rule is what guarantees the client's loop terminates.
|
|
|
258
333
|
| `fieldNames` | `wordfilter/v1/fieldNames` |
|
|
259
334
|
| `error` | `error`, unchanged |
|
|
260
335
|
|
|
336
|
+
The paths inside `fieldNames` change too, for 0.2.0 as much as 0.1.x. Both resolved them against the publication, so `content` meant "content on whatever was published". `wordfilter/v1/fieldNames` now resolves them against the challenge request's publication map, so every path starts with the publication type, and a bare `content` is rejected rather than silently matching nothing:
|
|
337
|
+
|
|
338
|
+
| 0.1.x / 0.2.0 path | Now |
|
|
339
|
+
|---|---|
|
|
340
|
+
| `content` | `comment.content`, and `commentEdit.content` if edits should be filtered too |
|
|
341
|
+
| `title` | `comment.title` |
|
|
342
|
+
| `author.displayName` | one entry per publication type: `comment.author.displayName`, `commentEdit.author.displayName`, `vote.author.displayName` |
|
|
343
|
+
|
|
344
|
+
If 0.1.x or 0.2.0 ran with the default field list, dropping `fieldNames` entirely and taking the new defaults is the closest equivalent. A 0.2.0 community that set `wordfilter/v1/fieldNames` with bare paths fails loudly at the next start, `validateChallengeSettings` naming the offending path, until the paths are prefixed.
|
|
345
|
+
|
|
261
346
|
There is no fallback to the old names, on purpose. A wordfilter that quietly stops filtering is the exact failure this package exists to prevent, so an unmigrated community fails loudly instead: pkc-js rejects the now-undeclared option at community start with `ERR_CHALLENGE_OPTION_NOT_DECLARED_IN_OPTION_INPUTS`, naming the offending option, and `validateChallengeSettings` rejects the edit if you try to save it.
|
|
262
347
|
|
|
263
348
|
Clients written against 0.1.x need the same rename in whatever they copied from this README.
|
|
@@ -17,7 +17,23 @@
|
|
|
17
17
|
export const WORDFILTER_V1_RULES_OPTION = "wordfilter/v1/rules";
|
|
18
18
|
/** The option key carrying the fields to filter. Optional, and defaults to {@link DEFAULT_FIELD_NAMES}. */
|
|
19
19
|
export const WORDFILTER_V1_FIELD_NAMES_OPTION = "wordfilter/v1/fieldNames";
|
|
20
|
-
|
|
20
|
+
// The free-text fields an ordinary author publishes. Paths are rooted at the challenge request's
|
|
21
|
+
// publication map, so the first segment is the publication type (comment, vote, commentEdit,
|
|
22
|
+
// commentModeration, communityEdit) and the rest is the path inside that publication as it is on the wire.
|
|
23
|
+
// Naming the type keeps `content` on a comment and `content` on a comment edit separately addressable, in
|
|
24
|
+
// case they ever diverge. Moderator and owner text (commentModeration.commentModeration.reason,
|
|
25
|
+
// communityEdit.communityEdit.title, ...) is opt-in through wordfilter/v1/fieldNames, not default. This
|
|
26
|
+
// list is part of the wordfilter/v1 contract: a client filtering fewer fields than a community checks gets
|
|
27
|
+
// rejected, so the README client snippet carries the same list.
|
|
28
|
+
export const DEFAULT_FIELD_NAMES = [
|
|
29
|
+
"comment.content",
|
|
30
|
+
"comment.title",
|
|
31
|
+
"comment.author.displayName",
|
|
32
|
+
"commentEdit.content",
|
|
33
|
+
"commentEdit.reason",
|
|
34
|
+
"commentEdit.author.displayName",
|
|
35
|
+
"vote.author.displayName"
|
|
36
|
+
];
|
|
21
37
|
export const DEFAULT_MAX_PASSES = 8;
|
|
22
38
|
export const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
39
|
const applyOnce = (text, rules) => rules.reduce((acc, { src, dst }) => acc.replace(new RegExp(escapeRegExp(src), "gi"), () => dst), text);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply-wordfilters.js","sourceRoot":"","sources":["../src/apply-wordfilters.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,wGAAwG;AACxG,4FAA4F;AAC5F,EAAE;AACF,qFAAqF;AACrF,wGAAwG;AACxG,oGAAoG;AACpG,sGAAsG;AACtG,wDAAwD;AAExD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,qBAAqB,CAAC;AAEhE,2GAA2G;AAC3G,MAAM,CAAC,MAAM,gCAAgC,GAAG,0BAA0B,CAAC;AAS3E,MAAM,CAAC,MAAM,mBAAmB,GAAsB,
|
|
1
|
+
{"version":3,"file":"apply-wordfilters.js","sourceRoot":"","sources":["../src/apply-wordfilters.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,wGAAwG;AACxG,4FAA4F;AAC5F,EAAE;AACF,qFAAqF;AACrF,wGAAwG;AACxG,oGAAoG;AACpG,sGAAsG;AACtG,wDAAwD;AAExD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,qBAAqB,CAAC;AAEhE,2GAA2G;AAC3G,MAAM,CAAC,MAAM,gCAAgC,GAAG,0BAA0B,CAAC;AAS3E,iGAAiG;AACjG,6FAA6F;AAC7F,2GAA2G;AAC3G,0GAA0G;AAC1G,gGAAgG;AAChG,wGAAwG;AACxG,2GAA2G;AAC3G,gEAAgE;AAChE,MAAM,CAAC,MAAM,mBAAmB,GAAsB;IACpD,iBAAiB;IACjB,eAAe;IACf,4BAA4B;IAC5B,qBAAqB;IACrB,oBAAoB;IACpB,gCAAgC;IAChC,yBAAyB;CAC1B,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAEhG,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,KAAgC,EAAU,EAAE,CAC3E,KAAK,CAAC,MAAM,CACV,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAClF,IAAI,CACL,CAAC;AAEJ;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,KAAgC,EAChC,YAAoB,kBAAkB;IAEtC,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACnC,IAAI,IAAI,KAAK,GAAG;YAAE,OAAO,GAAG,CAAC;QAC7B,GAAG,GAAG,IAAI,CAAC;IACb,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -17,8 +17,8 @@ const optionInputs = [
|
|
|
17
17
|
option: WORDFILTER_V1_FIELD_NAMES_OPTION,
|
|
18
18
|
label: "Field Names",
|
|
19
19
|
default: JSON.stringify(DEFAULT_FIELD_NAMES),
|
|
20
|
-
description: "JSON array of dot-notation paths
|
|
21
|
-
placeholder:
|
|
20
|
+
description: "JSON array of dot-notation paths to check, each starting with the publication type: comment.content, commentEdit.reason, commentModeration.commentModeration.reason and so on. Must be listed in publicOptions when set. Defaults to the text of comments and comment edits plus author.displayName on comments, comment edits and votes.",
|
|
21
|
+
placeholder: JSON.stringify(DEFAULT_FIELD_NAMES)
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
// Not namespaced, unlike the two above: the wordfilter/v1 namespace covers exactly what a publishing
|
|
@@ -38,17 +38,20 @@ const publicationFieldNames = [
|
|
|
38
38
|
"commentModeration",
|
|
39
39
|
"communityEdit"
|
|
40
40
|
];
|
|
41
|
-
|
|
41
|
+
// The object field paths are resolved against: the challenge request's publication map, keyed by type. A
|
|
42
|
+
// request carries exactly one of these, so `comment.content` is absent, and passes, on a comment edit.
|
|
43
|
+
const derivePublicationsFromChallengeRequest = (challengeRequestMessage) => {
|
|
44
|
+
const publications = {};
|
|
42
45
|
for (const fieldName of publicationFieldNames) {
|
|
43
46
|
const publication = challengeRequestMessage[fieldName];
|
|
44
47
|
if (publication) {
|
|
45
|
-
|
|
48
|
+
publications[fieldName] = publication;
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
|
-
return undefined;
|
|
51
|
+
return Object.keys(publications).length ? publications : undefined;
|
|
49
52
|
};
|
|
50
|
-
// Dot-notation lookup, the same convention publication-match uses for `propertyName`,
|
|
51
|
-
//
|
|
53
|
+
// Dot-notation lookup, the same convention publication-match uses for `propertyName`, except that the
|
|
54
|
+
// first segment names the publication type.
|
|
52
55
|
const getValueAtPath = (publication, path) => {
|
|
53
56
|
let current = publication;
|
|
54
57
|
for (const segment of path.split(".")) {
|
|
@@ -105,12 +108,19 @@ const parseFieldNames = (fieldNames) => {
|
|
|
105
108
|
if (typeof fieldName !== "string" || !fieldName) {
|
|
106
109
|
throw new Error(`${WORDFILTER_V1_FIELD_NAMES_OPTION}[${index}] must be a non-empty string`);
|
|
107
110
|
}
|
|
111
|
+
// A path that names no publication type resolves to nothing on every request, which is a wordfilter
|
|
112
|
+
// that has quietly stopped filtering. That is the failure this package exists to prevent, so it is
|
|
113
|
+
// rejected here and, for a config that predates this check, by getChallenge.
|
|
114
|
+
const publicationType = fieldName.split(".")[0] ?? "";
|
|
115
|
+
if (!publicationFieldNames.includes(publicationType)) {
|
|
116
|
+
throw new Error(`${WORDFILTER_V1_FIELD_NAMES_OPTION}[${index}] must start with a publication type (${publicationFieldNames.join(", ")}), got "${fieldName}"`);
|
|
117
|
+
}
|
|
108
118
|
return fieldName;
|
|
109
119
|
});
|
|
110
120
|
};
|
|
111
121
|
const getChallenge = async ({ challengeSettings, challengeRequestMessage }) => {
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
122
|
+
const publications = derivePublicationsFromChallengeRequest(challengeRequestMessage);
|
|
123
|
+
if (!publications) {
|
|
114
124
|
return { success: false, error: "Could not derive publication from challenge request." };
|
|
115
125
|
}
|
|
116
126
|
// validateChallengeSettings rejects an unparseable config at edit time, so reaching this catch means a
|
|
@@ -130,9 +140,10 @@ const getChallenge = async ({ challengeSettings, challengeRequestMessage }) => {
|
|
|
130
140
|
}
|
|
131
141
|
const error = challengeSettings?.options?.["error"] || DEFAULT_ERROR;
|
|
132
142
|
for (const fieldName of fieldNames) {
|
|
133
|
-
const value = getValueAtPath(
|
|
134
|
-
// Absent fields pass cleanly. A vote has no content, and
|
|
135
|
-
//
|
|
143
|
+
const value = getValueAtPath(publications, fieldName);
|
|
144
|
+
// Absent fields pass cleanly. A vote has no content, and every path that names another publication
|
|
145
|
+
// type is absent on this request. Neither is a failure. publication-match treats a missing property as
|
|
146
|
+
// a failure, which would reject every vote. Do not copy that.
|
|
136
147
|
if (typeof value !== "string") {
|
|
137
148
|
continue;
|
|
138
149
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wordfilter-challenge.js","sourceRoot":"","sources":["../src/wordfilter-challenge.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,mBAAmB,EACnB,gCAAgC,EAChC,0BAA0B,EAE3B,MAAM,wBAAwB,CAAC;AAEhC,MAAM,SAAS,GAAG,EAAE,CAAC;AACrB,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,MAAM,aAAa,GACjB,qFAAqF,CAAC;AAExF,MAAM,WAAW,GACf,0KAA0K,CAAC;AAE7K,MAAM,IAAI,GAA2B,YAAY,CAAC;AAElD,MAAM,YAAY,GAAoD;IACpE;QACE,MAAM,EAAE,0BAA0B;QAClC,KAAK,EAAE,aAAa;QACpB,OAAO,EAAE,IAAI;QACb,WAAW,EACT,mUAAmU;QACrU,WAAW,EAAE,4DAA4D;QACzE,QAAQ,EAAE,IAAI;KACf;IACD;QACE,MAAM,EAAE,gCAAgC;QACxC,KAAK,EAAE,aAAa;QACpB,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC;QAC5C,WAAW,EACT,
|
|
1
|
+
{"version":3,"file":"wordfilter-challenge.js","sourceRoot":"","sources":["../src/wordfilter-challenge.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitsocial/wordfilter-challenge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Standalone wordfilter challenge for pkc-js: proof that a community's word replacements were applied before signing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|