glassframe-protocol 1.2.1 → 2.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/CHANGELOG.md CHANGED
@@ -14,6 +14,190 @@ 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.1.0] - 2026-08-01
18
+
19
+ Additive - nothing from 2.0.0's API changes. New detections, a plugin
20
+ system, per-guild customization, and an owner-only cross-server dashboard.
21
+
22
+ ### Added - security detections
23
+
24
+ - **Dangerous role grants to a member.** The existing permission-grant
25
+ watchdog only caught a role's own permissions changing (`RoleUpdate`).
26
+ It missed the other half: someone with `ManageRoles` handing an
27
+ already-dangerous role directly to a member (`MemberRoleUpdate`) without
28
+ ever touching the role's definition. Now watched, gated the same way -
29
+ only flagged when the executor isn't recognized trusted staff.
30
+ - **Channel permission-overwrite abuse.** A nuke doesn't have to delete
31
+ anything - locking @everyone out of a public channel, or opening a
32
+ private one up to @everyone, does equivalent damage through permission
33
+ overwrites. Only @everyone's own overwrite is watched; a role- or
34
+ member-specific overwrite change is routine administration and ignored.
35
+ - **A dedicated @everyone/@here guard** in Basic Security, weighted well
36
+ above regular mention spam (a single mass ping reaches every member at
37
+ once) and skipped for recognized trusted staff, so a real admin's
38
+ announcement isn't flagged.
39
+ - **Raid-recruitment language is now actually used.** `raidCallout` scoring
40
+ existed in the NLP engine already but was never wired into
41
+ `BasicSecurityLayer`'s real decision logic - fixed. Also added a specific
42
+ invite-link + raid-language combo check (the "join this server to raid
43
+ with us" pattern), and expanded the raid lexicon to catch common word
44
+ forms ("raiding", "raids") that the bare-verb-only version missed.
45
+
46
+ ### Added - extensibility
47
+
48
+ - **`frame.registerLayer(name, layerInstance)`** - add a custom security
49
+ layer to the same shared pipeline the built-in four use. Must extend
50
+ `core/Layer`; gets per-guild enable/disable, state persistence, and
51
+ `!gf status`/`!gf metrics` visibility for free, since those now iterate
52
+ `frame.layers` dynamically instead of a fixed list. Does not get an
53
+ automatic button on the 5-button panel (that stays fixed at 5).
54
+ - **`frame.registerAction(name, handler)`** - add a punishment action
55
+ beyond ban/kick/timeout/quarantine, referenceable from
56
+ `config.punishment.ladder`. Runs through the same action queue as the
57
+ built-in ones.
58
+ - **Per-guild custom prefix** - `!gf prefix set <newPrefix>` /
59
+ `!gf prefix reset`. `config.prefix` always still works everywhere as a
60
+ fallback; an overlapping custom/default pair resolves by trying the
61
+ longer one first.
62
+ - **Per-layer log channel routing** - `getLogChannel(guild, layerName)` now
63
+ receives the reporting layer's name as an optional second argument, so
64
+ different layers can route to different channels. Fully backward
65
+ compatible - a `getLogChannel` that only takes one parameter keeps
66
+ working unchanged.
67
+
68
+ ### Added - the owner-only engine dashboard
69
+
70
+ - **`!gf engine`** - a bot-wide, cross-server dashboard: layer adoption
71
+ across every server, the busiest servers by event volume, AI usage
72
+ (Groq key pool/cooldowns/call volume), queue and cache health, and a live
73
+ feed of the last several things logged anywhere. See `docs/ENGINE.md`.
74
+ Deliberately not listed in `!gf help` - a per-server admin shouldn't know
75
+ it exists, let alone see data about other servers.
76
+ - **Its own permission model**, separate from every other command:
77
+ `options.owners` (a list of Discord user IDs) is required regardless, and
78
+ an optional `config.engine.password` is a second factor on the initial
79
+ command only - page-navigation clicks re-check `isOwner()` but don't
80
+ re-prompt for the password. Wrong guesses count toward a lockout
81
+ (`config.engine.maxAttempts`, `config.engine.lockoutMs`), and the command
82
+ message is deleted immediately either way so the password doesn't sit
83
+ visible in channel history.
84
+ - **5 pages** (Overview, Servers, AI, Performance, Activity Log), navigated
85
+ with a row of tab buttons - this is a second interactive surface,
86
+ entirely separate from the 5-button panel and the metrics command's own
87
+ button.
88
+ - `frame.getEngineReport()` - the raw data behind the dashboard, if you
89
+ want it for your own tooling instead of the rendered message.
90
+ - `PerformanceMonitor.recordEvent(name, guildId)` now optionally tracks
91
+ per-guild activity (`topGuildsByActivity()`) alongside the existing
92
+ by-name counters.
93
+ - `SmartLogger` keeps a bounded in-memory ring buffer (`getRecentLogs()`,
94
+ last 30) of recent events across every guild, powering the Activity Log
95
+ page.
96
+
97
+ ### Changed
98
+
99
+ - `!gf help` reorganized into sections (Getting started / Security tools /
100
+ Diagnostics / Customization) with a one-line explanation of what each
101
+ command actually does, instead of a flat list of syntax.
102
+ - `!gf status` now explains what each layer actually watches for, not just
103
+ whether it's on, and iterates every registered layer (including custom
104
+ ones) instead of a fixed four.
105
+ - `!gf metrics` and the global engine view now carry a short plain-language
106
+ explanation under every number - what it means, and whether it's
107
+ generally worth a closer look.
108
+
109
+ ### Fixed
110
+
111
+ - `raidCallout` NLP scoring was computed but never checked anywhere.
112
+ - The default `raidCalloutThreshold` (0.5) was miscalibrated against how
113
+ the scoring function actually behaves - lowered to 0.3, validated against
114
+ both a real raid-recruitment example and known-innocuous messages with
115
+ margin on both sides.
116
+
117
+ ## [2.0.0] - 2026-08-01
118
+
119
+ **Breaking.** Layer activation and the whitelist were accidentally global
120
+ across every server the bot is in - this release makes them per-guild, the
121
+ way they were always supposed to work, and adds real state persistence.
122
+
123
+ ### Fixed (breaking)
124
+
125
+ - **Layer on/off state is now per-guild.** Previously, `Layer.enabled` was
126
+ one shared boolean - arming AntiRaid from one server's control panel
127
+ silently armed it for *every* server the bot serves. `getStatus(guildId)`
128
+ accepted a `guildId` but never actually used it. Both are now genuinely
129
+ per-guild: `Layer.enabledGuilds` is a `Set<guildId>`, and
130
+ `frame.enableLayer(name, guildId)` / `frame.disableLayer(name, guildId)`
131
+ now **require** a `guildId` argument. Calling either without one throws,
132
+ rather than silently doing the wrong thing.
133
+ - **The whitelist is now per-guild.** `frame.whitelist` was a single global
134
+ `Set<userId>` - whitelisting someone in one server exempted them
135
+ everywhere. It's now a `Map<guildId, Set<userId>>`; use the new
136
+ `frame.addToWhitelist(guildId, userId)`,
137
+ `frame.removeFromWhitelist(guildId, userId)`, and
138
+ `frame.isWhitelisted(guildId, userId)` instead of touching the Map
139
+ directly. The constructor's `whitelist: [...]` option still works exactly
140
+ as before from the outside - it now seeds that list into every guild
141
+ (present and future) the first time GlassFrame sees it, rather than
142
+ applying globally.
143
+ - **State now actually persists.** `stateStore` was constructed and then
144
+ never read from or written to anywhere. Every layer toggle and whitelist
145
+ change is now saved per-guild, and restored at startup (and automatically
146
+ for any new guild the bot joins) - see `docs/STATE.md`.
147
+ - Fixed a related bug in `BasicSecurityLayer`: its spam-rate buffer was
148
+ keyed by `userId` alone, so the same user active in two different
149
+ servers could incorrectly share one rate-limit window. Now keyed by
150
+ `guildId:userId`.
151
+
152
+ ### Migrating from 1.x
153
+
154
+ **If you only use the built-in control panel and `!gf` commands, nothing in
155
+ your bot's code needs to change** - update the package and you're done; the
156
+ panel and commands already operate within a specific server, they just
157
+ didn't respect that internally before now.
158
+
159
+ **If your own code calls these directly, update the call sites:**
160
+
161
+ | 1.x | 2.0.0 |
162
+ |---|---|
163
+ | `frame.enableLayer(name)` | `frame.enableLayer(name, guildId)` |
164
+ | `frame.disableLayer(name)` | `frame.disableLayer(name, guildId)` |
165
+ | `frame.whitelist.add(userId)` | `frame.addToWhitelist(guildId, userId)` |
166
+ | `frame.whitelist.delete(userId)` | `frame.removeFromWhitelist(guildId, userId)` |
167
+ | `frame.whitelist.has(userId)` | `frame.isWhitelisted(guildId, userId)` |
168
+ | `layer.enabled` | `layer.isEnabled(guildId)` |
169
+
170
+ `options.autoStart` in the constructor is unchanged - still a plain array
171
+ of layer names - but now correctly applies per-guild (to every guild the
172
+ bot is already in, and automatically to any new one it joins) instead of
173
+ turning a layer on everywhere at once.
174
+
175
+ ### Added
176
+
177
+ - `frame.ready` - a Promise that resolves once every guild the bot was
178
+ already in at construction time has finished restoring its saved state.
179
+ State restoration is async (a real `stateStore` may hit disk/a database),
180
+ so this exists for anyone who wants an explicit guarantee rather than
181
+ relying on the fact that, in practice, it resolves faster than any
182
+ Discord event could arrive.
183
+ - `frame.getGuildMetrics(guildId)` - one server's layer status, whitelist
184
+ size, open-case count, and flagged-member count.
185
+ - `!gf metrics` now shows **this server's** numbers by default instead of
186
+ bot-wide totals, with one button on the message to switch to the global
187
+ (all-servers) view and back. This is a second button, separate from the
188
+ 5-button panel - the panel is still exactly 5 buttons; the metrics
189
+ command now has 1 of its own.
190
+ - `ProtocolCache.countByPrefix(prefix)` - powers the per-guild counts above
191
+ by counting non-expired `guildId:userId`-keyed entries for one guild.
192
+ - `docs/STATE.md` - the per-guild state and persistence model in full.
193
+
194
+ ## [1.2.1] - 2026-07-31
195
+
196
+ ### Added
197
+
198
+ - `homepage` and `discord` fields in `package.json` linking to the docs site
199
+ and the support server.
200
+
17
201
  ## [1.2.0] - 2026-07-31
