@nexpress/core 0.1.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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/audit-54XLVCWD.js +14 -0
- package/dist/audit-54XLVCWD.js.map +1 -0
- package/dist/auth.d.ts +640 -0
- package/dist/auth.js +94 -0
- package/dist/auth.js.map +1 -0
- package/dist/can-YLUHRJAB.js +19 -0
- package/dist/can-YLUHRJAB.js.map +1 -0
- package/dist/chunk-2G264RCD.js +68 -0
- package/dist/chunk-2G264RCD.js.map +1 -0
- package/dist/chunk-2YDGE7YX.js +92 -0
- package/dist/chunk-2YDGE7YX.js.map +1 -0
- package/dist/chunk-473S4TER.js +538 -0
- package/dist/chunk-473S4TER.js.map +1 -0
- package/dist/chunk-4ZLMEKFX.js +18 -0
- package/dist/chunk-4ZLMEKFX.js.map +1 -0
- package/dist/chunk-55FU6WED.js +179 -0
- package/dist/chunk-55FU6WED.js.map +1 -0
- package/dist/chunk-6YI5K2TI.js +1959 -0
- package/dist/chunk-6YI5K2TI.js.map +1 -0
- package/dist/chunk-BHK3AD3Q.js +41 -0
- package/dist/chunk-BHK3AD3Q.js.map +1 -0
- package/dist/chunk-CRUQBZUF.js +39 -0
- package/dist/chunk-CRUQBZUF.js.map +1 -0
- package/dist/chunk-CTSQ7BRI.js +175 -0
- package/dist/chunk-CTSQ7BRI.js.map +1 -0
- package/dist/chunk-DK2JBJH7.js +81 -0
- package/dist/chunk-DK2JBJH7.js.map +1 -0
- package/dist/chunk-DP2PREDU.js +597 -0
- package/dist/chunk-DP2PREDU.js.map +1 -0
- package/dist/chunk-EQ2Z3KMD.js +24 -0
- package/dist/chunk-EQ2Z3KMD.js.map +1 -0
- package/dist/chunk-FZ7O6DWI.js +305 -0
- package/dist/chunk-FZ7O6DWI.js.map +1 -0
- package/dist/chunk-ISLYFQWL.js +1270 -0
- package/dist/chunk-ISLYFQWL.js.map +1 -0
- package/dist/chunk-JJL74ZPK.js +68 -0
- package/dist/chunk-JJL74ZPK.js.map +1 -0
- package/dist/chunk-JKXAPSU4.js +24 -0
- package/dist/chunk-JKXAPSU4.js.map +1 -0
- package/dist/chunk-KU5M27ZC.js +24 -0
- package/dist/chunk-KU5M27ZC.js.map +1 -0
- package/dist/chunk-LSHHRDVR.js +34 -0
- package/dist/chunk-LSHHRDVR.js.map +1 -0
- package/dist/chunk-M43PGOQY.js +715 -0
- package/dist/chunk-M43PGOQY.js.map +1 -0
- package/dist/chunk-MEJAHXIO.js +150 -0
- package/dist/chunk-MEJAHXIO.js.map +1 -0
- package/dist/chunk-NUCGHWCF.js +101 -0
- package/dist/chunk-NUCGHWCF.js.map +1 -0
- package/dist/chunk-OK5HOCQI.js +845 -0
- package/dist/chunk-OK5HOCQI.js.map +1 -0
- package/dist/chunk-OROPGO65.js +13 -0
- package/dist/chunk-OROPGO65.js.map +1 -0
- package/dist/chunk-PPAS4SZR.js +176 -0
- package/dist/chunk-PPAS4SZR.js.map +1 -0
- package/dist/chunk-PPBWRKO2.js +171 -0
- package/dist/chunk-PPBWRKO2.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QO7LAQZH.js +321 -0
- package/dist/chunk-QO7LAQZH.js.map +1 -0
- package/dist/chunk-QVJ2HCAX.js +225 -0
- package/dist/chunk-QVJ2HCAX.js.map +1 -0
- package/dist/chunk-RIPHIRPP.js +68 -0
- package/dist/chunk-RIPHIRPP.js.map +1 -0
- package/dist/chunk-S27S42QY.js +134 -0
- package/dist/chunk-S27S42QY.js.map +1 -0
- package/dist/chunk-SBCVAC2Z.js +40 -0
- package/dist/chunk-SBCVAC2Z.js.map +1 -0
- package/dist/chunk-TFJ4MKPH.js +694 -0
- package/dist/chunk-TFJ4MKPH.js.map +1 -0
- package/dist/chunk-THX3SHYA.js +75 -0
- package/dist/chunk-THX3SHYA.js.map +1 -0
- package/dist/chunk-UGQSQO5B.js +222 -0
- package/dist/chunk-UGQSQO5B.js.map +1 -0
- package/dist/chunk-V2UNHGAP.js +26 -0
- package/dist/chunk-V2UNHGAP.js.map +1 -0
- package/dist/chunk-VGTPQXNQ.js +2790 -0
- package/dist/chunk-VGTPQXNQ.js.map +1 -0
- package/dist/chunk-VNIHXQ7W.js +194 -0
- package/dist/chunk-VNIHXQ7W.js.map +1 -0
- package/dist/chunk-WV272MPW.js +31 -0
- package/dist/chunk-WV272MPW.js.map +1 -0
- package/dist/chunk-X5KKBOUS.js +26 -0
- package/dist/chunk-X5KKBOUS.js.map +1 -0
- package/dist/chunk-XANPEOJC.js +17 -0
- package/dist/chunk-XANPEOJC.js.map +1 -0
- package/dist/chunk-XPVQIHAQ.js +83 -0
- package/dist/chunk-XPVQIHAQ.js.map +1 -0
- package/dist/chunk-ZCINJSS4.js +75 -0
- package/dist/chunk-ZCINJSS4.js.map +1 -0
- package/dist/community.d.ts +1425 -0
- package/dist/community.js +206 -0
- package/dist/community.js.map +1 -0
- package/dist/config-2GDU7PCK.js +32 -0
- package/dist/config-2GDU7PCK.js.map +1 -0
- package/dist/context-MNZ4QXPC.js +16 -0
- package/dist/context-MNZ4QXPC.js.map +1 -0
- package/dist/db-schema.d.ts +4 -0
- package/dist/db-schema.js +102 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.d.ts +7 -0
- package/dist/db.js +117 -0
- package/dist/db.js.map +1 -0
- package/dist/digest-SY42GQSU.js +17 -0
- package/dist/digest-SY42GQSU.js.map +1 -0
- package/dist/errors-5OS3S2J3.js +22 -0
- package/dist/errors-5OS3S2J3.js.map +1 -0
- package/dist/host-OBOI4MJK.js +51 -0
- package/dist/host-OBOI4MJK.js.map +1 -0
- package/dist/i18n.d.ts +301 -0
- package/dist/i18n.js +68 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index-B6-_vr_m.d.ts +590 -0
- package/dist/index-CY55LC0u.d.ts +4722 -0
- package/dist/index-CeiTvwbp.d.ts +168 -0
- package/dist/index-XwP1ET8b.d.ts +61 -0
- package/dist/index.d.ts +2037 -0
- package/dist/index.js +2205 -0
- package/dist/index.js.map +1 -0
- package/dist/job-log-VZXWQUDK.js +24 -0
- package/dist/job-log-VZXWQUDK.js.map +1 -0
- package/dist/jobs.d.ts +4 -0
- package/dist/jobs.js +76 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logger-DqGaOU_j.d.ts +29 -0
- package/dist/logger-S7REWDNE.js +16 -0
- package/dist/logger-S7REWDNE.js.map +1 -0
- package/dist/media.d.ts +5 -0
- package/dist/media.js +41 -0
- package/dist/media.js.map +1 -0
- package/dist/mentions-2IHFVSHW.js +23 -0
- package/dist/mentions-2IHFVSHW.js.map +1 -0
- package/dist/mutes-EWAE5FZR.js +21 -0
- package/dist/mutes-EWAE5FZR.js.map +1 -0
- package/dist/notification-prefs-VPJDU7I6.js +21 -0
- package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
- package/dist/observability.d.ts +156 -0
- package/dist/observability.js +32 -0
- package/dist/observability.js.map +1 -0
- package/dist/profanity-adapter-NU2JQSLX.js +12 -0
- package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
- package/dist/queue-XE5BC75T.js +14 -0
- package/dist/queue-XE5BC75T.js.map +1 -0
- package/dist/rate-limit.d.ts +99 -0
- package/dist/rate-limit.js +14 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/registry-XIXDEPVI.js +31 -0
- package/dist/registry-XIXDEPVI.js.map +1 -0
- package/dist/reputation-JRL2YQHM.js +11 -0
- package/dist/reputation-JRL2YQHM.js.map +1 -0
- package/dist/routes.d.ts +43 -0
- package/dist/routes.js +12 -0
- package/dist/routes.js.map +1 -0
- package/dist/scheduled-CIQM57HT.js +20 -0
- package/dist/scheduled-CIQM57HT.js.map +1 -0
- package/dist/seo.d.ts +410 -0
- package/dist/seo.js +44 -0
- package/dist/seo.js.map +1 -0
- package/dist/settings-FOBIESPB.js +17 -0
- package/dist/settings-FOBIESPB.js.map +1 -0
- package/dist/spam-adapter-XX3G737Z.js +12 -0
- package/dist/spam-adapter-XX3G737Z.js.map +1 -0
- package/dist/strings-VAE47B2C.js +29 -0
- package/dist/strings-VAE47B2C.js.map +1 -0
- package/dist/templates-IFVJMCJ6.js +12 -0
- package/dist/templates-IFVJMCJ6.js.map +1 -0
- package/dist/types-TlsbXS0T.d.ts +871 -0
- package/package.json +129 -0
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
2
|
+
import { F as NpPrincipal, e as NpAuthUser } from './types-TlsbXS0T.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Community role registry. Maps a role name + scope type to the
|
|
6
|
+
* capabilities a grant of that role unlocks. Plugins extend the registry
|
|
7
|
+
* via `registerCommunityRole(...)` (gated by the `members:write` or
|
|
8
|
+
* `community:moderate` capability — enforced at registration time, not
|
|
9
|
+
* here).
|
|
10
|
+
*
|
|
11
|
+
* The capability vocabulary is the single source of truth for "what
|
|
12
|
+
* actions exist in the community." `memberCan()` (../community/can.ts)
|
|
13
|
+
* looks up grants and matches their roles' capability lists against the
|
|
14
|
+
* requested action.
|
|
15
|
+
*/
|
|
16
|
+
type CommunityScope = "site" | "category" | "collection" | "thread";
|
|
17
|
+
/**
|
|
18
|
+
* Action vocabulary. Adding new actions later is fine, but rename with
|
|
19
|
+
* care — built-in role definitions reference these literals and a
|
|
20
|
+
* silent typo widens permissions instead of narrowing them.
|
|
21
|
+
*/
|
|
22
|
+
type CommunityCapability = "hide-comment" | "restore-comment" | "edit-any-comment" | "delete-any-comment" | "hide-thread" | "restore-thread" | "lock-thread" | "unlock-thread" | "pin-thread" | "unpin-thread" | "edit-any-thread" | "delete-any-thread" | "edit-own-thread" | "lock-own-thread" | "ban-member" | "unban-member" | "resolve-report" | "manage-category" | "view-staff-tools";
|
|
23
|
+
interface CommunityRoleDefinition {
|
|
24
|
+
/** e.g. `"category-mod"`. Plugins can ship custom roles like `"tag-mod"`. */
|
|
25
|
+
role: string;
|
|
26
|
+
/** What kind of scope a grant of this role applies to. */
|
|
27
|
+
scopeType: CommunityScope;
|
|
28
|
+
/** Capabilities a grant of this role unlocks within its scope. */
|
|
29
|
+
capabilities: readonly CommunityCapability[];
|
|
30
|
+
/**
|
|
31
|
+
* Human-readable label for admin UIs that surface a role picker. Falls
|
|
32
|
+
* back to `role` when omitted.
|
|
33
|
+
*/
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Optional plugin id that registered this role; null for built-ins. */
|
|
36
|
+
source?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Plugins call this from setup() to add their own role kinds. Throws
|
|
40
|
+
* when the (role, scopeType) pair is already registered to keep the
|
|
41
|
+
* registry deterministic — a plugin overriding a built-in role would
|
|
42
|
+
* silently widen permissions and is almost always a mistake.
|
|
43
|
+
*/
|
|
44
|
+
declare function registerCommunityRole(definition: CommunityRoleDefinition): void;
|
|
45
|
+
/** Look up a role by `(role, scopeType)`. Returns undefined when unknown. */
|
|
46
|
+
declare function getCommunityRole(role: string, scopeType: CommunityScope): CommunityRoleDefinition | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Returns every role currently registered, built-ins first then
|
|
49
|
+
* plugin-defined. Used by the admin role picker to render selectable
|
|
50
|
+
* options for a given scope.
|
|
51
|
+
*/
|
|
52
|
+
declare function listCommunityRoles(scopeType?: CommunityScope): CommunityRoleDefinition[];
|
|
53
|
+
/** Tests reset state between cases; production callers should never need this. */
|
|
54
|
+
declare function resetCommunityRoles(): void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Throws `NpForbiddenError` if the member is currently banned for any
|
|
58
|
+
* scope in the chain. Used at the top of community write services
|
|
59
|
+
* before any DB mutation. Pre-existing `memberCan` enforces the same
|
|
60
|
+
* rule for permission-based actions; this helper is the catch-all
|
|
61
|
+
* for write paths that don't go through capability checks.
|
|
62
|
+
*/
|
|
63
|
+
declare function assertNotBanned(memberId: string, scopes?: ReadonlyArray<{
|
|
64
|
+
type: CommunityScope;
|
|
65
|
+
id: string;
|
|
66
|
+
}>): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Structural enforcement of the ban-check gate (#311). Every
|
|
69
|
+
* community write service should run inside this wrapper — the ban
|
|
70
|
+
* check fires before `fn` and a service author can't accidentally
|
|
71
|
+
* ship a new write path that skips it.
|
|
72
|
+
*
|
|
73
|
+
* Pre-validation that doesn't write (input shape, target lookup
|
|
74
|
+
* existence) can run *before* this call; the gate is specifically
|
|
75
|
+
* for the moment between "we know enough to attempt the write" and
|
|
76
|
+
* the first DB mutation.
|
|
77
|
+
*
|
|
78
|
+
* `scopes` is the same chain `assertNotBanned` accepts — pass
|
|
79
|
+
* `[{ type: "collection", id: targetType }]` for collection-scoped
|
|
80
|
+
* actions, leave empty for site-wide-only enforcement (e.g. follows,
|
|
81
|
+
* polymorphic-target reactions where no obvious scope chain exists).
|
|
82
|
+
*/
|
|
83
|
+
declare function withMemberWrite<T>(memberId: string, scopes: ReadonlyArray<{
|
|
84
|
+
type: CommunityScope;
|
|
85
|
+
id: string;
|
|
86
|
+
}>, fn: () => Promise<T>): Promise<T>;
|
|
87
|
+
/**
|
|
88
|
+
* Action a member is attempting. Most actions are real
|
|
89
|
+
* `CommunityCapability` literals — those map 1:1 to a role's
|
|
90
|
+
* capability list. The two exceptions are `"edit-own"` and
|
|
91
|
+
* `"delete-own"`, which short-circuit on ownership without consulting
|
|
92
|
+
* grants at all.
|
|
93
|
+
*/
|
|
94
|
+
type MemberAction = CommunityCapability | "edit-own" | "delete-own";
|
|
95
|
+
/**
|
|
96
|
+
* Caller-provided context for a permission check. The caller — the
|
|
97
|
+
* comment service, a future thread service, etc. — provides the
|
|
98
|
+
* target's ownership + scope chain rather than `memberCan` looking
|
|
99
|
+
* it up via a polymorphic join. This keeps the resolver decoupled
|
|
100
|
+
* from the per-target table layout, and lets the surface evolve
|
|
101
|
+
* without touching this resolver.
|
|
102
|
+
*/
|
|
103
|
+
interface MemberCanTarget {
|
|
104
|
+
/** Free-form target type — `"comment" | "thread" | "reply" | "category" | "report" | "member"`. */
|
|
105
|
+
type: string;
|
|
106
|
+
/** Stable id for logs / future denial reasons. */
|
|
107
|
+
id: string;
|
|
108
|
+
/** Member id of the target's author. Required for own-action checks. */
|
|
109
|
+
ownerId?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Scope chain from most specific to least specific. A reply might
|
|
112
|
+
* provide `[{ type: "thread", id: "<threadId>" }, { type: "category",
|
|
113
|
+
* id: "<categoryId>" }]`; the resolver also checks site-wide grants
|
|
114
|
+
* regardless of what's in the chain.
|
|
115
|
+
*/
|
|
116
|
+
scopes?: ReadonlyArray<{
|
|
117
|
+
type: CommunityScope;
|
|
118
|
+
id: string;
|
|
119
|
+
}>;
|
|
120
|
+
}
|
|
121
|
+
interface MemberCanOptions {
|
|
122
|
+
/** Override the DB handle (tests). Defaults to `getDb()`. */
|
|
123
|
+
db?: NodePgDatabase<Record<string, unknown>>;
|
|
124
|
+
/** Reference time for ban/grant expiry checks. Defaults to `new Date()`. */
|
|
125
|
+
now?: Date;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Returns true when `memberId` is allowed to perform `action` on
|
|
129
|
+
* `target`. Walk order:
|
|
130
|
+
*
|
|
131
|
+
* 1. Active scoped ban → deny everything.
|
|
132
|
+
* 2. `edit-own` / `delete-own` → allow only when `target.ownerId === memberId`.
|
|
133
|
+
* 3. Site-wide grants whose role's capability list includes `action`.
|
|
134
|
+
* 4. Scoped grants matching any element of `target.scopes`, whose role
|
|
135
|
+
* includes `action`.
|
|
136
|
+
* 5. Otherwise deny.
|
|
137
|
+
*
|
|
138
|
+
* The resolver ignores staff (`np_users`) entirely. Staff bypass is the
|
|
139
|
+
* caller's responsibility — typically `principalCan(principal, …)` at
|
|
140
|
+
* the API layer, which routes to `memberCan` only when the principal
|
|
141
|
+
* is a member.
|
|
142
|
+
*/
|
|
143
|
+
declare function memberCan(memberId: string, action: MemberAction, target: MemberCanTarget, options?: MemberCanOptions): Promise<boolean>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pluggable anti-spam adapter. Plugins call `setSpamAdapter(adapter)`
|
|
147
|
+
* at startup; the community write path consults `getSpamAdapter()
|
|
148
|
+
* .check(text, context)` before inserting and acts on the verdict:
|
|
149
|
+
*
|
|
150
|
+
* - `"pass"` → write proceeds normally (status = `visible`)
|
|
151
|
+
* - `"flag"` → write proceeds but lands as `pending` (visible only
|
|
152
|
+
* to mods; appears in the report queue indirectly via
|
|
153
|
+
* the moderation surface)
|
|
154
|
+
* - `"reject"` → write is refused; the caller surfaces a 400
|
|
155
|
+
* `NpValidationError`. Adapters may attach a `reason`
|
|
156
|
+
* string for the error message.
|
|
157
|
+
*
|
|
158
|
+
* Adapters are intentionally synchronous-friendly (they may also
|
|
159
|
+
* return a Promise). The framework awaits the result so adapters that
|
|
160
|
+
* call out to a network service (Akismet, OpenAI moderation, etc.)
|
|
161
|
+
* work transparently.
|
|
162
|
+
*
|
|
163
|
+
* The default adapter is "no-op pass" — every write proceeds as
|
|
164
|
+
* before. Sites that want spam protection install one explicitly.
|
|
165
|
+
*/
|
|
166
|
+
type NpSpamVerdictKind = "pass" | "flag" | "reject";
|
|
167
|
+
interface NpSpamVerdict {
|
|
168
|
+
kind: NpSpamVerdictKind;
|
|
169
|
+
/**
|
|
170
|
+
* Optional human-readable reason. Used as the
|
|
171
|
+
* `NpValidationError` message on `reject`, surfaced to the
|
|
172
|
+
* audit log on `flag`. Don't include PII or provider error text
|
|
173
|
+
* verbatim — operators see this in logs.
|
|
174
|
+
*/
|
|
175
|
+
reason?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Free-form metadata the adapter wants to log alongside the
|
|
178
|
+
* verdict (model name, score, classifier id, etc.). Surfaced to
|
|
179
|
+
* the audit log; never echoed to the end user.
|
|
180
|
+
*/
|
|
181
|
+
metadata?: Record<string, unknown>;
|
|
182
|
+
}
|
|
183
|
+
interface NpSpamCheckContext {
|
|
184
|
+
/** Member id of the author. Adapters may use this to weight by
|
|
185
|
+
* reputation or recent infraction history. */
|
|
186
|
+
memberId: string;
|
|
187
|
+
/**
|
|
188
|
+
* Collection slug that owns the document the comment is attached
|
|
189
|
+
* to (`"posts"`, `"discussions"`, etc.) — same value as
|
|
190
|
+
* `np_comments.target_type`. The schema is polymorphic over
|
|
191
|
+
* collection, so this is the collection identifier, not a
|
|
192
|
+
* "comment vs thread" classifier.
|
|
193
|
+
*/
|
|
194
|
+
targetType: string;
|
|
195
|
+
/** Document id within `targetType` — the post / discussion the
|
|
196
|
+
* comment is attached to. */
|
|
197
|
+
targetId: string;
|
|
198
|
+
/** Parent comment id when this is a reply, otherwise null. */
|
|
199
|
+
parentId?: string | null;
|
|
200
|
+
}
|
|
201
|
+
interface NpSpamAdapter {
|
|
202
|
+
check(text: string, ctx: NpSpamCheckContext): NpSpamVerdict | Promise<NpSpamVerdict>;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Replace the global spam adapter. Call once at app boot, typically
|
|
206
|
+
* from a plugin's `setup()`. Multiple plugins competing for this slot
|
|
207
|
+
* should compose their checks behind a single `setSpamAdapter` call —
|
|
208
|
+
* the framework holds at most one adapter to keep the verdict
|
|
209
|
+
* unambiguous.
|
|
210
|
+
*/
|
|
211
|
+
declare function setSpamAdapter(adapter: NpSpamAdapter): void;
|
|
212
|
+
declare function getSpamAdapter(): NpSpamAdapter;
|
|
213
|
+
/** Reset to the default no-op adapter. Tests use this between cases. */
|
|
214
|
+
declare function resetSpamAdapter(): void;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Pluggable profanity adapter. Sister to `spam-adapter.ts`, but
|
|
218
|
+
* semantically scoped to *language* rather than *intent*: profanity
|
|
219
|
+
* adapters score the words in a piece of content, spam adapters score
|
|
220
|
+
* the likelihood that the post is unwanted commercial / abusive.
|
|
221
|
+
*
|
|
222
|
+
* Many sites want both: an off-the-shelf regex list to scrub slurs
|
|
223
|
+
* plus an ML / Akismet-style classifier for spam. Rather than force
|
|
224
|
+
* those to compose behind a single `setSpamAdapter` call, the
|
|
225
|
+
* framework holds two slots and runs profanity FIRST, then spam.
|
|
226
|
+
* Verdicts combine with the strongest-wins rule:
|
|
227
|
+
*
|
|
228
|
+
* - any `reject` → write is refused with that adapter's reason
|
|
229
|
+
* - any `flag` → write proceeds as `pending` with both adapters'
|
|
230
|
+
* metadata aggregated for the audit row
|
|
231
|
+
* - both `pass` → normal write
|
|
232
|
+
*
|
|
233
|
+
* The default adapter is "no-op pass" — every write proceeds as
|
|
234
|
+
* before. Sites that want profanity protection install one
|
|
235
|
+
* explicitly, typically from a plugin's `setup()`.
|
|
236
|
+
*/
|
|
237
|
+
type NpProfanityVerdictKind = "pass" | "flag" | "reject";
|
|
238
|
+
interface NpProfanityVerdict {
|
|
239
|
+
kind: NpProfanityVerdictKind;
|
|
240
|
+
/**
|
|
241
|
+
* Optional human-readable reason. Used as the
|
|
242
|
+
* `NpValidationError` message on `reject`, surfaced to the
|
|
243
|
+
* audit log on `flag`. Don't include the matched word verbatim
|
|
244
|
+
* if you don't want it echoed to the end user on reject.
|
|
245
|
+
*/
|
|
246
|
+
reason?: string;
|
|
247
|
+
/**
|
|
248
|
+
* Free-form metadata the adapter wants to log alongside the
|
|
249
|
+
* verdict (matched categories, severity, locale, etc.). Surfaced
|
|
250
|
+
* to the audit log; never echoed to the end user.
|
|
251
|
+
*/
|
|
252
|
+
metadata?: Record<string, unknown>;
|
|
253
|
+
}
|
|
254
|
+
interface NpProfanityCheckContext {
|
|
255
|
+
/** Member id of the author. Adapters may use this to weight
|
|
256
|
+
* by reputation or recent infraction history. */
|
|
257
|
+
memberId: string;
|
|
258
|
+
/**
|
|
259
|
+
* Surface the content lives on. For comments this is the
|
|
260
|
+
* collection slug of the parent doc (`"posts"`, `"discussions"`,
|
|
261
|
+
* etc.); for member-authored docs, this is the same collection
|
|
262
|
+
* slug. Mirrors `NpSpamCheckContext.targetType`.
|
|
263
|
+
*/
|
|
264
|
+
targetType: string;
|
|
265
|
+
/** Document id the content belongs to. Empty string for a
|
|
266
|
+
* pre-insert doc create — adapters that key off the id should
|
|
267
|
+
* treat empty as "new doc". */
|
|
268
|
+
targetId: string;
|
|
269
|
+
/** Parent comment id when this is a reply, otherwise null /
|
|
270
|
+
* undefined (for doc creates). */
|
|
271
|
+
parentId?: string | null;
|
|
272
|
+
}
|
|
273
|
+
interface NpProfanityAdapter {
|
|
274
|
+
check(text: string, ctx: NpProfanityCheckContext): NpProfanityVerdict | Promise<NpProfanityVerdict>;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Replace the global profanity adapter. Call once at app boot,
|
|
278
|
+
* typically from a plugin's `setup()`. The framework holds at most
|
|
279
|
+
* one adapter; sites that want to layer multiple lists should compose
|
|
280
|
+
* them inside a single adapter (the same convention as the spam
|
|
281
|
+
* adapter).
|
|
282
|
+
*/
|
|
283
|
+
declare function setProfanityAdapter(adapter: NpProfanityAdapter): void;
|
|
284
|
+
declare function getProfanityAdapter(): NpProfanityAdapter;
|
|
285
|
+
/** Reset to the default no-op adapter. Tests use this between cases. */
|
|
286
|
+
declare function resetProfanityAdapter(): void;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Pluggable reputation-rules hook. Sites install an adapter via
|
|
290
|
+
* `setReputationAdapter()` to compute reputation deltas in response
|
|
291
|
+
* to community events; the framework then atomically applies the
|
|
292
|
+
* delta to `np_members.reputation`.
|
|
293
|
+
*
|
|
294
|
+
* Default adapter is "no-op" (every event returns 0) — existing
|
|
295
|
+
* sites' reputation values stay at zero until they opt in.
|
|
296
|
+
*
|
|
297
|
+
* Adapter is single-method by design: a tagged-union `event` is the
|
|
298
|
+
* only argument, the return value is a signed integer delta. This
|
|
299
|
+
* keeps the API surface small while letting sites encode arbitrary
|
|
300
|
+
* weighting (e.g. "+5 for a like on a comment, −10 for a moderator
|
|
301
|
+
* hide, −0 if the reactor is a brand-new account, etc.").
|
|
302
|
+
*
|
|
303
|
+
* Adapters can be sync or async — the framework awaits the result.
|
|
304
|
+
* Throwing aborts only the reputation update, not the underlying
|
|
305
|
+
* community write (fail-soft via observability hook, same pattern
|
|
306
|
+
* as the spam adapter).
|
|
307
|
+
*/
|
|
308
|
+
type NpReputationEvent =
|
|
309
|
+
/** A new visible comment was inserted. Flagged / hidden / deleted
|
|
310
|
+
* comments do NOT emit this event. */
|
|
311
|
+
{
|
|
312
|
+
kind: "comment.created";
|
|
313
|
+
commentId: string;
|
|
314
|
+
memberId: string;
|
|
315
|
+
targetType: string;
|
|
316
|
+
targetId: string;
|
|
317
|
+
}
|
|
318
|
+
/** Mod (or member with the right grant) hid a comment. Adapters
|
|
319
|
+
* typically penalize the author. */
|
|
320
|
+
| {
|
|
321
|
+
kind: "comment.hidden";
|
|
322
|
+
commentId: string;
|
|
323
|
+
memberId: string;
|
|
324
|
+
byStaff: boolean;
|
|
325
|
+
reason?: string | null;
|
|
326
|
+
}
|
|
327
|
+
/** Mod-side hard delete (`staffDeleteComment`). The body is wiped;
|
|
328
|
+
* this is harsher than `hidden` and adapters usually penalize
|
|
329
|
+
* more. */
|
|
330
|
+
| {
|
|
331
|
+
kind: "comment.deleted";
|
|
332
|
+
commentId: string;
|
|
333
|
+
memberId: string;
|
|
334
|
+
byStaff: boolean;
|
|
335
|
+
}
|
|
336
|
+
/** Someone reacted to the recipient's content (comment / thread /
|
|
337
|
+
* reply). `recipientId` is the content author; `reactorId` is the
|
|
338
|
+
* member who clicked the reaction. Self-reactions are filtered
|
|
339
|
+
* before the event fires. */
|
|
340
|
+
| {
|
|
341
|
+
kind: "reaction.received";
|
|
342
|
+
reactionKind: string;
|
|
343
|
+
recipientId: string;
|
|
344
|
+
reactorId: string;
|
|
345
|
+
targetType: string;
|
|
346
|
+
targetId: string;
|
|
347
|
+
}
|
|
348
|
+
/** Reactor undid their reaction. Symmetric to `reaction.received`;
|
|
349
|
+
* adapters typically return the negative of the corresponding
|
|
350
|
+
* positive delta. */
|
|
351
|
+
| {
|
|
352
|
+
kind: "reaction.removed";
|
|
353
|
+
reactionKind: string;
|
|
354
|
+
recipientId: string;
|
|
355
|
+
reactorId: string;
|
|
356
|
+
targetType: string;
|
|
357
|
+
targetId: string;
|
|
358
|
+
}
|
|
359
|
+
/** A member created a top-level document in a collection that
|
|
360
|
+
* opted into `community.memberWrite.create` (Phase 9.7a). Fires
|
|
361
|
+
* after the row + revision are persisted; adapters can credit
|
|
362
|
+
* reputation for thread / post creation just like comments. */
|
|
363
|
+
| {
|
|
364
|
+
kind: "document.created";
|
|
365
|
+
collectionSlug: string;
|
|
366
|
+
documentId: string;
|
|
367
|
+
memberId: string;
|
|
368
|
+
}
|
|
369
|
+
/** Author deleted their own document (`memberWrite.delete`,
|
|
370
|
+
* Phase 9.7b). Symmetric to `document.created`; adapters
|
|
371
|
+
* typically debit the original credit so a member can't farm
|
|
372
|
+
* reputation by churn-creating and deleting threads. Mod-side
|
|
373
|
+
* deletes are NOT covered here — those go through the staff
|
|
374
|
+
* path which doesn't emit this event. */
|
|
375
|
+
| {
|
|
376
|
+
kind: "document.deleted";
|
|
377
|
+
collectionSlug: string;
|
|
378
|
+
documentId: string;
|
|
379
|
+
memberId: string;
|
|
380
|
+
};
|
|
381
|
+
interface NpReputationAdapter {
|
|
382
|
+
/** Returns the integer delta to apply to the affected member's
|
|
383
|
+
* reputation. Sign matters: positive credits, negative debits.
|
|
384
|
+
* Non-integer values are truncated; non-finite (NaN/Infinity)
|
|
385
|
+
* values are skipped. Returning 0 is the no-op path. */
|
|
386
|
+
apply(event: NpReputationEvent): number | Promise<number>;
|
|
387
|
+
}
|
|
388
|
+
declare function setReputationAdapter(adapter: NpReputationAdapter): void;
|
|
389
|
+
declare function getReputationAdapter(): NpReputationAdapter;
|
|
390
|
+
/** Reset to the no-op adapter. Tests use this between cases. */
|
|
391
|
+
declare function resetReputationAdapter(): void;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Calls the registered reputation adapter for `event`, then applies
|
|
395
|
+
* the returned delta to the affected member's reputation atomically:
|
|
396
|
+
*
|
|
397
|
+
* UPDATE np_members SET reputation = reputation + $delta
|
|
398
|
+
* WHERE id = $memberId
|
|
399
|
+
*
|
|
400
|
+
* Failure modes are intentionally fail-soft — a buggy adapter that
|
|
401
|
+
* throws, returns a non-finite value, or hits a transient DB error
|
|
402
|
+
* MUST NOT block the underlying community write (comment insert,
|
|
403
|
+
* reaction toggle, etc.). The caller's transactional state is not
|
|
404
|
+
* touched; we just log + skip.
|
|
405
|
+
*/
|
|
406
|
+
declare function applyReputation(memberId: string, event: NpReputationEvent): Promise<void>;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Site-wide community settings, persisted in the generic `np_settings`
|
|
410
|
+
* table under the `community` key. Sites that never visit the admin UI
|
|
411
|
+
* inherit `DEFAULT_COMMUNITY_SETTINGS` — every read goes through
|
|
412
|
+
* `getCommunitySettings()` which merges the stored value over the
|
|
413
|
+
* defaults so adding a new field doesn't break existing installs.
|
|
414
|
+
*
|
|
415
|
+
* Validation runs on the write path only — readers trust whatever is
|
|
416
|
+
* in the table because the only writer is the admin API which
|
|
417
|
+
* pre-validates. Tests poke values directly into `np_settings` for
|
|
418
|
+
* fault-injection cases.
|
|
419
|
+
*/
|
|
420
|
+
/**
|
|
421
|
+
* Per-member upload quota / rate limit. `null` on either field
|
|
422
|
+
* means unlimited (the default — no quota). Both bounds count
|
|
423
|
+
* non-deleted rows on `np_media` keyed by `uploaded_by_member_id`,
|
|
424
|
+
* so admin purges (Phase 9.7l) free up quota the same way a
|
|
425
|
+
* member self-deleting their content would. Staff uploads are
|
|
426
|
+
* never gated.
|
|
427
|
+
*/
|
|
428
|
+
interface NpMemberUploadQuota {
|
|
429
|
+
/** Max uploads in the trailing 24h window. `null` = unlimited. */
|
|
430
|
+
perDay: number | null;
|
|
431
|
+
/** Lifetime cap on non-deleted member uploads. `null` = unlimited. */
|
|
432
|
+
total: number | null;
|
|
433
|
+
}
|
|
434
|
+
interface NpCommunitySettings {
|
|
435
|
+
/**
|
|
436
|
+
* Allow-list of reaction `kind` strings. Members can only add
|
|
437
|
+
* reactions whose kind is in this list; values that pass the
|
|
438
|
+
* `KIND_RE` regex but aren't in the list are rejected with a 400.
|
|
439
|
+
* Removal of an already-existing reaction is NOT gated — if a kind
|
|
440
|
+
* is removed from the list, members can still un-react it.
|
|
441
|
+
*/
|
|
442
|
+
reactionKinds: string[];
|
|
443
|
+
/**
|
|
444
|
+
* When false, `/api/members/register` refuses new sign-ups with a
|
|
445
|
+
* 403. Existing members can still sign in. Sites that want
|
|
446
|
+
* invite-only flows turn this off and provision via admin tooling.
|
|
447
|
+
*/
|
|
448
|
+
registrationEnabled: boolean;
|
|
449
|
+
/** Per-member upload limits. See `NpMemberUploadQuota`. */
|
|
450
|
+
memberUploadQuota: NpMemberUploadQuota;
|
|
451
|
+
}
|
|
452
|
+
declare const DEFAULT_COMMUNITY_SETTINGS: NpCommunitySettings;
|
|
453
|
+
declare function getCommunitySettings(): Promise<NpCommunitySettings>;
|
|
454
|
+
/**
|
|
455
|
+
* Validates an incoming partial patch from the admin UI. Returns the
|
|
456
|
+
* fully-merged settings object that should be persisted. Throws
|
|
457
|
+
* `NpValidationError` with field-level errors on any malformed input.
|
|
458
|
+
*/
|
|
459
|
+
declare function validateCommunitySettingsPatch(current: NpCommunitySettings, patch: unknown): NpCommunitySettings;
|
|
460
|
+
declare function updateCommunitySettings(patch: unknown, updatedBy: string | null): Promise<NpCommunitySettings>;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Public-facing member profile. Hand-picked from `np_members` to
|
|
464
|
+
* exclude PII (email, password hash, login attempts, reset tokens,
|
|
465
|
+
* notification prefs, plugin meta) — page authors building public
|
|
466
|
+
* surfaces (`/u/[handle]` etc.) get a safe-to-render shape without
|
|
467
|
+
* having to remember which columns are sensitive.
|
|
468
|
+
*
|
|
469
|
+
* Suspended / deleted members are filtered out — calling
|
|
470
|
+
* `getMemberProfile` for a hidden member returns `null`. The
|
|
471
|
+
* "imported" status (Phase 21 WordPress-import provisional members)
|
|
472
|
+
* IS exposed because those profiles are visible on the public site
|
|
473
|
+
* by design. Bans are a separate, scope-based concept (`np_bans`)
|
|
474
|
+
* and don't hide the profile shell — they restrict posting; the
|
|
475
|
+
* profile page itself stays reachable like Reddit / Discourse.
|
|
476
|
+
*/
|
|
477
|
+
interface NpMemberProfile {
|
|
478
|
+
id: string;
|
|
479
|
+
handle: string;
|
|
480
|
+
displayName: string;
|
|
481
|
+
avatarUrl: string | null;
|
|
482
|
+
bio: string | null;
|
|
483
|
+
reputation: number;
|
|
484
|
+
joinedAt: Date;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Fetch a public member profile by id or handle.
|
|
488
|
+
*
|
|
489
|
+
* Resolves the avatar to a public URL (via `getMediaUrl`) so the
|
|
490
|
+
* caller doesn't need to know about the storage adapter. Pass an
|
|
491
|
+
* explicit `variant` to fetch a sized avatar — defaults to
|
|
492
|
+
* `"thumbnail"` since profile cards typically render at small
|
|
493
|
+
* sizes. Pass `"original"` for the full avatar (e.g. on the
|
|
494
|
+
* profile detail page itself).
|
|
495
|
+
*
|
|
496
|
+
* Returns `null` when:
|
|
497
|
+
* - no row matches the id / handle,
|
|
498
|
+
* - the member's status is `suspended` or `deleted` (treat as
|
|
499
|
+
* "not found" for public surfaces).
|
|
500
|
+
*/
|
|
501
|
+
declare function getMemberProfile(idOrHandle: string, options?: {
|
|
502
|
+
avatarVariant?: "original" | "thumbnail" | "small" | "medium" | "large" | (string & {});
|
|
503
|
+
}): Promise<NpMemberProfile | null>;
|
|
504
|
+
/**
|
|
505
|
+
* Batch variant of `getMemberProfile` for listings (discussion
|
|
506
|
+
* indexes, comment threads, follower lists, …). Single SELECT
|
|
507
|
+
* for the rows; avatar URLs resolve in parallel via `Promise.all`.
|
|
508
|
+
*
|
|
509
|
+
* The caller passes member IDs (the `memberAuthorId` /
|
|
510
|
+
* `memberId` foreign keys most listing rows already carry).
|
|
511
|
+
* Handle-based batches aren't supported — list rows that
|
|
512
|
+
* reference a handle and not an id are rare; pass IDs.
|
|
513
|
+
*
|
|
514
|
+
* Returns a `Map<id, NpMemberProfile>` with one entry per id
|
|
515
|
+
* that matched (suspended / deleted members are dropped, so the
|
|
516
|
+
* map size may be smaller than the input). Order isn't preserved
|
|
517
|
+
* because callers typically use `byId.get(row.memberId)` per row
|
|
518
|
+
* rather than a parallel array.
|
|
519
|
+
*
|
|
520
|
+
* Empty input → empty map (no DB query).
|
|
521
|
+
*/
|
|
522
|
+
declare function getMemberProfiles(ids: readonly string[], options?: {
|
|
523
|
+
avatarVariant?: "original" | "thumbnail" | "small" | "medium" | "large" | (string & {});
|
|
524
|
+
}): Promise<Map<string, NpMemberProfile>>;
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Tiny safe markdown renderer for comment bodies. Deliberately minimal:
|
|
528
|
+
* we escape every byte first, then pattern-match a small set of inline
|
|
529
|
+
* + block constructs so the output HTML can only ever contain the
|
|
530
|
+
* limited tag set listed below. No raw HTML pass-through, ever.
|
|
531
|
+
*
|
|
532
|
+
* Supported:
|
|
533
|
+
* - Bold `**text**` → `<strong>text</strong>`
|
|
534
|
+
* - Italic `*text*` → `<em>text</em>`
|
|
535
|
+
* - Inline code `` `code` `` → `<code>code</code>`
|
|
536
|
+
* - Code block ``` … ``` → `<pre><code>…</code></pre>`
|
|
537
|
+
* - Link `[t](url)` → `<a href="url" rel="…">t</a>`
|
|
538
|
+
* (URL must start with http://, https://, or mailto:)
|
|
539
|
+
* - Paragraph break: blank line
|
|
540
|
+
* - Hard break single \n → `<br/>`
|
|
541
|
+
*
|
|
542
|
+
* NOT supported (deliberate, to keep the renderer tight + safe):
|
|
543
|
+
* raw HTML, headings, lists, blockquotes, images, tables. If a
|
|
544
|
+
* site needs richer formatting, plug `marked` + `dompurify` here
|
|
545
|
+
* without changing the public function shape.
|
|
546
|
+
*/
|
|
547
|
+
/**
|
|
548
|
+
* Render a comment body markdown source to safe HTML. Pure function;
|
|
549
|
+
* idempotent; safe to call on the write path AND on display (we still
|
|
550
|
+
* persist the rendered version to avoid re-rendering on every read).
|
|
551
|
+
*/
|
|
552
|
+
declare function renderCommentMarkdown(source: string): string;
|
|
553
|
+
|
|
554
|
+
type CommentStatus = "visible" | "pending" | "hidden" | "deleted";
|
|
555
|
+
interface NpCommentRow {
|
|
556
|
+
id: string;
|
|
557
|
+
targetType: string;
|
|
558
|
+
targetId: string;
|
|
559
|
+
parentId: string | null;
|
|
560
|
+
memberId: string;
|
|
561
|
+
bodyMd: string;
|
|
562
|
+
bodyHtml: string;
|
|
563
|
+
status: CommentStatus;
|
|
564
|
+
hiddenReason: string | null;
|
|
565
|
+
editedAt: Date | null;
|
|
566
|
+
/** Tenant the comment belongs to. Phase 18 added the column; the type was incomplete until #364. */
|
|
567
|
+
siteId: string;
|
|
568
|
+
createdAt: Date;
|
|
569
|
+
/**
|
|
570
|
+
* Phase 21.11 — author's `np_members.status` at read time.
|
|
571
|
+
* `listComments` joins against `np_members` so callers can render
|
|
572
|
+
* a `(imported)` badge without a second round trip. Older callers
|
|
573
|
+
* that don't read this field stay unaffected — the column is
|
|
574
|
+
* nullable on the type because the underlying join is `LEFT JOIN`
|
|
575
|
+
* and `createComment` returns the row before the join is wired.
|
|
576
|
+
*/
|
|
577
|
+
authorStatus?: string | null;
|
|
578
|
+
}
|
|
579
|
+
interface NpCommentCreateInput {
|
|
580
|
+
targetType: string;
|
|
581
|
+
targetId: string;
|
|
582
|
+
parentId?: string | null;
|
|
583
|
+
memberId: string;
|
|
584
|
+
bodyMd: string;
|
|
585
|
+
}
|
|
586
|
+
declare function createComment(input: NpCommentCreateInput): Promise<NpCommentRow>;
|
|
587
|
+
/**
|
|
588
|
+
* Comment ordering options.
|
|
589
|
+
*
|
|
590
|
+
* - `newest` — created_at DESC (default; matches the
|
|
591
|
+
* surface a fresh thread should show)
|
|
592
|
+
* - `oldest` — created_at ASC (chronological reads)
|
|
593
|
+
* - `top` — reactions DESC, then created_at DESC as
|
|
594
|
+
* tiebreaker. Useful for high-traffic threads where the
|
|
595
|
+
* "best" comment should bubble up regardless of when
|
|
596
|
+
* it was posted.
|
|
597
|
+
*/
|
|
598
|
+
type NpCommentSort = "newest" | "oldest" | "top";
|
|
599
|
+
interface NpCommentListOptions {
|
|
600
|
+
/** Default 50, max 200. */
|
|
601
|
+
limit?: number;
|
|
602
|
+
/** Default 0. */
|
|
603
|
+
offset?: number;
|
|
604
|
+
/** Newest first by default. */
|
|
605
|
+
order?: NpCommentSort;
|
|
606
|
+
/** Override visibility — staff/mods may want to see hidden rows. */
|
|
607
|
+
includeHidden?: boolean;
|
|
608
|
+
/**
|
|
609
|
+
* Phase 16.1 — when set, the viewer's mute list is applied
|
|
610
|
+
* so authors they've muted disappear from the result. The
|
|
611
|
+
* filter only kicks in for the logged-in viewer; anonymous
|
|
612
|
+
* viewers see every visible comment.
|
|
613
|
+
*/
|
|
614
|
+
viewerMemberId?: string;
|
|
615
|
+
}
|
|
616
|
+
interface NpCommentListResult {
|
|
617
|
+
comments: NpCommentRow[];
|
|
618
|
+
totalDocs: number;
|
|
619
|
+
}
|
|
620
|
+
declare function listComments(targetType: string, targetId: string, options?: NpCommentListOptions): Promise<NpCommentListResult>;
|
|
621
|
+
interface NpCommentUpdateInput {
|
|
622
|
+
commentId: string;
|
|
623
|
+
memberId: string;
|
|
624
|
+
bodyMd: string;
|
|
625
|
+
}
|
|
626
|
+
declare function updateComment(input: NpCommentUpdateInput): Promise<NpCommentRow>;
|
|
627
|
+
interface NpCommentDeleteInput {
|
|
628
|
+
commentId: string;
|
|
629
|
+
memberId: string;
|
|
630
|
+
}
|
|
631
|
+
declare function deleteComment(input: NpCommentDeleteInput): Promise<void>;
|
|
632
|
+
interface NpCommentHideInput {
|
|
633
|
+
commentId: string;
|
|
634
|
+
memberId: string;
|
|
635
|
+
reason?: string | null;
|
|
636
|
+
}
|
|
637
|
+
declare function hideComment(input: NpCommentHideInput): Promise<void>;
|
|
638
|
+
interface NpCommentRestoreInput {
|
|
639
|
+
commentId: string;
|
|
640
|
+
memberId: string;
|
|
641
|
+
}
|
|
642
|
+
declare function restoreComment(input: NpCommentRestoreInput): Promise<void>;
|
|
643
|
+
declare function staffHideComment(commentId: string, staffUserId: string, reason?: string | null): Promise<void>;
|
|
644
|
+
declare function staffRestoreComment(commentId: string, staffUserId: string): Promise<void>;
|
|
645
|
+
declare function staffDeleteComment(commentId: string, staffUserId: string): Promise<void>;
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Reactions service. `kind` is gated by both:
|
|
649
|
+
* 1. `KIND_RE` — a syntactic check (lowercase token, ≤30 chars)
|
|
650
|
+
* that runs on every add/remove call without a DB round-trip.
|
|
651
|
+
* 2. The site's reaction allow-list, persisted in
|
|
652
|
+
* `np_settings.community.reactionKinds` and edited from the
|
|
653
|
+
* admin community settings page. v1 ships with `["like"]` as
|
|
654
|
+
* the only allowed kind. Removal is NOT gated against the
|
|
655
|
+
* allow-list — if a site retires a reaction, members can still
|
|
656
|
+
* undo their old reactions of that kind.
|
|
657
|
+
*/
|
|
658
|
+
declare const DEFAULT_REACTION_KINDS: readonly ["like"];
|
|
659
|
+
interface NpReactionRow {
|
|
660
|
+
id: string;
|
|
661
|
+
targetType: string;
|
|
662
|
+
targetId: string;
|
|
663
|
+
memberId: string;
|
|
664
|
+
kind: string;
|
|
665
|
+
createdAt: Date;
|
|
666
|
+
}
|
|
667
|
+
interface NpReactToInput {
|
|
668
|
+
targetType: string;
|
|
669
|
+
targetId: string;
|
|
670
|
+
memberId: string;
|
|
671
|
+
kind: string;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Adds a reaction. Idempotent: if `(target_type, target_id, member_id,
|
|
675
|
+
* kind)` already exists, returns the existing row instead of bumping
|
|
676
|
+
* the unique-constraint into an error. The first time a member reacts
|
|
677
|
+
* to a comment we also fire a notification to the comment author.
|
|
678
|
+
*/
|
|
679
|
+
declare function addReaction(input: NpReactToInput): Promise<NpReactionRow>;
|
|
680
|
+
declare function removeReaction(input: NpReactToInput): Promise<void>;
|
|
681
|
+
/**
|
|
682
|
+
* Per-target counts grouped by kind. Returns `{ like: 12 }`-style
|
|
683
|
+
* objects; missing kinds are absent (caller defaults to 0).
|
|
684
|
+
*/
|
|
685
|
+
declare function countReactions(targetType: string, targetId: string): Promise<Record<string, number>>;
|
|
686
|
+
/**
|
|
687
|
+
* Returns the kinds the given member has reacted with on a target.
|
|
688
|
+
* Used by the site UI to render the like button as toggled-on.
|
|
689
|
+
*/
|
|
690
|
+
declare function listMemberReactions(targetType: string, targetId: string, memberId: string): Promise<string[]>;
|
|
691
|
+
/**
|
|
692
|
+
* Internal helper — assert that the target exists for the given kind.
|
|
693
|
+
* Today only `comment` is supported. The polymorphic shape leaves
|
|
694
|
+
* room for `thread` / `reply` once a thread schema lands; the forum
|
|
695
|
+
* plugin shipped without one (it reuses `np_comments` under the
|
|
696
|
+
* `discussions` collection), so widening this surface is on hold
|
|
697
|
+
* until a separate threads design.
|
|
698
|
+
*/
|
|
699
|
+
declare function assertReactableExists(targetType: string, targetId: string): Promise<void>;
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Follow graph service. v1 supports `member` follows; `thread` and
|
|
703
|
+
* `tag` lands when those subjects exist. Self-follow is rejected so
|
|
704
|
+
* the recommended-follows / "people you follow" reads don't have to
|
|
705
|
+
* special-case it.
|
|
706
|
+
*/
|
|
707
|
+
declare const SUPPORTED_TARGETS$1: readonly ["member", "thread", "tag"];
|
|
708
|
+
type FollowTarget = (typeof SUPPORTED_TARGETS$1)[number];
|
|
709
|
+
interface NpFollowRow {
|
|
710
|
+
id: string;
|
|
711
|
+
followerId: string;
|
|
712
|
+
targetType: string;
|
|
713
|
+
targetId: string;
|
|
714
|
+
createdAt: Date;
|
|
715
|
+
}
|
|
716
|
+
interface NpFollowInput {
|
|
717
|
+
followerId: string;
|
|
718
|
+
targetType: FollowTarget;
|
|
719
|
+
targetId: string;
|
|
720
|
+
}
|
|
721
|
+
declare function follow(input: NpFollowInput): Promise<NpFollowRow>;
|
|
722
|
+
declare function unfollow(input: NpFollowInput): Promise<void>;
|
|
723
|
+
declare function isFollowing(input: NpFollowInput): Promise<boolean>;
|
|
724
|
+
/**
|
|
725
|
+
* "Who am I following?" — paged. Used by the site UI to populate a
|
|
726
|
+
* member's profile or settings page.
|
|
727
|
+
*/
|
|
728
|
+
declare function listFollowing(followerId: string, options?: {
|
|
729
|
+
targetType?: FollowTarget;
|
|
730
|
+
limit?: number;
|
|
731
|
+
offset?: number;
|
|
732
|
+
}): Promise<NpFollowRow[]>;
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Per-member notification inbox. v1 is synchronous: every event that
|
|
736
|
+
* generates a notification writes a row immediately. The inbox is
|
|
737
|
+
* in-app only — email fan-out and per-member frequency preferences
|
|
738
|
+
* are out of scope for the shipped roadmap.
|
|
739
|
+
*
|
|
740
|
+
* `kind` is a free-form string. The current vocabulary:
|
|
741
|
+
* - `comment.reply` — your comment got a reply
|
|
742
|
+
* - `reaction.received` — someone reacted to your content
|
|
743
|
+
* - `follow.received` — someone followed you
|
|
744
|
+
* Plugins can write their own kinds; the recipient UI fans them out
|
|
745
|
+
* to whichever rendering it knows.
|
|
746
|
+
*/
|
|
747
|
+
interface NpNotificationRow {
|
|
748
|
+
id: string;
|
|
749
|
+
memberId: string;
|
|
750
|
+
kind: string;
|
|
751
|
+
payload: Record<string, unknown>;
|
|
752
|
+
readAt: Date | null;
|
|
753
|
+
createdAt: Date;
|
|
754
|
+
}
|
|
755
|
+
interface CreateNotificationInput {
|
|
756
|
+
/** The recipient — whose inbox this lands in. */
|
|
757
|
+
memberId: string;
|
|
758
|
+
kind: string;
|
|
759
|
+
payload?: Record<string, unknown>;
|
|
760
|
+
/**
|
|
761
|
+
* Phase 16.1 — the member whose action triggered the
|
|
762
|
+
* notification (e.g. the comment author, the reactor, the
|
|
763
|
+
* follower). When set, the recipient's mute list is
|
|
764
|
+
* consulted: if the recipient has muted the actor, the
|
|
765
|
+
* notification is silently dropped. Returns `null` from
|
|
766
|
+
* the call site.
|
|
767
|
+
*
|
|
768
|
+
* Optional because some kinds are actor-less (system
|
|
769
|
+
* notices, scheduled reminders).
|
|
770
|
+
*/
|
|
771
|
+
actorMemberId?: string | null;
|
|
772
|
+
}
|
|
773
|
+
declare function createNotification(input: CreateNotificationInput): Promise<NpNotificationRow | null>;
|
|
774
|
+
interface ListNotificationsOptions {
|
|
775
|
+
/** Default 50, max 200. */
|
|
776
|
+
limit?: number;
|
|
777
|
+
/** Default 0. */
|
|
778
|
+
offset?: number;
|
|
779
|
+
/** When true, returns only unread. */
|
|
780
|
+
unreadOnly?: boolean;
|
|
781
|
+
}
|
|
782
|
+
interface NpNotificationListResult {
|
|
783
|
+
notifications: NpNotificationRow[];
|
|
784
|
+
totalDocs: number;
|
|
785
|
+
unread: number;
|
|
786
|
+
}
|
|
787
|
+
declare function listNotifications(memberId: string, options?: ListNotificationsOptions): Promise<NpNotificationListResult>;
|
|
788
|
+
declare function unreadNotificationCount(memberId: string): Promise<number>;
|
|
789
|
+
interface MarkReadInput {
|
|
790
|
+
memberId: string;
|
|
791
|
+
notificationIds: string[];
|
|
792
|
+
}
|
|
793
|
+
declare function markNotificationsRead(input: MarkReadInput): Promise<number>;
|
|
794
|
+
declare function markAllNotificationsRead(memberId: string): Promise<number>;
|
|
795
|
+
/**
|
|
796
|
+
* Internal sanity check used by the API: throws when one principal
|
|
797
|
+
* tries to read another member's notification. Centralised here
|
|
798
|
+
* because every per-id route gets the same rule.
|
|
799
|
+
*/
|
|
800
|
+
declare function assertOwnsNotification(memberId: string, notificationId: string): Promise<void>;
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Unified permission check. Staff routes pass `{ kind: "staff", user }`;
|
|
804
|
+
* member routes pass `{ kind: "member", memberId }`. Staff with
|
|
805
|
+
* `admin`, `editor`, or `moderator` role short-circuit to allow all
|
|
806
|
+
* community-mod actions — they're trusted by virtue of being CMS
|
|
807
|
+
* staff. Other staff roles (author, viewer) and members fall through
|
|
808
|
+
* to the member-side resolver, which checks role grants in
|
|
809
|
+
* `np_member_roles`.
|
|
810
|
+
*
|
|
811
|
+
* `edit-own` / `delete-own` actions still require ownership even for
|
|
812
|
+
* staff — the API layer should already check ownership for self-only
|
|
813
|
+
* routes, but the ownership rule here is belt-and-braces.
|
|
814
|
+
*/
|
|
815
|
+
type Principal = NpPrincipal;
|
|
816
|
+
declare function principalCan(principal: Principal, action: MemberAction, target: MemberCanTarget): Promise<boolean>;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Append-only moderation audit log. Every hide / restore / ban /
|
|
820
|
+
* role-grant write goes through here so admins can later answer
|
|
821
|
+
* "who took this action and when?" without diffing application logs.
|
|
822
|
+
*
|
|
823
|
+
* Writes are best-effort: a failed audit insert MUST NOT prevent the
|
|
824
|
+
* underlying mod action from succeeding (logged via the observability
|
|
825
|
+
* hooks instead). Reads are paginated and indexed by target.
|
|
826
|
+
*/
|
|
827
|
+
type AuditActorKind = "staff" | "member" | "system";
|
|
828
|
+
interface AuditActor {
|
|
829
|
+
kind: AuditActorKind;
|
|
830
|
+
/** Set only for `kind: "staff"`. */
|
|
831
|
+
userId?: string;
|
|
832
|
+
/** Set only for `kind: "member"`. */
|
|
833
|
+
memberId?: string;
|
|
834
|
+
}
|
|
835
|
+
interface RecordAuditEventInput {
|
|
836
|
+
actor: AuditActor;
|
|
837
|
+
action: string;
|
|
838
|
+
targetType?: string;
|
|
839
|
+
targetId?: string;
|
|
840
|
+
payload?: Record<string, unknown>;
|
|
841
|
+
/**
|
|
842
|
+
* Phase 17 — site this event belongs to. When omitted the
|
|
843
|
+
* writer reads `getCurrentSiteId()` so request-driven calls
|
|
844
|
+
* automatically scope to the resolving tenant. Pass `null`
|
|
845
|
+
* explicitly to record an unscoped event (super-admin
|
|
846
|
+
* cross-site action, background job).
|
|
847
|
+
*/
|
|
848
|
+
siteId?: string | null;
|
|
849
|
+
}
|
|
850
|
+
interface AuditEventRow {
|
|
851
|
+
id: string;
|
|
852
|
+
actorKind: AuditActorKind;
|
|
853
|
+
actorUserId: string | null;
|
|
854
|
+
actorMemberId: string | null;
|
|
855
|
+
action: string;
|
|
856
|
+
targetType: string | null;
|
|
857
|
+
targetId: string | null;
|
|
858
|
+
payload: Record<string, unknown>;
|
|
859
|
+
siteId: string | null;
|
|
860
|
+
createdAt: Date;
|
|
861
|
+
}
|
|
862
|
+
declare function recordAuditEvent(input: RecordAuditEventInput): Promise<void>;
|
|
863
|
+
interface ListAuditOptions {
|
|
864
|
+
/** Filter to audit events targeting one specific row. */
|
|
865
|
+
targetType?: string;
|
|
866
|
+
targetId?: string;
|
|
867
|
+
/** Filter to events caused by a specific actor. */
|
|
868
|
+
actorUserId?: string;
|
|
869
|
+
actorMemberId?: string;
|
|
870
|
+
/**
|
|
871
|
+
* Filter to events whose `action` matches. Common operational
|
|
872
|
+
* query: "show every ban issued this week" →
|
|
873
|
+
* `action="member.ban.issue"` plus `since`.
|
|
874
|
+
*/
|
|
875
|
+
action?: string;
|
|
876
|
+
/** Lower-bound `created_at` (inclusive). */
|
|
877
|
+
since?: Date;
|
|
878
|
+
/** Upper-bound `created_at` (exclusive). */
|
|
879
|
+
until?: Date;
|
|
880
|
+
/**
|
|
881
|
+
* Phase 17 — site filter. `undefined` means "use current
|
|
882
|
+
* request's site" (the typical admin-page query). Pass an
|
|
883
|
+
* explicit string to view another site's audit log
|
|
884
|
+
* (super-admin cross-site triage). Pass `null` to skip the
|
|
885
|
+
* filter entirely (every site's events).
|
|
886
|
+
*/
|
|
887
|
+
siteId?: string | null;
|
|
888
|
+
limit?: number;
|
|
889
|
+
offset?: number;
|
|
890
|
+
}
|
|
891
|
+
declare function listAuditEvents(options?: ListAuditOptions): Promise<{
|
|
892
|
+
events: AuditEventRow[];
|
|
893
|
+
totalDocs: number;
|
|
894
|
+
}>;
|
|
895
|
+
|
|
896
|
+
declare const SUPPORTED_TARGETS: readonly ["comment", "thread", "reply", "member"];
|
|
897
|
+
type ReportTarget = (typeof SUPPORTED_TARGETS)[number];
|
|
898
|
+
interface NpReportRow {
|
|
899
|
+
id: string;
|
|
900
|
+
reporterId: string;
|
|
901
|
+
targetType: string;
|
|
902
|
+
targetId: string;
|
|
903
|
+
reason: string;
|
|
904
|
+
resolvedAt: Date | null;
|
|
905
|
+
resolvedByUserId: string | null;
|
|
906
|
+
resolvedByMemberId: string | null;
|
|
907
|
+
resolution: string | null;
|
|
908
|
+
siteId: string;
|
|
909
|
+
createdAt: Date;
|
|
910
|
+
}
|
|
911
|
+
interface FileReportInput {
|
|
912
|
+
reporterId: string;
|
|
913
|
+
targetType: ReportTarget;
|
|
914
|
+
targetId: string;
|
|
915
|
+
reason: string;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Members file reports against a piece of community content. The
|
|
919
|
+
* reason is free-form; mods triage it via `listReports` and
|
|
920
|
+
* `resolveReport`.
|
|
921
|
+
*/
|
|
922
|
+
declare function fileReport(input: FileReportInput): Promise<NpReportRow>;
|
|
923
|
+
interface ListReportsOptions {
|
|
924
|
+
/** Default: only unresolved. Pass `"all"` to include resolved. */
|
|
925
|
+
status?: "unresolved" | "resolved" | "all";
|
|
926
|
+
/** Filter to a specific target type. */
|
|
927
|
+
targetType?: string;
|
|
928
|
+
/**
|
|
929
|
+
* Phase 18 — site scope. `undefined` (default) → use the
|
|
930
|
+
* request resolver's site. Pass an explicit string to view
|
|
931
|
+
* another tenant's queue (super-admin) or `null` to skip
|
|
932
|
+
* the filter entirely.
|
|
933
|
+
*/
|
|
934
|
+
siteId?: string | null;
|
|
935
|
+
limit?: number;
|
|
936
|
+
offset?: number;
|
|
937
|
+
}
|
|
938
|
+
interface ListReportsResult {
|
|
939
|
+
reports: NpReportRow[];
|
|
940
|
+
totalDocs: number;
|
|
941
|
+
}
|
|
942
|
+
declare function listReports(options?: ListReportsOptions): Promise<ListReportsResult>;
|
|
943
|
+
interface ResolveReportInput {
|
|
944
|
+
reportId: string;
|
|
945
|
+
/** Free-form short label: e.g. `"hidden"`, `"banned"`, `"dismissed"`. */
|
|
946
|
+
resolution: string;
|
|
947
|
+
actor: Principal;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Marks a report resolved. Caller is responsible for taking the
|
|
951
|
+
* actual moderation action (hide, ban, etc.) — this only flips the
|
|
952
|
+
* report row and writes an audit entry.
|
|
953
|
+
*/
|
|
954
|
+
declare function resolveReport(input: ResolveReportInput): Promise<NpReportRow>;
|
|
955
|
+
/** Cheap "is anything in the queue?" probe for the admin badge. */
|
|
956
|
+
declare function unresolvedReportCount(): Promise<number>;
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Ban service. The 9.1a schema already had `np_bans`; this layer
|
|
960
|
+
* adds the issue / list / revoke flow plus audit logging.
|
|
961
|
+
*
|
|
962
|
+
* Scope rules in v1:
|
|
963
|
+
* - `site` — issuer must be staff (admin / editor / moderator).
|
|
964
|
+
* - `category`, `collection` — issuer must be a community-mod or
|
|
965
|
+
* a staff mod. We don't currently verify the issuer holds the
|
|
966
|
+
* matching scoped grant; the API layer is responsible for that
|
|
967
|
+
* check via `principalCan` before calling `issueBan`. The audit
|
|
968
|
+
* log records the issuer either way for forensic review.
|
|
969
|
+
*/
|
|
970
|
+
type BanScope = "site" | "category" | "collection";
|
|
971
|
+
type BanKind = "temporary" | "permanent";
|
|
972
|
+
interface NpBanRow {
|
|
973
|
+
id: string;
|
|
974
|
+
memberId: string;
|
|
975
|
+
scopeType: BanScope;
|
|
976
|
+
scopeId: string | null;
|
|
977
|
+
kind: BanKind;
|
|
978
|
+
expiresAt: Date | null;
|
|
979
|
+
reason: string | null;
|
|
980
|
+
byUserId: string | null;
|
|
981
|
+
byMemberId: string | null;
|
|
982
|
+
/** Tenant the ban belongs to. Phase 18 added the column; the type was incomplete until #364. */
|
|
983
|
+
siteId: string;
|
|
984
|
+
createdAt: Date;
|
|
985
|
+
}
|
|
986
|
+
interface IssueBanInput {
|
|
987
|
+
memberId: string;
|
|
988
|
+
scopeType: BanScope;
|
|
989
|
+
scopeId?: string | null;
|
|
990
|
+
kind: BanKind;
|
|
991
|
+
/** Required when `kind === "temporary"`. */
|
|
992
|
+
expiresAt?: Date | null;
|
|
993
|
+
reason?: string | null;
|
|
994
|
+
actor: Principal;
|
|
995
|
+
}
|
|
996
|
+
declare function issueBan(input: IssueBanInput): Promise<NpBanRow>;
|
|
997
|
+
declare function listBansForMember(memberId: string): Promise<NpBanRow[]>;
|
|
998
|
+
interface RevokeBanInput {
|
|
999
|
+
banId: string;
|
|
1000
|
+
actor: Principal;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* "Revoking" a ban means deleting the row outright. The audit log
|
|
1004
|
+
* preserves the history (issue + revoke each leave an entry), so we
|
|
1005
|
+
* don't need a soft-delete column.
|
|
1006
|
+
*/
|
|
1007
|
+
declare function revokeBan(input: RevokeBanInput): Promise<void>;
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Member role grant service. Wraps `np_member_roles` writes with
|
|
1011
|
+
* registry validation, audit logging, and friendly errors for the
|
|
1012
|
+
* `(member, role, scope_type, scope_id)` unique conflict that
|
|
1013
|
+
* Postgres surfaces as a 23505 raw error.
|
|
1014
|
+
*
|
|
1015
|
+
* Read path (`memberCan` in `community/can.ts`) already filters by
|
|
1016
|
+
* `expires_at IS NULL OR expires_at > now`, so an expired grant
|
|
1017
|
+
* disappears from the resolver automatically — `listMemberRoleGrants`
|
|
1018
|
+
* mirrors that filter so the admin UI doesn't show ghost rows.
|
|
1019
|
+
*
|
|
1020
|
+
* Permission gating is the API layer's job (today: admin-only). The
|
|
1021
|
+
* core helpers don't re-check, so a privileged programmatic caller
|
|
1022
|
+
* can grant on behalf of any actor.
|
|
1023
|
+
*/
|
|
1024
|
+
interface NpMemberRoleGrantRow {
|
|
1025
|
+
id: string;
|
|
1026
|
+
memberId: string;
|
|
1027
|
+
role: string;
|
|
1028
|
+
scopeType: CommunityScope;
|
|
1029
|
+
scopeId: string | null;
|
|
1030
|
+
grantedBy: string | null;
|
|
1031
|
+
grantedAt: Date;
|
|
1032
|
+
expiresAt: Date | null;
|
|
1033
|
+
/** Tenant the grant belongs to. Phase 18 added the column; the type was incomplete until #364. */
|
|
1034
|
+
siteId: string;
|
|
1035
|
+
}
|
|
1036
|
+
interface GrantMemberRoleInput {
|
|
1037
|
+
memberId: string;
|
|
1038
|
+
role: string;
|
|
1039
|
+
scopeType: CommunityScope;
|
|
1040
|
+
/** Required when `scopeType !== "site"`; ignored otherwise. */
|
|
1041
|
+
scopeId?: string | null;
|
|
1042
|
+
/** Optional time-boxed grant. `null` = perpetual. */
|
|
1043
|
+
expiresAt?: Date | null;
|
|
1044
|
+
/** Staff user issuing the grant — recorded on the row + audit. */
|
|
1045
|
+
grantedByUserId: string;
|
|
1046
|
+
}
|
|
1047
|
+
declare function grantMemberRole(input: GrantMemberRoleInput): Promise<NpMemberRoleGrantRow>;
|
|
1048
|
+
/**
|
|
1049
|
+
* List currently-active grants for a member. Mirrors the
|
|
1050
|
+
* `memberCan` filter so expired rows are hidden.
|
|
1051
|
+
*/
|
|
1052
|
+
declare function listMemberRoleGrants(memberId: string): Promise<NpMemberRoleGrantRow[]>;
|
|
1053
|
+
interface RevokeMemberRoleInput {
|
|
1054
|
+
grantId: string;
|
|
1055
|
+
revokedByUserId: string;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Revoke = hard delete. Audit trail preserves history. Mirrors
|
|
1059
|
+
* `revokeBan`'s semantic — the grant either exists and counts, or
|
|
1060
|
+
* it doesn't; soft-deleted rows would only confuse the resolver.
|
|
1061
|
+
*/
|
|
1062
|
+
declare function revokeMemberRole(input: RevokeMemberRoleInput): Promise<void>;
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Aggregate result of a member content purge. Comments are
|
|
1066
|
+
* counted as deleted regardless of soft-vs-hard semantic (the
|
|
1067
|
+
* underlying `staffDeleteComment` is a soft delete that wipes the
|
|
1068
|
+
* body). Documents are reported per-collection because the staff
|
|
1069
|
+
* UI typically wants to call out "X discussions, Y posts" rather
|
|
1070
|
+
* than a flat total. Media has a `skipped` bucket because
|
|
1071
|
+
* `deleteMedia` refuses rows that are still referenced from a
|
|
1072
|
+
* doc (`np_media_refs`) — those need to be unlinked first; the
|
|
1073
|
+
* mod can re-run after the reference is gone.
|
|
1074
|
+
*/
|
|
1075
|
+
interface NpMemberPurgeResult {
|
|
1076
|
+
comments: number;
|
|
1077
|
+
documents: Record<string, number>;
|
|
1078
|
+
media: {
|
|
1079
|
+
deleted: number;
|
|
1080
|
+
skipped: number;
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Wipes everything a single member authored: comments, top-level
|
|
1085
|
+
* docs in any collection that opted into `community.memberWrite`,
|
|
1086
|
+
* and uploaded media. Used by the moderation tooling to clean up
|
|
1087
|
+
* after a spam wave or a banned account.
|
|
1088
|
+
*
|
|
1089
|
+
* Failure mode is idempotent rather than atomic — if a transient
|
|
1090
|
+
* error interrupts the purge mid-way, the operator re-runs and
|
|
1091
|
+
* the helper skips items already removed (it always re-queries
|
|
1092
|
+
* the live state before each loop). The aggregate audit event
|
|
1093
|
+
* records the actual counts performed, not the intent.
|
|
1094
|
+
*
|
|
1095
|
+
* Out of scope (deliberately): banning, identity revocation,
|
|
1096
|
+
* follower / following links, reputation reset. Each of those is
|
|
1097
|
+
* a separate moderation action with its own UI; bundling them
|
|
1098
|
+
* into a single "purge" hides intent.
|
|
1099
|
+
*/
|
|
1100
|
+
declare function purgeMemberContent(memberId: string, staffUser: NpAuthUser): Promise<NpMemberPurgeResult>;
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Phase 16.1 — member-to-member mute. One-directional: A
|
|
1104
|
+
* muting B hides B from A's surfaces (comments, notification
|
|
1105
|
+
* fan-out). B keeps posting normally.
|
|
1106
|
+
*
|
|
1107
|
+
* Distinct from `np_bans` (staff-issued, global write block).
|
|
1108
|
+
* Mutes are always self-service: a member calls these helpers
|
|
1109
|
+
* for their own mute list, never for someone else's.
|
|
1110
|
+
*/
|
|
1111
|
+
interface NpMemberMuteRow {
|
|
1112
|
+
memberId: string;
|
|
1113
|
+
targetId: string;
|
|
1114
|
+
createdAt: Date;
|
|
1115
|
+
}
|
|
1116
|
+
interface MuteMemberInput {
|
|
1117
|
+
/** The muter — the current member taking the action. */
|
|
1118
|
+
memberId: string;
|
|
1119
|
+
/** The muted — whose content should disappear. */
|
|
1120
|
+
targetId: string;
|
|
1121
|
+
}
|
|
1122
|
+
declare function muteMember(input: MuteMemberInput): Promise<void>;
|
|
1123
|
+
declare function unmuteMember(input: MuteMemberInput): Promise<boolean>;
|
|
1124
|
+
/**
|
|
1125
|
+
* `true` when `memberId` has muted `targetId` on the current
|
|
1126
|
+
* site. Used by comment listing + notification fan-out to
|
|
1127
|
+
* filter views and skip alerts.
|
|
1128
|
+
*/
|
|
1129
|
+
declare function isMuted(input: MuteMemberInput): Promise<boolean>;
|
|
1130
|
+
/**
|
|
1131
|
+
* Returns the set of `targetId`s the given member has muted on
|
|
1132
|
+
* the current site. Used to filter listComments output in one
|
|
1133
|
+
* DB round-trip rather than `isMuted()` per row.
|
|
1134
|
+
*/
|
|
1135
|
+
declare function getMutedTargetIds(memberId: string): Promise<Set<string>>;
|
|
1136
|
+
interface NpMemberMuteSummary {
|
|
1137
|
+
targetId: string;
|
|
1138
|
+
handle: string;
|
|
1139
|
+
displayName: string;
|
|
1140
|
+
createdAt: string;
|
|
1141
|
+
}
|
|
1142
|
+
interface ListMutesOptions {
|
|
1143
|
+
/** Default 50, max 200. */
|
|
1144
|
+
limit?: number;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Surfaces the muter's list with the muted member's display
|
|
1148
|
+
* info joined in, so the settings UI doesn't have to round-
|
|
1149
|
+
* trip through `/api/members/[handle]` for every row.
|
|
1150
|
+
*/
|
|
1151
|
+
declare function listMutes(memberId: string, options?: ListMutesOptions): Promise<NpMemberMuteSummary[]>;
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Phase 16.2 — @mention extraction + notification fan-out.
|
|
1155
|
+
*
|
|
1156
|
+
* The mention vocabulary mirrors the handle constraint enforced
|
|
1157
|
+
* during registration (`/^[a-z0-9][a-z0-9_-]{2,29}$/`). The matcher
|
|
1158
|
+
* uses a negative lookbehind so `email@host.com` doesn't trigger a
|
|
1159
|
+
* mention, plus a negative lookahead so `@alice-` (handle followed
|
|
1160
|
+
* by a hyphen that's not part of the handle) is rejected — handles
|
|
1161
|
+
* end at non-handle characters, never mid-symbol.
|
|
1162
|
+
*
|
|
1163
|
+
* Fan-out semantics:
|
|
1164
|
+
* - Self-mentions are skipped (the author already knows).
|
|
1165
|
+
* - Caller-supplied `exclude` set lets the comment write path
|
|
1166
|
+
* skip the parent author so they don't get both `comment.reply`
|
|
1167
|
+
* AND `comment.mention`.
|
|
1168
|
+
* - Caller-supplied `previousHandles` lets the edit path only
|
|
1169
|
+
* notify newly-added mentions (otherwise toggling a single
|
|
1170
|
+
* other word in a comment would re-notify everyone).
|
|
1171
|
+
* - Inactive / banned / deleted members are filtered out at
|
|
1172
|
+
* resolve time.
|
|
1173
|
+
* - Mute is enforced inside `createNotification` (the
|
|
1174
|
+
* recipient's mute list drops actor-keyed notifications).
|
|
1175
|
+
*/
|
|
1176
|
+
/** Source-of-truth handle pattern, kept in sync with `apps/web` register routes. */
|
|
1177
|
+
declare const MENTION_HANDLE_RE: RegExp;
|
|
1178
|
+
interface NpMentionTarget {
|
|
1179
|
+
id: string;
|
|
1180
|
+
handle: string;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Extract unique mention handles from plain text or markdown source.
|
|
1184
|
+
* Order is preserved (first appearance wins) so a UI that wants to
|
|
1185
|
+
* display "you mentioned @alice and @bob" gets the same order as
|
|
1186
|
+
* the body text.
|
|
1187
|
+
*/
|
|
1188
|
+
declare function extractMentionHandles(source: string): string[];
|
|
1189
|
+
/**
|
|
1190
|
+
* Walk a Lexical-shaped rich-text payload, concatenate its text
|
|
1191
|
+
* nodes, and run the mention extractor over the joined result.
|
|
1192
|
+
* Mirrors the search-index walker (`collections/search.ts`) so a
|
|
1193
|
+
* mention split across two adjacent text spans (e.g. `@` and
|
|
1194
|
+
* `alice` in different runs because of formatting toggles) still
|
|
1195
|
+
* resolves correctly — text nodes are joined without separators.
|
|
1196
|
+
*/
|
|
1197
|
+
declare function extractMentionHandlesFromRichText(content: unknown): string[];
|
|
1198
|
+
/**
|
|
1199
|
+
* Scan a collection-document data payload (the same shape passed
|
|
1200
|
+
* to `createMemberDocument` / `updateMemberDocument`) and pull
|
|
1201
|
+
* out every mention handle it contains. String values are scanned
|
|
1202
|
+
* with the markdown extractor; object values shaped like Lexical
|
|
1203
|
+
* rich text (`{ root: { children: [...] } }`) are walked. Other
|
|
1204
|
+
* values are ignored.
|
|
1205
|
+
*
|
|
1206
|
+
* Field names are not assumed: any string or rich-text field
|
|
1207
|
+
* contributes. The mention pattern is anchored to `@<handle>`
|
|
1208
|
+
* with handle-shape constraints, so unrelated string fields
|
|
1209
|
+
* (`category: "news"`) won't trigger false positives.
|
|
1210
|
+
*/
|
|
1211
|
+
declare function extractMentionHandlesFromDocData(data: Record<string, unknown>): string[];
|
|
1212
|
+
/**
|
|
1213
|
+
* Resolve handles to active member ids. Inactive / banned /
|
|
1214
|
+
* deleted members are filtered out so a mention of an account
|
|
1215
|
+
* the site no longer wants to notify is silently dropped (rather
|
|
1216
|
+
* than raising an error to the writer — the writer can't tell the
|
|
1217
|
+
* difference between "typo" and "account closed", and either way
|
|
1218
|
+
* the right behaviour is "no notification").
|
|
1219
|
+
*
|
|
1220
|
+
* Lookups are case-insensitive on the handle (the storage column
|
|
1221
|
+
* stores the canonical lowercased form).
|
|
1222
|
+
*/
|
|
1223
|
+
declare function resolveMentionedMembers(handles: string[]): Promise<NpMentionTarget[]>;
|
|
1224
|
+
interface FanOutMentionsInput {
|
|
1225
|
+
/** The author whose write triggered the fan-out. Self-mentions are skipped. */
|
|
1226
|
+
actorMemberId: string;
|
|
1227
|
+
/** Notification `kind` (e.g. `"comment.mention"`, `"discussion.mention"`). */
|
|
1228
|
+
kind: string;
|
|
1229
|
+
/**
|
|
1230
|
+
* Plain text or markdown to scan. Either `source` or `content`
|
|
1231
|
+
* (or both) must be provided; if both are set the handles are
|
|
1232
|
+
* unioned.
|
|
1233
|
+
*/
|
|
1234
|
+
source?: string;
|
|
1235
|
+
/** Lexical-shaped rich-text JSON to scan. */
|
|
1236
|
+
content?: unknown;
|
|
1237
|
+
/**
|
|
1238
|
+
* Collection-document data payload to scan. All string +
|
|
1239
|
+
* rich-text fields contribute. Useful for the
|
|
1240
|
+
* `createMemberDocument` / `updateMemberDocument` paths.
|
|
1241
|
+
*/
|
|
1242
|
+
data?: Record<string, unknown>;
|
|
1243
|
+
/**
|
|
1244
|
+
* Recipients that already received a notification for this same
|
|
1245
|
+
* event (e.g. the parent author got `comment.reply`). They are
|
|
1246
|
+
* skipped to avoid the "two pings for one comment" pattern.
|
|
1247
|
+
*/
|
|
1248
|
+
exclude?: ReadonlySet<string>;
|
|
1249
|
+
/** Merged into the notification payload. `mentionedMemberId` is added automatically. */
|
|
1250
|
+
payload?: Record<string, unknown>;
|
|
1251
|
+
/**
|
|
1252
|
+
* Edit path: handles that were present in the prior revision
|
|
1253
|
+
* are skipped so toggling unrelated words doesn't re-notify
|
|
1254
|
+
* everyone already mentioned.
|
|
1255
|
+
*/
|
|
1256
|
+
previousHandles?: ReadonlySet<string>;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Fan-out mention notifications. Returns the number of
|
|
1260
|
+
* notifications actually inserted (mute / inactive / self / dedup
|
|
1261
|
+
* exclusions all reduce the count).
|
|
1262
|
+
*/
|
|
1263
|
+
declare function fanOutMentionNotifications(input: FanOutMentionsInput): Promise<number>;
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Phase 16.3 — per-member notification preferences.
|
|
1267
|
+
*
|
|
1268
|
+
* The persisted shape is a JSONB blob on `np_members.notification_prefs`
|
|
1269
|
+
* so adding fields (digest cadence in 16.4, channel toggles later)
|
|
1270
|
+
* stays a typescript-only change. Today we honor:
|
|
1271
|
+
*
|
|
1272
|
+
* - `disabled: string[]` — kinds the member opted out of. The
|
|
1273
|
+
* `createNotification` gate consults this and silently drops
|
|
1274
|
+
* the row. Default empty (= every kind enabled).
|
|
1275
|
+
*
|
|
1276
|
+
* The vocabulary of `kinds` is defined here so the UI has a single
|
|
1277
|
+
* source of truth — settings page renders a toggle for each entry,
|
|
1278
|
+
* and the API only accepts kinds that appear in the list (so a
|
|
1279
|
+
* forged client can't disable arbitrary strings to bloat the JSONB).
|
|
1280
|
+
*/
|
|
1281
|
+
interface NpNotificationKindMeta {
|
|
1282
|
+
kind: string;
|
|
1283
|
+
/** Short human label. */
|
|
1284
|
+
label: string;
|
|
1285
|
+
/** Description rendered next to the toggle. */
|
|
1286
|
+
description: string;
|
|
1287
|
+
}
|
|
1288
|
+
/** Plugin-extensible registration. Idempotent on `kind`. */
|
|
1289
|
+
declare function registerNotificationKind(meta: NpNotificationKindMeta): void;
|
|
1290
|
+
/** Returns the union of builtin + plugin-registered kinds. */
|
|
1291
|
+
declare function listNotificationKinds(): NpNotificationKindMeta[];
|
|
1292
|
+
type NpDigestCadence = "off" | "daily" | "weekly";
|
|
1293
|
+
interface NpNotificationPrefs {
|
|
1294
|
+
/** Kinds the member opted out of. Empty / missing = all kinds enabled. */
|
|
1295
|
+
disabled: string[];
|
|
1296
|
+
/**
|
|
1297
|
+
* Phase 16.4 — email digest cadence. `off` (default) disables
|
|
1298
|
+
* the digest. `daily` and `weekly` opt the member into a
|
|
1299
|
+
* batched email of unread notifications, scheduled by the
|
|
1300
|
+
* `notifications:sendDigest` recurring job.
|
|
1301
|
+
*/
|
|
1302
|
+
digest: NpDigestCadence;
|
|
1303
|
+
/**
|
|
1304
|
+
* Set when the digest sweep last sent an email to this member.
|
|
1305
|
+
* Used to scope each digest to "unread since the last send" so
|
|
1306
|
+
* members aren't repeatedly emailed about the same row. Stored
|
|
1307
|
+
* as ISO-8601 string in the JSONB blob; `null` for accounts
|
|
1308
|
+
* that have never received a digest.
|
|
1309
|
+
*
|
|
1310
|
+
* Issue #218 — superseded by `lastDigestAtBySite` once a member
|
|
1311
|
+
* receives a digest under the per-site fan-out path. The legacy
|
|
1312
|
+
* field is preserved for forward-compat reads (single-site
|
|
1313
|
+
* deploys still see + write it via the fallback chain) and as
|
|
1314
|
+
* a "any digest, ever?" marker for analytics.
|
|
1315
|
+
*/
|
|
1316
|
+
lastDigestAt: string | null;
|
|
1317
|
+
/**
|
|
1318
|
+
* Issue #218 — per-(site, cadence) timestamp map. Replaces the
|
|
1319
|
+
* single `lastDigestAt` for multi-site deployments. Empty when
|
|
1320
|
+
* the member has never received a digest under the site-scoped
|
|
1321
|
+
* sweep.
|
|
1322
|
+
*/
|
|
1323
|
+
lastDigestAtBySite: Record<string, Partial<Record<NpDigestCadence, string>>>;
|
|
1324
|
+
}
|
|
1325
|
+
declare function getMemberNotificationPrefs(memberId: string): Promise<NpNotificationPrefs>;
|
|
1326
|
+
interface SetMemberNotificationPrefsInput {
|
|
1327
|
+
memberId: string;
|
|
1328
|
+
/**
|
|
1329
|
+
* Replacement deny-list. Only kinds listed in
|
|
1330
|
+
* `listNotificationKinds()` are accepted; unknown strings
|
|
1331
|
+
* raise NpValidationError so a forged client can't bloat the
|
|
1332
|
+
* JSONB or hide future framework kinds via a stale list.
|
|
1333
|
+
* Optional — when omitted the existing list is preserved.
|
|
1334
|
+
*/
|
|
1335
|
+
disabled?: string[];
|
|
1336
|
+
/**
|
|
1337
|
+
* Phase 16.4 — email digest cadence. Optional; when omitted
|
|
1338
|
+
* the existing setting is preserved. `off` clears the
|
|
1339
|
+
* member's enrollment.
|
|
1340
|
+
*/
|
|
1341
|
+
digest?: NpDigestCadence;
|
|
1342
|
+
}
|
|
1343
|
+
declare function setMemberNotificationPrefs(input: SetMemberNotificationPrefsInput): Promise<NpNotificationPrefs>;
|
|
1344
|
+
/**
|
|
1345
|
+
* Phase 16.4 — bookkeeping helper called by the digest sweep
|
|
1346
|
+
* after a successful email send. Stamps `lastDigestAt` so the
|
|
1347
|
+
* next run scopes its query to the correct window. Read-merge
|
|
1348
|
+
* to preserve other JSONB keys.
|
|
1349
|
+
*
|
|
1350
|
+
* Issue #218 — when a `siteId` + `cadence` pair is supplied,
|
|
1351
|
+
* the per-site / per-cadence map is updated so the next sweep
|
|
1352
|
+
* for that tenant scopes to the correct "since" window. The
|
|
1353
|
+
* legacy single `lastDigestAt` field is also stamped for
|
|
1354
|
+
* forward-compat with single-site deploys (and as a "received
|
|
1355
|
+
* any digest, ever?" marker for analytics).
|
|
1356
|
+
*/
|
|
1357
|
+
declare function recordDigestSent(memberId: string, sentAt: Date, scope?: {
|
|
1358
|
+
siteId: string;
|
|
1359
|
+
cadence: NpDigestCadence;
|
|
1360
|
+
}): Promise<void>;
|
|
1361
|
+
/**
|
|
1362
|
+
* Inbox-side gate consulted by `createNotification`. Returns
|
|
1363
|
+
* `false` when the recipient explicitly opted out of `kind`.
|
|
1364
|
+
* Errors fail-open (return `true`) so a transient DB blip
|
|
1365
|
+
* doesn't silently swallow notifications.
|
|
1366
|
+
*/
|
|
1367
|
+
declare function isNotificationKindEnabled(memberId: string, kind: string): Promise<boolean>;
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Phase 16.4 — email digest fan-out. The `notifications:sendDigest`
|
|
1371
|
+
* recurring job calls `runDigestSweep(cadence)` on a daily and a
|
|
1372
|
+
* weekly schedule; the function fetches every active member who
|
|
1373
|
+
* opted into that cadence, builds an inbox summary scoped to "since
|
|
1374
|
+
* last digest" (falling back to the cadence window when the member
|
|
1375
|
+
* has never received one), renders an email through the configured
|
|
1376
|
+
* `NpEmailAdapter`, and stamps `lastDigestAt` on success.
|
|
1377
|
+
*
|
|
1378
|
+
* The job is idempotent enough for production use: a sweep that
|
|
1379
|
+
* runs twice for the same window won't re-email members because
|
|
1380
|
+
* `lastDigestAt` advances on the first send. Failures inside the
|
|
1381
|
+
* loop are logged-and-continued — one stuck member doesn't block
|
|
1382
|
+
* the rest of the sweep.
|
|
1383
|
+
*/
|
|
1384
|
+
interface NpDigestNotificationSummary {
|
|
1385
|
+
id: string;
|
|
1386
|
+
kind: string;
|
|
1387
|
+
payload: Record<string, unknown>;
|
|
1388
|
+
createdAt: Date;
|
|
1389
|
+
}
|
|
1390
|
+
interface NpDigestEmailContent {
|
|
1391
|
+
subject: string;
|
|
1392
|
+
text: string;
|
|
1393
|
+
html: string;
|
|
1394
|
+
}
|
|
1395
|
+
interface BuildDigestEmailInput {
|
|
1396
|
+
member: {
|
|
1397
|
+
displayName: string;
|
|
1398
|
+
handle: string;
|
|
1399
|
+
};
|
|
1400
|
+
notifications: NpDigestNotificationSummary[];
|
|
1401
|
+
cadence: NpDigestCadence;
|
|
1402
|
+
/** Site display name; defaults to "your site" so the noop adapter is still readable. */
|
|
1403
|
+
siteName?: string;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Pure renderer; exposed so plugins / tests can call it without
|
|
1407
|
+
* the DB read path.
|
|
1408
|
+
*/
|
|
1409
|
+
declare function buildDigestEmail(input: BuildDigestEmailInput): NpDigestEmailContent;
|
|
1410
|
+
interface RunDigestSweepInput {
|
|
1411
|
+
cadence: "daily" | "weekly";
|
|
1412
|
+
/** Defaults to `new Date()`. Tests override for determinism. */
|
|
1413
|
+
now?: Date;
|
|
1414
|
+
/** Site name woven into subject + body. Defaults to `"your site"`. */
|
|
1415
|
+
siteName?: string;
|
|
1416
|
+
}
|
|
1417
|
+
interface RunDigestSweepResult {
|
|
1418
|
+
considered: number;
|
|
1419
|
+
sent: number;
|
|
1420
|
+
skipped: number;
|
|
1421
|
+
failed: number;
|
|
1422
|
+
}
|
|
1423
|
+
declare function runDigestSweep(input: RunDigestSweepInput): Promise<RunDigestSweepResult>;
|
|
1424
|
+
|
|
1425
|
+
export { type AuditActor, type AuditActorKind, type AuditEventRow, type BanKind, type BanScope, type BuildDigestEmailInput, type CommentStatus, type CommunityCapability, type CommunityRoleDefinition, type CommunityScope, type CreateNotificationInput, DEFAULT_COMMUNITY_SETTINGS, DEFAULT_REACTION_KINDS, type FanOutMentionsInput, type FileReportInput, type GrantMemberRoleInput, type IssueBanInput, type ListAuditOptions, type ListMutesOptions, type ListNotificationsOptions, type ListReportsOptions, type ListReportsResult, MENTION_HANDLE_RE, type MarkReadInput, type MemberAction, type MemberCanTarget, type MuteMemberInput, type NpBanRow, type NpCommentCreateInput, type NpCommentDeleteInput, type NpCommentHideInput, type NpCommentListOptions, type NpCommentListResult, type NpCommentRestoreInput, type NpCommentRow, type NpCommentSort, type NpCommentUpdateInput, type NpCommunitySettings, type NpDigestCadence, type NpDigestEmailContent, type NpDigestNotificationSummary, type NpFollowInput, type NpFollowRow, type NpMemberMuteRow, type NpMemberMuteSummary, type NpMemberProfile, type NpMemberPurgeResult, type NpMemberRoleGrantRow, type NpMemberUploadQuota, type NpMentionTarget, type NpNotificationKindMeta, type NpNotificationListResult, type NpNotificationPrefs, type NpNotificationRow, type NpProfanityAdapter, type NpProfanityCheckContext, type NpProfanityVerdict, type NpProfanityVerdictKind, type NpReactToInput, type NpReactionRow, type NpReportRow, type NpReputationAdapter, type NpReputationEvent, type NpSpamAdapter, type NpSpamCheckContext, type NpSpamVerdict, type NpSpamVerdictKind, type Principal, type RecordAuditEventInput, type ResolveReportInput, type RevokeBanInput, type RevokeMemberRoleInput, type RunDigestSweepInput, type RunDigestSweepResult, type SetMemberNotificationPrefsInput, addReaction, applyReputation, assertNotBanned, assertOwnsNotification, assertReactableExists, buildDigestEmail, countReactions, createComment, createNotification, deleteComment, extractMentionHandles, extractMentionHandlesFromDocData, extractMentionHandlesFromRichText, fanOutMentionNotifications, fileReport, follow, getCommunityRole, getCommunitySettings, getMemberNotificationPrefs, getMemberProfile, getMemberProfiles, getMutedTargetIds, getProfanityAdapter, getReputationAdapter, getSpamAdapter, grantMemberRole, hideComment, isFollowing, isMuted, isNotificationKindEnabled, issueBan, listAuditEvents, listBansForMember, listComments, listCommunityRoles, listFollowing, listMemberReactions, listMemberRoleGrants, listMutes, listNotificationKinds, listNotifications, listReports, markAllNotificationsRead, markNotificationsRead, memberCan, muteMember, principalCan, purgeMemberContent, recordAuditEvent, recordDigestSent, registerCommunityRole, registerNotificationKind, removeReaction, renderCommentMarkdown, resetCommunityRoles, resetProfanityAdapter, resetReputationAdapter, resetSpamAdapter, resolveMentionedMembers, resolveReport, restoreComment, revokeBan, revokeMemberRole, runDigestSweep, setMemberNotificationPrefs, setProfanityAdapter, setReputationAdapter, setSpamAdapter, staffDeleteComment, staffHideComment, staffRestoreComment, unfollow, unmuteMember, unreadNotificationCount, unresolvedReportCount, updateComment, updateCommunitySettings, validateCommunitySettingsPatch, withMemberWrite };
|