glassframe-protocol 2.1.0 → 2.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/CHANGELOG.md CHANGED
@@ -14,6 +14,50 @@ lives in `src/core/VersionInfo.js` (`VersionInfo.info()` /
14
14
  `VersionInfo.banner()`), so code can read the current version without
15
15
  parsing this file.
16
16
 
17
+ ## [2.2.0] - 2026-08-05
18
+
19
+ Fixes a real bug in message deletion and adds tenure-aware leniency for
20
+ spam detection. No breaking changes.
21
+
22
+ ### Fixed
23
+
24
+ - **Protected/trusted members' messages were being deleted anyway.**
25
+ `BasicSecurityLayer` computed trust and used it to gate the @everyone
26
+ check, but every *other* trigger (message rate, duplicate flood, mention
27
+ spam, scam/phishing/raid language, links) deleted the message
28
+ unconditionally, before `PunishmentEngine` ever got a chance to decide
29
+ the sender shouldn't be punished. A real admin posting several messages
30
+ quickly during a busy conversation, or pasting a couple of links, had
31
+ their message removed even though they were never going to be actioned.
32
+ Deletion is now gated the same way punishment is: never for a HIGH/
33
+ PROTECTED-trust sender.
34
+
35
+ ### Added
36
+
37
+ - **Expanded `roleAnalysis.protectedNames`**: added `co-owner`, `founder`,
38
+ `management`, `manager`, `support`, `helper`, `developer` alongside the
39
+ existing `owner`/`admin`/`administrator`/`moderator`/`mod`/`staff`/
40
+ `discord staff`/`security`.
41
+ - **Established-member leniency** (`RoleAnalyzer.isEstablishedMember()`,
42
+ `roleAnalysis.maturity`, `basicSecurity.establishedMemberLeniency`) - a
43
+ member with an old-enough account (default 6 months) and long-enough
44
+ tenure in the server (default 1 month) needs a much bigger burst before
45
+ a pure rate/duplicate-flood signal fires at all (2.5x by default), that
46
+ signal counts for less when it does (0.3x weight by default), and their
47
+ messages are never deleted for tripping *only* a rate/volume signal.
48
+ This is a separate axis from role-based trust above - an ordinary member
49
+ with no special role can be "established."
50
+
51
+ Deliberately scoped narrow: this only ever softens detection of *sending
52
+ frequently*. Scam/phishing/raid-recruitment language, suspicious links,
53
+ and non-trusted mass pings stay at full sensitivity and still get
54
+ deleted and actioned regardless of how long the account has existed -
55
+ tenure says nothing about whether a specific message is dangerous, and a
56
+ long-standing account can still genuinely raid, spam, or get
57
+ compromised. Modeled on how invisible CAPTCHA works: judge from signals
58
+ already available rather than one blunt threshold, without ever handing
59
+ out blanket immunity.
60
+
17
61
  ## [2.1.0] - 2026-08-01
18
62
 
19
63
  Additive - nothing from 2.0.0's API changes. New detections, a plugin