18
202
 
19
203
  Packaging and licensing changes to make this publishable to npm. No
@@ -79,9 +79,10 @@ toggle gets reported there through Components V2 messages.
79
79
 
80
80
  Two ways, pick either or both:
81
81
 
82
- - **Code**: list layer names in `autoStart` (see Step 2), or call
83
- `frame.enableLayer("antiNuke")` / `frame.disableLayer("antiNuke")`
84
- anywhere after construction.
82
+ - **Code**: list layer names in `autoStart` (see Step 2) - applies to every
83
+ server by default - or call
84
+ `frame.enableLayer("antiNuke", guildId)` / `frame.disableLayer("antiNuke", guildId)`
85
+ for one specific server anywhere after construction.
85
86
  - **In Discord**: an admin (Manage Server permission) sends `!gf panel` and
86
87
  gets 5 buttons - Activate/Deactivate AntiRaid, AI Moderation, AntiNuke,
87
88
  Basic Security, and Full Protocol (all four at once). This is the only
@@ -119,13 +120,31 @@ Nobody needs this guide again after setup. Server admins use:
119
120
  | `!gf scan` | Checks current roles for name/permission mismatches |
120
121
  | `!gf metrics` | Performance and cache health |
121
122
  | `!gf phishing add/remove/list <domain>` | Manage the link blocklist |
