@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 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 challenge through `community.challenges[i].publicOptions` and never install this
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 challenge.
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
- The rules are published in the community record, in the challenge's `publicOptions`. Collect them from every wordfilter challenge the community has configured, in challenge order.
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?.wordfilters)
56
+ .filter((challenge) => challenge.publicOptions?.["wordfilter/v1/rules"])
39
57
  .map((challenge) => {
40
58
  try {
41
- return {
42
- rules: JSON.parse(challenge.publicOptions.wordfilters),
43
- fieldNames: challenge.publicOptions.fieldNames
44
- ? JSON.parse(challenge.publicOptions.fieldNames)
45
- : ["content", "title", "author.displayName"]
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
- Skip anything you cannot parse. Never throw, and never refuse to load the community: a malformed rule set must cost the filter, not the whole board.
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
- for (const { rules, fieldNames } of configs) {
95
- if (fieldNames.includes("content") && content) content = applyWordfilters(content, rules);
96
- if (fieldNames.includes("title") && title) title = applyWordfilters(title, rules);
97
- }
98
- const comment = await pkc.createComment({ content, title, communityAddress, signer });
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
- `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.
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.wordfilters' '[{"src":"cloud","dst":"butt"}]' \
132
- '--settings.challenges[0].publicOptions[0]' wordfilters
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
- wordfilters: JSON.stringify([
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(["content", "title", "author.displayName"]),
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: ["wordfilters", "fieldNames", "error"]
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 `wordfilters` is missing from `publicOptions`, so the failure surfaces when you save rather than when every author starts getting rejected.
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
- | `wordfilters` | yes | yes | JSON array of `{ src, dst }`, applied in array order, cascading |
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 `wordfilters` always, and `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. `error` is returned in the rejection itself, so publishing it is transparency rather than a requirement.
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
- - `wordfilters` missing from `publicOptions`, or unparseable JSON, or not an array of `{ src, dst }` strings
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 community configured through pkc-js 0.0.85+ cannot produce
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 challenge. A community can only accept or reject, so the replacement has to
2
- // happen on the side holding the signing key. This file is the loop that side has to run, kept free of
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 challenge through
6
- // `community.challenges[i].publicOptions` and have no reason to know which package produced it. The
7
- // README carries this same loop as a copy-pasteable snippet for them, and that snippet is the contract.
8
- // This export exists for the minority that does bundle the package, and must stay behaviourally
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 community configured through pkc-js 0.0.85+ cannot produce
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,qGAAqG;AACrG,uGAAuG;AACvG,0EAA0E;AAC1E,EAAE;AACF,sFAAsF;AACtF,oGAAoG;AACpG,wGAAwG;AACxG,gGAAgG;AAChG,qCAAqC;AASrC,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;;;;;;;;;;;GAWG;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
+ {"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: "wordfilters",
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: "fieldNames",
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 = (wordfilters) => {
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 (wordfilters === undefined)
65
+ if (rules === undefined)
63
66
  return [];
64
67
  let parsed;
65
68
  try {
66
- parsed = JSON.parse(wordfilters);
69
+ parsed = JSON.parse(rules);
67
70
  }
68
71
  catch (e) {
69
- throw new Error(`wordfilters is not valid JSON: ${e.message}`);
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("wordfilters must be a JSON array of {src, dst} objects");
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(`wordfilters[${index}] must be an object with src and dst`);
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(`wordfilters[${index}].src must be a string`);
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(`wordfilters[${index}].dst must be a string`);
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(`fieldNames is not valid JSON: ${e.message}`);
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("fieldNames must be a JSON array of dot-notation property paths");
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(`fieldNames[${index}] must be a non-empty string`);
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?.["wordfilters"]);
120
- fieldNames = parseFieldNames(challengeSettings?.options?.["fieldNames"]);
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("wordfilters")) {
151
- throw new Error("wordfilters must be listed in publicOptions: publishing clients apply the replacements before signing, and a client that cannot read the rules cannot satisfy them");
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?.["wordfilters"]);
156
+ const rules = parseRules(challengeSettings.options?.[WORDFILTER_V1_RULES_OPTION]);
154
157
  if (rules.length > MAX_RULES) {
155
- throw new Error(`wordfilters has ${rules.length} rules, the maximum is ${MAX_RULES}`);
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(`wordfilters[${index}].src must not be empty`);
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(`wordfilters[${index}].src is ${src.length} characters, the maximum is ${MAX_RULE_STRING_LENGTH}`);
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(`wordfilters[${index}].dst is ${dst.length} characters, the maximum is ${MAX_RULE_STRING_LENGTH}`);
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(`wordfilters[${index}].src and dst are both '${src}', which replaces nothing`);
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(`wordfilters[${index}].src ('${src}') is already the src of wordfilters[${firstIndex}]: the second rule can never match`);
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(`wordfilters[${index}].dst ('${dst}') contains wordfilters[${otherIndex}].src ('${src}'): the replacement would itself be filtered, so no text could ever pass`);
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 wordfilters: a client that checks the default fields while the community checks a
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?.["fieldNames"] !== undefined) {
193
- parseFieldNames(challengeSettings.options["fieldNames"]);
194
- if (!challengeSettings.publicOptions?.includes("fieldNames")) {
195
- throw new Error("fieldNames must be listed in publicOptions when it is set: a client filtering the default fields cannot satisfy a community checking different ones");
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":"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"}
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.1.2",
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",