package/dist/config.js CHANGED
@@ -11,8 +11,9 @@ module.exports = {
11
11
  roleAnalysis: {
12
12
  // Role-name fragments (lowercased, substring match) that imply real authority.
13
13
  protectedNames: [
14
- "owner", "admin", "administrator", "moderator", "mod",
15
- "staff", "discord staff", "security"
14
+ "owner", "co-owner", "founder", "admin", "administrator",
15
+ "moderator", "mod", "staff", "discord staff", "security",
16
+ "management", "manager", "support", "helper", "developer"
16
17
  ],
17
18
  // Permission names (PermissionsBitField.Flags keys) treated as "dangerous"
18
19
  // when granted to a role - watched by AntiNuke's grant watchdog.
@@ -23,7 +24,16 @@ module.exports = {
23
24
  // Permission names that mark a member as genuinely trusted staff.
24
25
  trustPermissions: ["Administrator", "ManageGuild", "ManageRoles", "BanMembers", "KickMembers"],
25
26
  // A role younger than this that already holds a dangerous permission is extra suspicious.
26
- newRoleGraceMs: 1000 * 60 * 10
27
+ newRoleGraceMs: 1000 * 60 * 10,
28
+ // A DIFFERENT axis from role-based trust above: how long an account
29
+ // and membership has simply existed, with no relation to roles or
30
+ // permissions. Used only to soften pure rate/volume spam detection for
31
+ // members who are just very active - see basicSecurity.establishedMemberLeniency
32
+ // and docs/PROTOCOL_LAYERS.md ("Established-member leniency").
33
+ maturity: {
34
+ minAccountAgeMs: 1000 * 60 * 60 * 24 * 180, // 6 months
35
+ minServerTenureMs: 1000 * 60 * 60 * 24 * 30 // 1 month in this server
36
+ }
27
37
  },
28
38
 
29
39
  threatEngine: {
@@ -48,7 +58,18 @@ module.exports = {
48
58
  // Seed domains for PhishingDatabase (src/security/PhishingDatabase.js).
49
59
  // Empty by default - populate with what you actually see via
50
60
  // `!gf phishing add <domain>` rather than shipping unverified blocklists.
51
- linkGuard: { seedDomains: [] }
61
+ linkGuard: { seedDomains: [] },
62
+ // See roleAnalysis.maturity. Applies ONLY to pure rate/volume signals
63
+ // (message rate, duplicate flood) for established members - content
64
+ // signals (scam/phishing/raid language, links, non-trusted mass pings)
65
+ // stay at full strength no matter how old or long-tenured the sender
66
+ // is, since tenure says nothing about whether a given message is
67
+ // dangerous. Set both multipliers to 1 to disable this and treat
68
+ // every member identically.
69
+ establishedMemberLeniency: {
70
+ thresholdMultiplier: 2.5, // established members need this many times the normal rate before a signal fires at all
71
+ weightMultiplier: 0.3 // and the signal counts for less when it does
72
+ }
52
73
  },
53
74
 
54
75
  antiRaid: {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const VERSION = "2.1.0";
3
+ const VERSION = "2.2.0";
4
4
  const NAME = "GlassFrame Protocol";
5
5
  const RELEASED = "2026-07-31";
6
6
 
@@ -45,21 +45,35 @@ class BasicSecurityLayer extends Layer {
45
45
  const linkFlag = this._checkLinks(message.content || "");
46
46
  const hasInvite = this._checkInviteLink(message.content || "");
47
47
  const trust = this.frame.roleAnalyzer.trustLevel(message.member);
48
+ const established = this.frame.roleAnalyzer.isEstablishedMember(message.member);
49
+ const leniency = cfg.establishedMemberLeniency;
48
50
 
49
51
  const reasons = [];
50
52
  let weight = 0;
51
-
52
- if (buf.timestamps.length >= cfg.spam.messageThreshold) {
53
- reasons.push(`message rate ${buf.timestamps.length}/${cfg.spam.windowMs / 1000}s`);
54
- weight += 20;
53
+ let contentSignalFired = false; // anything other than pure rate/volume - see the deletion gate below
54
+
55
+ // Rate/volume signals: an established member needs a much bigger burst
56
+ // before this counts as a signal at all, and it counts for less when it
57
+ // does - see roleAnalysis.maturity for the reasoning. A brand-new or
58
+ // short-tenured member gets the normal, stricter threshold.
59
+ const messageThreshold = established ? Math.ceil(cfg.spam.messageThreshold * leniency.thresholdMultiplier) : cfg.spam.messageThreshold;
60
+ if (buf.timestamps.length >= messageThreshold) {
61
+ reasons.push(`message rate ${buf.timestamps.length}/${cfg.spam.windowMs / 1000}s${established ? " (established member, reduced sensitivity)" : ""}`);
62
+ weight += established ? Math.round(20 * leniency.weightMultiplier) : 20;
55
63
  }
56
- if (buf.dupCount >= cfg.spam.duplicateThreshold) {
57
- reasons.push(`duplicate flood x${buf.dupCount}`);
58
- weight += 20;
64
+ const duplicateThreshold = established ? Math.ceil(cfg.spam.duplicateThreshold * leniency.thresholdMultiplier) : cfg.spam.duplicateThreshold;
65
+ if (buf.dupCount >= duplicateThreshold) {
66
+ reasons.push(`duplicate flood x${buf.dupCount}${established ? " (established member, reduced sensitivity)" : ""}`);
67
+ weight += established ? Math.round(20 * leniency.weightMultiplier) : 20;
59
68
  }
69
+
70
+ // Everything below is about message CONTENT, not sending frequency -
71
+ // full strength regardless of tenure, since how long an account has
72
+ // existed says nothing about whether THIS message is dangerous.
60
73
  if (mentionCount >= cfg.mentionSpam.maxMentionsPerMessage) {
61
74
  reasons.push(`mention spam (${mentionCount})`);
62
75
  weight += 25;
76
+ contentSignalFired = true;
63
77
  }
64
78
  // A single @everyone/@here reaches every member at once - much higher
65
79
  // impact than several individual mentions, so it's weighted heavier and
@@ -68,18 +82,22 @@ class BasicSecurityLayer extends Layer {
68
82
  if (message.mentions.everyone && trust !== "HIGH" && trust !== "PROTECTED") {
69
83
  reasons.push("@everyone/@here mention from a non-trusted member");
70
84
  weight += 50;
85
+ contentSignalFired = true;
71
86
  }
72
87
  if (classification.scam >= cfg.nlp.scamScoreThreshold) {
73
88
  reasons.push(`scam language (${classification.scam.toFixed(2)})`);
74
89
  weight += 35;
90
+ contentSignalFired = true;
75
91
  }
76
92
  if (classification.phishing >= cfg.nlp.phishingScoreThreshold) {
77
93
  reasons.push(`phishing language (${classification.phishing.toFixed(2)})`);
78
94
  weight += 35;
95
+ contentSignalFired = true;
79
96
  }
80
97
  if (classification.raidCallout >= cfg.nlp.raidCalloutThreshold) {
81
98
  reasons.push(`raid-recruitment language (${classification.raidCallout.toFixed(2)})`);
82
99
  weight += 35;
100
+ contentSignalFired = true;
83
101
  }
84
102
  // The specific pattern in a lot of real raid-tool spam: an invite link
85
103
  // to another server, paired with language recruiting people to raid
@@ -90,10 +108,12 @@ class BasicSecurityLayer extends Layer {
90
108
  if (hasInvite && classification.raidCallout >= cfg.nlp.raidCalloutThreshold && trust !== "HIGH" && trust !== "PROTECTED") {
91
109
  reasons.push("Discord invite link paired with raid-recruitment language");
92
110
  weight += 50;
111
+ contentSignalFired = true;
93
112
  }
94
113
  if (linkFlag) {
95
114
  reasons.push(linkFlag);
96
115
  weight += 30;
116
+ contentSignalFired = true;
97
117
  }
98
118
 
99
119
  if (!reasons.length) {
@@ -101,10 +121,22 @@ class BasicSecurityLayer extends Layer {
101
121
  return;
102
122
  }
103
123
 
104
- try {
105
- if (message.deletable) await message.delete();
106
- } catch {
107
- /* best effort - still report the signal even if delete fails */
124
+ // Never delete a recognized admin/staff member's message over a
125
+ // heuristic - they'll be flagged for review if warranted (see
126
+ // PunishmentEngine's trust gating), but the message itself stays up.
127
+ // Same treatment for an established member who ONLY tripped a rate/
128
+ // volume signal (just a very active regular, not a raider) - but if a
129
+ // CONTENT signal fired too, the message comes down regardless of
130
+ // tenure, since a long-standing account can still post something
131
+ // genuinely dangerous.
132
+ const isProtectedTrust = trust === "HIGH" || trust === "PROTECTED";
133
+ const softenedForTenure = established && !contentSignalFired;
134
+ if (!isProtectedTrust && !softenedForTenure) {
135
+ try {
136
+ if (message.deletable) await message.delete();
137
+ } catch {
138
+ /* best effort - still report the signal even if delete fails */
139
+ }
108
140
  }
109
141
 
110
142
  await this.frame.punishmentEngine.report({
@@ -39,6 +39,32 @@ class RoleAnalyzer {
39
39
  return "LOW";
40
40
  }
41
41
 
42
+ /**
43
+ * A DIFFERENT question from trustLevel() above: not "does this member
44
+ * hold real authority," but "has this account and this membership simply
45
+ * existed long enough to not look like a fresh raid/alt account." No
46
+ * relation to roles or permissions - a totally ordinary member with no
47
+ * special role can be "established," and a MEDIUM-trust role holder with
48
+ * a week-old account isn't.
49
+ *
50
+ * This exists to soften pure rate/volume spam detection (see
51
+ * BasicSecurityLayer) for members who are simply very active, the way
52
+ * modern invisible CAPTCHA judges from signals already available instead
53
+ * of a one-size-fits-all challenge - it is deliberately NOT used to
54
+ * soften content-based detection (scam/phishing/raid language, links),
55
+ * since account age says nothing about whether a specific message is
56
+ * dangerous. An established account can still raid or spam; this only
57
+ * ever reduces false positives on "sends a lot of messages," never
58
+ * blanket-exempts anyone from everything else.
59
+ */
60
+ isEstablishedMember(member) {
61
+ if (!member || !member.user) return false;
62
+ const cfg = this.config.maturity;
63
+ const accountAge = Date.now() - member.user.createdTimestamp;
64
+ const serverTenure = member.joinedTimestamp ? Date.now() - member.joinedTimestamp : 0;
65
+ return accountAge >= cfg.minAccountAgeMs && serverTenure >= cfg.minServerTenureMs;
66
+ }
67
+
42
68
  /**
43
69
  * Scans a guild's current roles for name/permission mismatches: a role
44
70
  * whose name signals authority ("admin", "staff", ...) but carries no real
@@ -132,3 +132,27 @@ frame.registerAction("addMutedRole", async (member) => {
132
132
  ```js
133
133
  config: { punishment: { ladder: { medium: "addMutedRole" } } }
134
134
  ```
135
+
136
+ ## Established-member leniency
137
+
138
+ A separate axis from role-based trust, for a question role trust can't
139
+ answer: not "does this member have authority," but "has this account and
140
+ this membership simply existed long enough to not look like a fresh raid
141
+ account." `RoleAnalyzer.isEstablishedMember(member)` checks account age and
142
+ server tenure against `roleAnalysis.maturity` (defaults: 6 months, 1
143
+ month). A totally ordinary member with no special role can be
144
+ "established"; a MEDIUM-trust role holder with a week-old account isn't.
145
+
146
+ `BasicSecurityLayer` is the only place this is used, and only for the two
147
+ pure rate/volume checks (message rate, duplicate flood) -
148
+ `basicSecurity.establishedMemberLeniency` raises the threshold before
149
+ either fires at all and reduces the weight when it does, and neither
150
+ deletes an established member's message on its own. Every content-based
151
+ check (scam/phishing/raid-recruitment language, links, non-trusted mass
152
+ pings) is completely unaffected by tenure - full threshold, full weight,
153
+ message still deleted - since how long an account has existed says nothing
154
+ about whether a specific message is dangerous. This is deliberate: the
155
+ point is recognizing "this is just a very active regular," not granting
156
+ immunity. An established account can still raid, spam, or get compromised,
157
+ and will still be caught the moment it does anything content-wise, not
158
+ just volume-wise.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glassframe-protocol",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "A layered, self-contained Discord security engine - AntiRaid, AntiNuke, Basic Security, and optional Groq-powered AI Moderation sharing one threat-scoring and punishment pipeline. Prefix commands only, Components V2 output, no slash commands.",
5
5
  "main": "dist/index.js",
6
6
  "files": [