122
- | `!gf whitelist add/remove <userId>` | Exempt a user from punitive action |
123
+ | `!gf whitelist add/remove <userId>` | Exempt a user from punitive action in this server |
123
124
  | `!gf help` | Lists all of the above |
124
125
 
126
+ ## Step 6 (optional) - Your own bot-wide dashboard
127
+
128
+ If you run the bot yourself, `!gf engine` gives you a cross-server view
129
+ (busiest server, AI usage, a live activity feed) that regular server admins
130
+ can't see:
131
+
132
+ ```js
133
+ const frame = new GlassFrame(client, {
134
+ getLogChannel: /* ... */,
135
+ owners: ["your-discord-user-id"]
136
+ });
137
+ ```
138
+
139
+ See `docs/ENGINE.md` for the optional password/lockout system and what
140
+ each of its 5 pages shows.
141
+
125
142
  ## Going deeper
126
143
 
127
144
  - `README.md` - full feature overview
128
145
  - `docs/PROTOCOL_LAYERS.md` - how a signal becomes an action
146
+ - `docs/STATE.md` - per-server layer/whitelist state and persistence
147
+ - `docs/ENGINE.md` - the owner-only bot-wide dashboard
129
148
  - `docs/CACHE_ARCHITECTURE.md` - every internal cache and its TTL
