@bitsocial/wordfilter-challenge 0.1.2 → 0.2.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/AGENTS.md +9 -1
- package/README.md +84 -31
- package/dist/apply-wordfilters.d.ts +17 -2
- package/dist/apply-wordfilters.js +25 -10
- package/dist/apply-wordfilters.js.map +1 -1
- package/dist/wordfilter-challenge.js +35 -32
- package/dist/wordfilter-challenge.js.map +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -17,8 +17,16 @@ Conventions specific to this challenge:
|
|
|
17
17
|
- `README.md` is the spec. Behaviour changes belong there first.
|
|
18
18
|
- `src/apply-wordfilters.ts` imports nothing. It runs in a browser, in a publishing client, and its
|
|
19
19
|
behaviour must stay identical to the snippet in the README, which is what most frontends will actually
|
|
20
|
-
copy. They discover the
|
|
20
|
+
copy. They discover the contract through `community.challenges[i].publicOptions` and never install this
|
|
21
21
|
package.
|
|
22
|
+
- `WORDFILTER_V1_RULES_OPTION` and `WORDFILTER_V1_FIELD_NAMES_OPTION` are the wire contract, not internal
|
|
23
|
+
names. Clients recognise `wordfilter/v1` by the presence of the rules key in a community's
|
|
24
|
+
`publicOptions`, and most of them never install this package, so renaming either string silently stops
|
|
25
|
+
every such client from filtering. A rename is a new contract version, never an edit to v1. The literals
|
|
26
|
+
are pinned in `test/wordfilter-challenge.test.ts` so this fails a test rather than a live board.
|
|
27
|
+
- The `wordfilter/v1` namespace covers exactly the options a publishing client must read. Anything a
|
|
28
|
+
client never reads, `error` today, keeps its plain name and stays free to differ between
|
|
29
|
+
implementations. This package is one implementation of the contract, not its owner.
|
|
22
30
|
- `validateChallengeSettings` is sync and does no network. It runs on every community start, so an async
|
|
23
31
|
validator would turn a third party's outage into a startup problem.
|
|
24
32
|
- `ChallengeFileFactory` never throws. It runs on load as well as on edit, so a throw fails community
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bitsocial/wordfilter-challenge
|
|
2
2
|
|
|
3
|
-
A [pkc-js](https://github.com/pkcprotocol/pkc-js) challenge that makes community wordfilters a real rule instead of a cosmetic display filter.
|
|
3
|
+
A [pkc-js](https://github.com/pkcprotocol/pkc-js) challenge that makes community wordfilters a real rule instead of a cosmetic display filter. It implements the [`wordfilter/v1` contract](#the-wordfilterv1-contract), which is what publishing clients actually code against.
|
|
4
4
|
|
|
5
5
|
> **Status: implemented.** This README is the spec, and `src/` follows it. Tracked in [pkc-js#281](https://github.com/pkcprotocol/pkc-js/issues/281).
|
|
6
6
|
|
|
@@ -22,28 +22,49 @@ So the replacement has to happen on the side holding the signing key, which is t
|
|
|
22
22
|
|
|
23
23
|
A client that does not implement the replacement cannot publish unfiltered text. It simply gets rejected, which is the intended failure mode.
|
|
24
24
|
|
|
25
|
+
## The `wordfilter/v1` contract
|
|
26
|
+
|
|
27
|
+
A publishing client has to recognise "this community wants word replacements applied before I sign" from the published community record alone. The record deliberately does not say which challenge produced it: `path`, `name` and `options` are all stripped when `community.settings.challenges[i]` becomes the public `community.challenges[i]`. Only `publicOptions`, the subset the owner opted into publishing, survives.
|
|
28
|
+
|
|
29
|
+
So the signal a client keys off is an option key that names a **contract** rather than a package:
|
|
30
|
+
|
|
31
|
+
| Public option | Required | Meaning |
|
|
32
|
+
|---|---|---|
|
|
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 `content`, `title`, `author.displayName` |
|
|
35
|
+
|
|
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
|
+
|
|
38
|
+
The namespace covers exactly what a publishing client must read, and nothing else. `error` is not in it, because no client reads `error`: the community returns it in the rejection. An implementation is free to name or shape that option however it likes.
|
|
39
|
+
|
|
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
|
+
|
|
25
42
|
## For UI client developers
|
|
26
43
|
|
|
27
|
-
This is the section that matters if you are building a frontend that publishes to communities using this
|
|
44
|
+
This is the section that matters if you are building a frontend that publishes to communities using this contract.
|
|
28
45
|
|
|
29
46
|
### 1. Read the rules off the community
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
The challenge's `name` is **not** in the published record: `path`, `name` and `options` are all stripped when `community.settings.challenges[i]` becomes `community.challenges[i]`. The presence of a `wordfilters` public option is the only thing a client can key off, which is exactly why the challenge requires it to be published.
|
|
48
|
+
Collect the rules from every challenge on the community that publishes `wordfilter/v1/rules`, in challenge order. A community may have more than one.
|
|
34
49
|
|
|
35
50
|
```js
|
|
51
|
+
const isRule = (rule) => typeof rule?.src === "string" && typeof rule?.dst === "string";
|
|
52
|
+
const isFieldName = (name) => typeof name === "string";
|
|
53
|
+
|
|
36
54
|
function getWordfilterConfigs(community) {
|
|
37
55
|
return (community.challenges ?? [])
|
|
38
|
-
.filter((challenge) => challenge.publicOptions?.
|
|
56
|
+
.filter((challenge) => challenge.publicOptions?.["wordfilter/v1/rules"])
|
|
39
57
|
.map((challenge) => {
|
|
40
58
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
const rules = JSON.parse(challenge.publicOptions["wordfilter/v1/rules"]);
|
|
60
|
+
const fieldNames = challenge.publicOptions["wordfilter/v1/fieldNames"]
|
|
61
|
+
? JSON.parse(challenge.publicOptions["wordfilter/v1/fieldNames"])
|
|
62
|
+
: ["content", "title", "author.displayName"];
|
|
63
|
+
// JSON.parse is happy to hand back null, an object or an array of numbers, and
|
|
64
|
+
// applyWordfilters would throw on any of them. Treat the wrong shape like bad JSON.
|
|
65
|
+
if (!Array.isArray(rules) || !rules.every(isRule)) return undefined;
|
|
66
|
+
if (!Array.isArray(fieldNames) || !fieldNames.every(isFieldName)) return undefined;
|
|
67
|
+
return { rules, fieldNames };
|
|
47
68
|
} catch {
|
|
48
69
|
// A rule set you cannot parse is skipped, never fatal. Worst case the community
|
|
49
70
|
// rejects the publication with a readable error.
|
|
@@ -54,7 +75,9 @@ function getWordfilterConfigs(community) {
|
|
|
54
75
|
}
|
|
55
76
|
```
|
|
56
77
|
|
|
57
|
-
|
|
78
|
+
Note what this function does not do: it never looks at which challenge implementation produced the options, because the record does not say and it does not need to.
|
|
79
|
+
|
|
80
|
+
Skip anything you cannot parse, and anything that parses to the wrong shape. Never throw, and never refuse to load the community: a malformed rule set must cost the filter, not the whole board. This package's own `validateChallengeSettings` will not let a community publish a malformed rule set, but the contract is open to other implementations and your client cannot know which one it is talking to.
|
|
58
81
|
|
|
59
82
|
### 2. Apply the rules, looping until the text stops changing
|
|
60
83
|
|
|
@@ -91,15 +114,27 @@ Filter the text before it reaches `pkc.createComment()`, so the transformation h
|
|
|
91
114
|
|
|
92
115
|
```js
|
|
93
116
|
const configs = getWordfilterConfigs(community);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
117
|
+
|
|
118
|
+
// One call per field, carrying the rules of every challenge that covers that field. Do not apply the
|
|
119
|
+
// challenges one at a time: a later challenge's replacement can reintroduce an earlier challenge's
|
|
120
|
+
// src, and the earlier challenge would then reject the publication. The loop inside applyWordfilters
|
|
121
|
+
// only sees that interaction when both rule sets are in the same call.
|
|
122
|
+
const rulesFor = (fieldName) =>
|
|
123
|
+
configs.filter(({ fieldNames }) => fieldNames.includes(fieldName)).flatMap(({ rules }) => rules);
|
|
124
|
+
|
|
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"));
|
|
128
|
+
|
|
129
|
+
const comment = await pkc.createComment({ content, title, author: { displayName }, communityAddress, signer });
|
|
99
130
|
await comment.publish();
|
|
100
131
|
```
|
|
101
132
|
|
|
102
|
-
|
|
133
|
+
Those three fields 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.
|
|
134
|
+
|
|
135
|
+
The `wordfilter/v1/fieldNames` entries are dot-notation paths on the publication, the same convention `publication-match` uses for `propertyName`. So `content` covers both a comment and a comment edit without the config naming publication types.
|
|
136
|
+
|
|
137
|
+
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.
|
|
103
138
|
|
|
104
139
|
### 4. Show the user what happened
|
|
105
140
|
|
|
@@ -128,8 +163,8 @@ bitsocial challenge install @bitsocial/wordfilter-challenge
|
|
|
128
163
|
```bash
|
|
129
164
|
bitsocial community edit your-community.bso \
|
|
130
165
|
'--settings.challenges[0].name' @bitsocial/wordfilter-challenge \
|
|
131
|
-
'--settings.challenges[0].options.
|
|
132
|
-
'--settings.challenges[0].publicOptions[0]'
|
|
166
|
+
'--settings.challenges[0].options.wordfilter/v1/rules' '[{"src":"cloud","dst":"butt"}]' \
|
|
167
|
+
'--settings.challenges[0].publicOptions[0]' wordfilter/v1/rules
|
|
133
168
|
```
|
|
134
169
|
|
|
135
170
|
### With pkc-js over RPC
|
|
@@ -166,49 +201,67 @@ await community.edit({
|
|
|
166
201
|
{
|
|
167
202
|
name: "@bitsocial/wordfilter-challenge",
|
|
168
203
|
options: {
|
|
169
|
-
|
|
204
|
+
"wordfilter/v1/rules": JSON.stringify([
|
|
170
205
|
{ src: "cloud", dst: "butt" },
|
|
171
206
|
{ src: "millennials", dst: "snake people" },
|
|
172
207
|
{ src: "spamword", dst: "" }
|
|
173
208
|
]),
|
|
174
|
-
fieldNames: JSON.stringify([
|
|
209
|
+
"wordfilter/v1/fieldNames": JSON.stringify([
|
|
210
|
+
"content",
|
|
211
|
+
"title",
|
|
212
|
+
"author.displayName"
|
|
213
|
+
]),
|
|
175
214
|
error: "This board replaces certain words. Please repost with the replacements applied."
|
|
176
215
|
},
|
|
177
|
-
publicOptions: ["
|
|
216
|
+
publicOptions: ["wordfilter/v1/rules", "wordfilter/v1/fieldNames", "error"]
|
|
178
217
|
}
|
|
179
218
|
]
|
|
180
219
|
}
|
|
181
220
|
});
|
|
182
221
|
```
|
|
183
222
|
|
|
184
|
-
`publicOptions` is **required**, not decorative. Options are private by default in pkc-js, and a client that cannot read the rules cannot satisfy them. The challenge's `validateChallengeSettings` hook rejects the edit if `
|
|
223
|
+
`publicOptions` is **required**, not decorative. Options are private by default in pkc-js, and a client that cannot read the rules cannot satisfy them. The challenge's `validateChallengeSettings` hook rejects the edit if `wordfilter/v1/rules` is missing from `publicOptions`, so the failure surfaces when you save rather than when every author starts getting rejected.
|
|
185
224
|
|
|
186
225
|
### Options
|
|
187
226
|
|
|
188
227
|
| Option | Required | Must be in `publicOptions` | Description |
|
|
189
228
|
|---|---|---|---|
|
|
190
|
-
| `
|
|
191
|
-
| `fieldNames` | no | when set | JSON array of dot-notation paths. Defaults to `content`, `title`, `author.displayName` |
|
|
229
|
+
| `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 `content`, `title`, `author.displayName` |
|
|
192
231
|
| `error` | no | owner's call | Message shown to the author when a publication is rejected |
|
|
193
232
|
|
|
194
|
-
An option has to be public when a client cannot satisfy the challenge without reading it. That covers `
|
|
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 three 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.
|
|
195
234
|
|
|
196
235
|
### Validation
|
|
197
236
|
|
|
198
237
|
`validateChallengeSettings` rejects at edit time:
|
|
199
238
|
|
|
200
|
-
- `
|
|
239
|
+
- `wordfilter/v1/rules` missing from `publicOptions`, or unparseable JSON, or not an array of `{ src, dst }` strings
|
|
201
240
|
- an empty `src`
|
|
202
241
|
- `src === dst`
|
|
203
242
|
- the same `src` in more than one rule
|
|
204
243
|
- any `dst` containing any `src`
|
|
205
244
|
- more than 64 rules, or a `src` or `dst` longer than 128 characters
|
|
206
|
-
- `fieldNames`, when set, unparseable, not an array of non-empty strings, or missing from `publicOptions`
|
|
245
|
+
- `wordfilter/v1/fieldNames`, when set, unparseable, not an array of non-empty strings, or missing from `publicOptions`
|
|
207
246
|
|
|
208
247
|
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.
|
|
209
248
|
|
|
210
249
|
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.
|
|
211
250
|
|
|
251
|
+
## Migrating from 0.1.x
|
|
252
|
+
|
|
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`:
|
|
254
|
+
|
|
255
|
+
| 0.1.x | Now |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `wordfilters` | `wordfilter/v1/rules` |
|
|
258
|
+
| `fieldNames` | `wordfilter/v1/fieldNames` |
|
|
259
|
+
| `error` | `error`, unchanged |
|
|
260
|
+
|
|
261
|
+
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
|
+
|
|
263
|
+
Clients written against 0.1.x need the same rename in whatever they copied from this README.
|
|
264
|
+
|
|
212
265
|
## Matching semantics
|
|
213
266
|
|
|
214
267
|
- **Literal, never regex.** vichan supports arbitrary PCRE, but only because its config is a server-local file the operator wrote. Here the rules ship inside a signed record that every browser client downloads and executes, so patterns are literal strings.
|
|
@@ -243,7 +296,7 @@ npm run build
|
|
|
243
296
|
| Path | What it is |
|
|
244
297
|
|---|---|
|
|
245
298
|
| `src/wordfilter-challenge.ts` | The `ChallengeFileFactory`: `optionInputs`, `getChallenge`, `validateChallengeSettings` |
|
|
246
|
-
| `src/apply-wordfilters.ts` | The client-side replacement loop. Imports nothing, so it bundles for a browser |
|
|
299
|
+
| `src/apply-wordfilters.ts` | The client-side replacement loop and the `wordfilter/v1` option keys. Imports nothing, so it bundles for a browser |
|
|
247
300
|
| `src/types.ts` | Re-exports of the pkc-js challenge types |
|
|
248
301
|
|
|
249
302
|
## License
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The option key carrying the rules, and the only thing a client keys off to recognise the contract.
|
|
3
|
+
*
|
|
4
|
+
* Namespaced by contract rather than by package on purpose. A client that matched on the package name,
|
|
5
|
+
* or on a generic `wordfilters` key, would lock every other implementation of the same behaviour out of
|
|
6
|
+
* every UI. Anything that publishes this key claims to implement `wordfilter/v1`, whoever wrote it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const WORDFILTER_V1_RULES_OPTION = "wordfilter/v1/rules";
|
|
9
|
+
/** The option key carrying the fields to filter. Optional, and defaults to {@link DEFAULT_FIELD_NAMES}. */
|
|
10
|
+
export declare const WORDFILTER_V1_FIELD_NAMES_OPTION = "wordfilter/v1/fieldNames";
|
|
1
11
|
export interface WordfilterRule {
|
|
2
12
|
/** The literal string to look for. Never a regex, and matched case-insensitively. */
|
|
3
13
|
src: string;
|
|
@@ -16,7 +26,12 @@ export declare const escapeRegExp: (str: string) => string;
|
|
|
16
26
|
* the community checks for.
|
|
17
27
|
*
|
|
18
28
|
* @throws if the rules never stabilise. `validateChallengeSettings` rejects the rule sets that can cause
|
|
19
|
-
* this (no `dst` may contain any `src`), so a
|
|
20
|
-
* one, but a client should still not spin forever on a hostile record.
|
|
29
|
+
* this within one challenge (no `dst` may contain any `src`), so a single challenge configured through
|
|
30
|
+
* pkc-js 0.0.85+ cannot produce one, but a client should still not spin forever on a hostile record.
|
|
31
|
+
*
|
|
32
|
+
* Two challenges on the same community can undo each other, which no per-challenge validator can see.
|
|
33
|
+
* The README's client snippet merges the rules of every challenge covering a field into one call so the
|
|
34
|
+
* loop sees the interaction; the result is then either this throw or a fixed point one challenge still
|
|
35
|
+
* rejects, and either way the board is misconfigured.
|
|
21
36
|
*/
|
|
22
37
|
export declare function applyWordfilters(text: string, rules: readonly WordfilterRule[], maxPasses?: number): string;
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
// The client side of the
|
|
2
|
-
// happen on the side holding the signing key. This file is the loop that side has to
|
|
3
|
-
// any pkc-js import so it can be pulled into a browser bundle on its own.
|
|
1
|
+
// The client side of the `wordfilter/v1` contract. A community can only accept or reject, so the
|
|
2
|
+
// replacement has to happen on the side holding the signing key. This file is the loop that side has to
|
|
3
|
+
// run, kept free of any pkc-js import so it can be pulled into a browser bundle on its own.
|
|
4
4
|
//
|
|
5
|
-
// Most frontends will never install this package: they discover the
|
|
6
|
-
// `community.challenges[i].publicOptions` and have no reason to know which package produced it
|
|
7
|
-
// README carries this same loop as a copy-pasteable snippet for them, and that
|
|
8
|
-
// This export exists for the minority that does bundle the package, and must
|
|
9
|
-
// identical to the README's version.
|
|
5
|
+
// Most frontends will never install this package: they discover the contract through
|
|
6
|
+
// `community.challenges[i].publicOptions` and have no reason to know which package produced it, or that
|
|
7
|
+
// this one exists. The README carries this same loop as a copy-pasteable snippet for them, and that
|
|
8
|
+
// snippet is the contract. This export exists for the minority that does bundle the package, and must
|
|
9
|
+
// stay behaviourally identical to the README's version.
|
|
10
|
+
/**
|
|
11
|
+
* The option key carrying the rules, and the only thing a client keys off to recognise the contract.
|
|
12
|
+
*
|
|
13
|
+
* Namespaced by contract rather than by package on purpose. A client that matched on the package name,
|
|
14
|
+
* or on a generic `wordfilters` key, would lock every other implementation of the same behaviour out of
|
|
15
|
+
* every UI. Anything that publishes this key claims to implement `wordfilter/v1`, whoever wrote it.
|
|
16
|
+
*/
|
|
17
|
+
export const WORDFILTER_V1_RULES_OPTION = "wordfilter/v1/rules";
|
|
18
|
+
/** The option key carrying the fields to filter. Optional, and defaults to {@link DEFAULT_FIELD_NAMES}. */
|
|
19
|
+
export const WORDFILTER_V1_FIELD_NAMES_OPTION = "wordfilter/v1/fieldNames";
|
|
10
20
|
export const DEFAULT_FIELD_NAMES = ["content", "title", "author.displayName"];
|
|
11
21
|
export const DEFAULT_MAX_PASSES = 8;
|
|
12
22
|
export const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -20,8 +30,13 @@ const applyOnce = (text, rules) => rules.reduce((acc, { src, dst }) => acc.repla
|
|
|
20
30
|
* the community checks for.
|
|
21
31
|
*
|
|
22
32
|
* @throws if the rules never stabilise. `validateChallengeSettings` rejects the rule sets that can cause
|
|
23
|
-
* this (no `dst` may contain any `src`), so a
|
|
24
|
-
* one, but a client should still not spin forever on a hostile record.
|
|
33
|
+
* this within one challenge (no `dst` may contain any `src`), so a single challenge configured through
|
|
34
|
+
* pkc-js 0.0.85+ cannot produce one, but a client should still not spin forever on a hostile record.
|
|
35
|
+
*
|
|
36
|
+
* Two challenges on the same community can undo each other, which no per-challenge validator can see.
|
|
37
|
+
* The README's client snippet merges the rules of every challenge covering a field into one call so the
|
|
38
|
+
* loop sees the interaction; the result is then either this throw or a fixed point one challenge still
|
|
39
|
+
* rejects, and either way the board is misconfigured.
|
|
25
40
|
*/
|
|
26
41
|
export function applyWordfilters(text, rules, maxPasses = DEFAULT_MAX_PASSES) {
|
|
27
42
|
let out = text;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply-wordfilters.js","sourceRoot":"","sources":["../src/apply-wordfilters.ts"],"names":[],"mappings":"AAAA,
|
|
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,CAAC,SAAS,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;AAEjG,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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_FIELD_NAMES } from "./apply-wordfilters.js";
|
|
1
|
+
import { DEFAULT_FIELD_NAMES, WORDFILTER_V1_FIELD_NAMES_OPTION, WORDFILTER_V1_RULES_OPTION } from "./apply-wordfilters.js";
|
|
2
2
|
const MAX_RULES = 64;
|
|
3
3
|
const MAX_RULE_STRING_LENGTH = 128;
|
|
4
4
|
const DEFAULT_ERROR = "This community replaces certain words. Please repost with the replacements applied.";
|
|
@@ -6,21 +6,24 @@ const description = "Publications must already have the community's word replace
|
|
|
6
6
|
const type = "text/plain";
|
|
7
7
|
const optionInputs = [
|
|
8
8
|
{
|
|
9
|
-
option:
|
|
9
|
+
option: WORDFILTER_V1_RULES_OPTION,
|
|
10
10
|
label: "Wordfilters",
|
|
11
11
|
default: "[]",
|
|
12
|
-
description: "JSON array of {src, dst} replacements, applied in array order, cascading. Matched literally and case-insensitively. Must be listed in publicOptions so publishing clients can read and apply it.",
|
|
12
|
+
description: "JSON array of {src, dst} replacements, applied in array order, cascading. Matched literally and case-insensitively. Must be listed in publicOptions so publishing clients can read and apply it. The key names the wordfilter/v1 contract rather than this package, so a client written against it works with any implementation.",
|
|
13
13
|
placeholder: '[{"src":"cloud","dst":"butt"},{"src":"spamword","dst":""}]',
|
|
14
14
|
required: true
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
option:
|
|
17
|
+
option: WORDFILTER_V1_FIELD_NAMES_OPTION,
|
|
18
18
|
label: "Field Names",
|
|
19
19
|
default: JSON.stringify(DEFAULT_FIELD_NAMES),
|
|
20
20
|
description: "JSON array of dot-notation paths on the publication to check, the same convention publication-match uses for propertyName. Must be listed in publicOptions when set. Defaults to content, title and author.displayName.",
|
|
21
21
|
placeholder: '["content","title","author.displayName"]'
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
+
// Not namespaced, unlike the two above: the wordfilter/v1 namespace covers exactly what a publishing
|
|
25
|
+
// client has to read, and no client ever reads this. It comes back in the rejection itself, so an
|
|
26
|
+
// implementation is free to name or shape it differently.
|
|
24
27
|
option: "error",
|
|
25
28
|
label: "Error",
|
|
26
29
|
default: DEFAULT_ERROR,
|
|
@@ -56,31 +59,31 @@ const getValueAtPath = (publication, path) => {
|
|
|
56
59
|
}
|
|
57
60
|
return current;
|
|
58
61
|
};
|
|
59
|
-
const parseRules = (
|
|
62
|
+
const parseRules = (rules) => {
|
|
60
63
|
// Only an absent option means "no rules". An empty string satisfies core's `required` check but is not
|
|
61
64
|
// valid JSON, so it falls through to the parse error rather than silently disabling the filter.
|
|
62
|
-
if (
|
|
65
|
+
if (rules === undefined)
|
|
63
66
|
return [];
|
|
64
67
|
let parsed;
|
|
65
68
|
try {
|
|
66
|
-
parsed = JSON.parse(
|
|
69
|
+
parsed = JSON.parse(rules);
|
|
67
70
|
}
|
|
68
71
|
catch (e) {
|
|
69
|
-
throw new Error(
|
|
72
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION} is not valid JSON: ${e.message}`);
|
|
70
73
|
}
|
|
71
74
|
if (!Array.isArray(parsed)) {
|
|
72
|
-
throw new Error(
|
|
75
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION} must be a JSON array of {src, dst} objects`);
|
|
73
76
|
}
|
|
74
77
|
return parsed.map((rule, index) => {
|
|
75
78
|
if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
|
|
76
|
-
throw new Error(
|
|
79
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}] must be an object with src and dst`);
|
|
77
80
|
}
|
|
78
81
|
const { src, dst } = rule;
|
|
79
82
|
if (typeof src !== "string") {
|
|
80
|
-
throw new Error(
|
|
83
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].src must be a string`);
|
|
81
84
|
}
|
|
82
85
|
if (typeof dst !== "string") {
|
|
83
|
-
throw new Error(
|
|
86
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].dst must be a string`);
|
|
84
87
|
}
|
|
85
88
|
return { src, dst };
|
|
86
89
|
});
|
|
@@ -93,14 +96,14 @@ const parseFieldNames = (fieldNames) => {
|
|
|
93
96
|
parsed = JSON.parse(fieldNames);
|
|
94
97
|
}
|
|
95
98
|
catch (e) {
|
|
96
|
-
throw new Error(
|
|
99
|
+
throw new Error(`${WORDFILTER_V1_FIELD_NAMES_OPTION} is not valid JSON: ${e.message}`);
|
|
97
100
|
}
|
|
98
101
|
if (!Array.isArray(parsed)) {
|
|
99
|
-
throw new Error(
|
|
102
|
+
throw new Error(`${WORDFILTER_V1_FIELD_NAMES_OPTION} must be a JSON array of dot-notation property paths`);
|
|
100
103
|
}
|
|
101
104
|
return parsed.map((fieldName, index) => {
|
|
102
105
|
if (typeof fieldName !== "string" || !fieldName) {
|
|
103
|
-
throw new Error(
|
|
106
|
+
throw new Error(`${WORDFILTER_V1_FIELD_NAMES_OPTION}[${index}] must be a non-empty string`);
|
|
104
107
|
}
|
|
105
108
|
return fieldName;
|
|
106
109
|
});
|
|
@@ -116,8 +119,8 @@ const getChallenge = async ({ challengeSettings, challengeRequestMessage }) => {
|
|
|
116
119
|
let rules;
|
|
117
120
|
let fieldNames;
|
|
118
121
|
try {
|
|
119
|
-
rules = parseRules(challengeSettings?.options?.[
|
|
120
|
-
fieldNames = parseFieldNames(challengeSettings?.options?.[
|
|
122
|
+
rules = parseRules(challengeSettings?.options?.[WORDFILTER_V1_RULES_OPTION]);
|
|
123
|
+
fieldNames = parseFieldNames(challengeSettings?.options?.[WORDFILTER_V1_FIELD_NAMES_OPTION]);
|
|
121
124
|
}
|
|
122
125
|
catch (e) {
|
|
123
126
|
return { success: false, error: `Invalid wordfilter challenge settings: ${e.message}` };
|
|
@@ -147,32 +150,32 @@ const getChallenge = async ({ challengeSettings, challengeRequestMessage }) => {
|
|
|
147
150
|
const validateChallengeSettings = ({ challengeSettings }) => {
|
|
148
151
|
// A client that cannot read the rules cannot satisfy them, so publication is required rather than the
|
|
149
152
|
// owner's call. See "Require publication when clients need to read it" in challenge-authoring.md.
|
|
150
|
-
if (!challengeSettings.publicOptions?.includes(
|
|
151
|
-
throw new Error(
|
|
153
|
+
if (!challengeSettings.publicOptions?.includes(WORDFILTER_V1_RULES_OPTION)) {
|
|
154
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION} must be listed in publicOptions: publishing clients apply the replacements before signing, and a client that cannot read the rules cannot satisfy them`);
|
|
152
155
|
}
|
|
153
|
-
const rules = parseRules(challengeSettings.options?.[
|
|
156
|
+
const rules = parseRules(challengeSettings.options?.[WORDFILTER_V1_RULES_OPTION]);
|
|
154
157
|
if (rules.length > MAX_RULES) {
|
|
155
|
-
throw new Error(
|
|
158
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION} has ${rules.length} rules, the maximum is ${MAX_RULES}`);
|
|
156
159
|
}
|
|
157
160
|
const seenSources = new Map();
|
|
158
161
|
for (const [index, { src, dst }] of rules.entries()) {
|
|
159
162
|
if (!src) {
|
|
160
|
-
throw new Error(
|
|
163
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].src must not be empty`);
|
|
161
164
|
}
|
|
162
165
|
if (src.length > MAX_RULE_STRING_LENGTH) {
|
|
163
|
-
throw new Error(
|
|
166
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].src is ${src.length} characters, the maximum is ${MAX_RULE_STRING_LENGTH}`);
|
|
164
167
|
}
|
|
165
168
|
if (dst.length > MAX_RULE_STRING_LENGTH) {
|
|
166
|
-
throw new Error(
|
|
169
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].dst is ${dst.length} characters, the maximum is ${MAX_RULE_STRING_LENGTH}`);
|
|
167
170
|
}
|
|
168
171
|
// Matching is case-insensitive, so "AB" and "ab" are the same rule and the same no-op.
|
|
169
172
|
const lowerCasedSrc = src.toLowerCase();
|
|
170
173
|
if (lowerCasedSrc === dst.toLowerCase()) {
|
|
171
|
-
throw new Error(
|
|
174
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].src and dst are both '${src}', which replaces nothing`);
|
|
172
175
|
}
|
|
173
176
|
const firstIndex = seenSources.get(lowerCasedSrc);
|
|
174
177
|
if (firstIndex !== undefined) {
|
|
175
|
-
throw new Error(
|
|
178
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].src ('${src}') is already the src of rule ${firstIndex}: the second rule can never match`);
|
|
176
179
|
}
|
|
177
180
|
seenSources.set(lowerCasedSrc, index);
|
|
178
181
|
}
|
|
@@ -183,16 +186,16 @@ const validateChallengeSettings = ({ challengeSettings }) => {
|
|
|
183
186
|
const lowerCasedDst = dst.toLowerCase();
|
|
184
187
|
for (const [otherIndex, { src }] of rules.entries()) {
|
|
185
188
|
if (lowerCasedDst.includes(src.toLowerCase())) {
|
|
186
|
-
throw new Error(
|
|
189
|
+
throw new Error(`${WORDFILTER_V1_RULES_OPTION}[${index}].dst ('${dst}') contains rule ${otherIndex}'s src ('${src}'): the replacement would itself be filtered, so no text could ever pass`);
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
|
-
// Same reasoning as
|
|
193
|
+
// Same reasoning as the rules: a client that checks the default fields while the community checks a
|
|
191
194
|
// different set rejects every author, with nothing in the record explaining why.
|
|
192
|
-
if (challengeSettings.options?.[
|
|
193
|
-
parseFieldNames(challengeSettings.options[
|
|
194
|
-
if (!challengeSettings.publicOptions?.includes(
|
|
195
|
-
throw new Error(
|
|
195
|
+
if (challengeSettings.options?.[WORDFILTER_V1_FIELD_NAMES_OPTION] !== undefined) {
|
|
196
|
+
parseFieldNames(challengeSettings.options[WORDFILTER_V1_FIELD_NAMES_OPTION]);
|
|
197
|
+
if (!challengeSettings.publicOptions?.includes(WORDFILTER_V1_FIELD_NAMES_OPTION)) {
|
|
198
|
+
throw new Error(`${WORDFILTER_V1_FIELD_NAMES_OPTION} must be listed in publicOptions when it is set: a client filtering the default fields cannot satisfy a community checking different ones`);
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wordfilter-challenge.js","sourceRoot":"","sources":["../src/wordfilter-challenge.ts"],"names":[],"mappings":"AAQA,OAAO,
|
|
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.2.0",
|
|
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",
|