130
149
  - `docs/PERFORMANCE.md` - the bounded-concurrency queues and `!gf metrics`
131
150
  - `docs/COMMANDS.md` - full command/button reference
package/README.md CHANGED
@@ -7,18 +7,23 @@ pipeline instead of each reacting on its own, so the same member never gets
7
7
  hit by four different modules for one incident, and a single soft flag
8
8
  never turns into an auto-ban.
9
9
 
10
+ Everything is per-server: one bot process can run GlassFrame across many
11
+ Discord servers, each with its own independent set of active layers and its
12
+ own whitelist, persisted across restarts. See `docs/STATE.md`.
13
+
10
14
  No slash commands - prefix commands only. No legacy `EmbedBuilder` - every
11
15
  log message renders through Discord's Components V2 system. No emoji
12
16
  anywhere in code or output.
13
17
 
14
18
  ## Install
15
19
 
16
- Copy the `glassframe-protocol` folder into your bot project, then:
17
-
18
20
  ```
19
- npm install
21
+ npm install glassframe-protocol
20
22
  ```
21
23
 
24
+ Or copy the `glassframe-protocol` folder directly into your bot project and
25
+ `npm install` inside it - either works the same way.
26
+
22
27
  Requires `discord.js` v14.16+ (Components V2 support and the
23
28
  `guildAuditLogEntryCreate` event) and Node.js 18+ (global `fetch`, used only
24
29
  by the optional AI layer).
@@ -56,8 +61,8 @@ client.once("ready", () => {
56
61
  const frame = new GlassFrame(client, {
57
62
  getLogChannel: async (guild) =>
58
63
  guild.channels.cache.find((c) => c.name === "security-logs") ?? null,
59
- whitelist: ["123456789012345678"],
60
- autoStart: ["basicSecurity"], // layers not listed here start OFF until armed from the panel
64
+ whitelist: ["123456789012345678"], // trusted everywhere, seeded into every guild on first run
65
+ autoStart: ["basicSecurity"], // starting layers for every guild - not a global on/off
61
66
  config: {
62
67
  antiRaid: { joinThreshold: 8 },
63
68
  aiModeration: { apiKeys: [process.env.GROQ_KEY_1, process.env.GROQ_KEY_2].filter(Boolean) }
@@ -75,8 +80,10 @@ the 5-button control panel and arm layers from there instead of in code.
75
80
 
76
81
  ## The four layers
77
82
 
78
- - **Basic Security** - spam rate, duplicate flood, mention spam, local NLP
79
- scam/phishing scoring, and link checks via `PhishingDatabase` (a
83
+ - **Basic Security** - spam rate, duplicate flood, mention spam, a
84
+ dedicated trust-aware @everyone/@here guard, local NLP scam/phishing/
85
+ raid-recruitment scoring (including a specific invite-link +
86
+ raid-language combo check), and link checks via `PhishingDatabase` (a
80
87
  server-editable blocklist plus raw-IP/punycode/brand-look-alike/shortener
81
88
  heuristics - `!gf phishing add/remove/list`).
82
89
  - **AntiRaid** - join-velocity lockdown with automatic verification-level
@@ -85,30 +92,36 @@ the 5-button control panel and arm layers from there instead of in code.
85
92
  catches a raid trickling in too slowly to trip the rate counter.
86
93
  - **AntiNuke** - audit-log burst detection across channels, roles, bans,
87
94
  kicks, webhooks, emoji, and stickers per executor; a dangerous
88
- permission-grant watchdog (catches a role quietly gaining Administrator
89
- the moment it happens, with optional auto-revert); a server-identity
90
- watchdog (name/icon/vanity URL changes); an invite-abuse watchdog
91
- (unrestricted invites from non-staff); and webhook-flood containment
92
- (deletes an abusive webhook and traces it back to its creator, since
93
- webhook messages carry no guild member for other layers to see).
95
+ permission-grant watchdog covering both a role's own permissions
96
+ changing *and* a member being quietly handed an already-dangerous role
97
+ (with optional auto-revert); a channel permission-overwrite watchdog
98
+ (catches @everyone getting locked out of, or let into, a channel without
99
+ anything being deleted); a server-identity watchdog (name/icon/vanity URL
100
+ changes); an invite-abuse watchdog (unrestricted invites from non-staff);
101
+ and webhook-flood containment (deletes an abusive webhook and traces it
102
+ back to its creator, since webhook messages carry no guild member for
103
+ other layers to see).
94
104
  - **AI Moderation** (optional, off by default) - Groq-powered second opinion
95
105
  for messages the local NLP scores as "gray zone": not clean, not clearly
96
106
  over threshold. Supports a pool of API keys with automatic per-key
97
107
  cooldown on rate limits. See `config.aiModeration` and
98
108
  `docs/CACHE_ARCHITECTURE.md` for how verdicts get cached and keys rotated.
99
109
 
100
- Every layer starts disabled. Nothing runs until you list it in
101
- `autoStart`, call `frame.enableLayer(name)`, or arm it from the control
102
- panel.
110
+ Every layer starts disabled, per guild. Nothing runs until you list it in
111
+ `autoStart`, call `frame.enableLayer(name, guildId)`, or arm it from the
112
+ control panel. Add your own layer alongside these four with
113
+ `frame.registerLayer()` - see "Extending it" below.
103
114
 
104
115
  ## Performance under load
105
116
 
106
117
  Every Discord mutation `PunishmentEngine` performs and every outbound Groq
107
118
  request go through a bounded-concurrency queue (`frame.actionQueue` /
108
119
  `frame.aiQueue`) instead of firing unbounded, so a burst can't trip a rate
109
- limit by hammering it all at once. `frame.getMetrics()` / `!gf metrics`
110
- gives you queue depth, cache stats, and average latency in one snapshot.
111
- Full write-up in `docs/PERFORMANCE.md`.
120
+ limit by hammering it all at once. `!gf metrics` shows this server's numbers
121
+ by default (open cases, flagged members, active layers), with a button on
122
+ the message to switch to bot-wide totals across every server
123
+ (`frame.getMetrics()` / `frame.getGuildMetrics(guildId)`). Full write-up in
124
+ `docs/PERFORMANCE.md`.
112
125
 
113
126
  ## How punishment works
114
127
 
@@ -121,15 +134,37 @@ for human review, never auto-banned), and only if there isn't already an
121
134
  open case for that member from a moment ago. Full write-up in
122
135
  `docs/PROTOCOL_LAYERS.md`.
123
136
 
137
+ ## Extending it
138
+
139
+ - **`frame.registerLayer(name, layerInstance)`** - add your own security
140
+ layer to the same pipeline the built-in four use (per-guild enable/state/
141
+ persistence come free from extending `core/Layer`). Doesn't get a button
142
+ on the panel automatically - the panel stays fixed at exactly 5.
143
+ - **`frame.registerAction(name, handler)`** - add a custom punishment
144
+ action beyond ban/kick/timeout/quarantine, referenceable from
145
+ `config.punishment.ladder`.
146
+ - **`!gf prefix set <newPrefix>`** - each server can run its own prefix;
147
+ `config.prefix` always still works everywhere as a fallback.
148
+ - **`getLogChannel(guild, layerName)`** - route different layers to
149
+ different channels, if you want; the second parameter is optional and
150
+ backward compatible.
151
+ - **`!gf engine`** - an owner-only, bot-wide dashboard (busiest server, AI
152
+ usage, live cross-server activity feed), gated separately from every
153
+ other command and not listed in `!gf help`. See `docs/ENGINE.md`.
154
+
124
155
  ## Docs
125
156
 
126
157
  - `GETTING_STARTED.md` - adding GlassFrame to a bot you already have running.
127
158
  - `docs/PROTOCOL_LAYERS.md` - full architecture: containment vs. punishment,
128
- how a signal becomes an action, how to add a fifth layer.
159
+ how a signal becomes an action, `registerLayer`/`registerAction`.
160
+ - `docs/STATE.md` - per-guild layer/whitelist state and how persistence
161
+ across restarts works.
129
162
  - `docs/CACHE_ARCHITECTURE.md` - every cache in the library, its key shape,
130
163
  its TTL, and how to turn on cache-level debug logging.
131
164
  - `docs/PERFORMANCE.md` - the bounded-concurrency queues and internal
132
165
  metrics collector, and what `!gf metrics` shows.
166
+ - `docs/ENGINE.md` - the owner-only bot-wide dashboard: the password/lockout
167
+ system and what each of its 5 pages shows.
133
168
  - `docs/COMMANDS.md` - the full prefix command reference and the control
134
169
  panel's five buttons.
135
170
  - `docs/PUBLISHING.md` - publishing this package to npm, and what actually
package/dist/config.js CHANGED
@@ -44,7 +44,7 @@ module.exports = {
44
44
  basicSecurity: {
45
45
  spam: { windowMs: 6000, messageThreshold: 6, duplicateThreshold: 4 },
46
46
  mentionSpam: { maxMentionsPerMessage: 8 },
47
- nlp: { scamScoreThreshold: 0.62, phishingScoreThreshold: 0.55 },
47
+ nlp: { scamScoreThreshold: 0.62, phishingScoreThreshold: 0.55, raidCalloutThreshold: 0.3 },
48
48
  // Seed domains for PhishingDatabase (src/security/PhishingDatabase.js).
49
49
  // Empty by default - populate with what you actually see via
50
50
  // `!gf phishing add <domain>` rather than shipping unverified blocklists.
@@ -83,6 +83,10 @@ module.exports = {
83
83
  // A brand-new invite with no expiry and no use limit, created by anyone
84
84
  // other than the owner, is worth a look the moment it's created.
85
85
  watchInviteAbuse: true,
86
+ // Watches @everyone's channel permission overwrites specifically - a
87
+ // channel getting locked out for everyone, or a private channel getting
88
+ // opened to everyone, is a nuke technique that doesn't delete anything.
89
+ watchOverwriteAbuse: true,
86
90
  // A webhook that starts pushing a burst of messages within minutes of
87
91
  // being created is a classic "nuke via webhook" pattern - this catches
88
92
  // it even though the messages themselves look like normal messageCreate
@@ -134,5 +138,20 @@ module.exports = {
134
138
  // Default for GlassFrame's debug flag - logs every cache SET/HIT/MISS/EXPIRE to the console.
135
139
  // See docs/CACHE_ARCHITECTURE.md. Overridden by the `debug` constructor option if provided.
136
140
  debug: false
141
+ },
142
+
143
+ // The owner-only `!gf engine` dashboard - see docs/ENGINE.md. Being an
144
+ // owner (options.owners) is required no matter what; a password is an
145
+ // optional second factor on top of that for the initial command only -
146
+ // page-navigation button clicks are still gated by isOwner() but don't
147
+ // re-prompt for the password.
148
+ engine: {
149
+ // Set via an environment variable in your own bot, e.g.
150
+ // process.env.GLASSFRAME_ENGINE_PASSWORD - never hardcode a real
151
+ // password into a committed config.js. Leave null to skip the second
152
+ // factor and rely on the owners list alone.
153
+ password: null,
154
+ maxAttempts: 3,
155
+ lockoutMs: 1000 * 60 * 10
137
156
  }
138
157
  };