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 +184 -0
- package/GETTING_STARTED.md +23 -4
- package/README.md +55 -20
- package/dist/config.js +20 -1
- package/dist/src/GlassFrame.js +312 -42
- package/dist/src/commands/PrefixRouter.js +123 -48
- package/dist/src/core/Cache.js +10 -0
- package/dist/src/core/Layer.js +28 -23
- package/dist/src/core/PerformanceMonitor.js +16 -4
- package/dist/src/core/VersionInfo.js +1 -1
- package/dist/src/layers/AIModerationLayer.js +4 -4
- package/dist/src/layers/AntiNukeLayer.js +113 -16
- package/dist/src/layers/AntiRaidLayer.js +5 -7
- package/dist/src/layers/BasicSecurityLayer.js +39 -12
- package/dist/src/logging/SmartLogger.js +33 -3
- package/dist/src/moderation/PunishmentEngine.js +19 -7
- package/dist/src/ui/ControlPanel.js +195 -1
- package/dist/src/utils/nlpEngine.js +2 -2
- package/docs/CACHE_ARCHITECTURE.md +9 -0
- package/docs/COMMANDS.md +37 -11
- package/docs/ENGINE.md +83 -0
- package/docs/PROTOCOL_LAYERS.md +62 -17
- package/docs/STATE.md +101 -0
- package/package.json +1 -1
package/docs/ENGINE.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# The Engine Dashboard
|
|
2
|
+
|
|
3
|
+
`!gf engine` is a bot-wide, cross-server dashboard for whoever actually runs
|
|
4
|
+
the bot - not a per-server admin tool. It shows things a single server's
|
|
5
|
+
admin shouldn't be able to see about *other* servers (which server is
|
|
6
|
+
busiest, bot-wide AI usage, a live feed of what's happening everywhere), so
|
|
7
|
+
it has its own, separate permission model from every other command.
|
|
8
|
+
|
|
9
|
+
It is deliberately **not** listed in `!gf help` or `docs/COMMANDS.md`'s
|
|
10
|
+
in-Discord command table - a random server admin shouldn't even know it
|
|
11
|
+
exists. You're expected to learn about it from this file.
|
|
12
|
+
|
|
13
|
+
## Who can use it
|
|
14
|
+
|
|
15
|
+
Two independent checks, not one:
|
|
16
|
+
|
|
17
|
+
1. **`isOwner(userId)`** - you must be listed in `options.owners` when you
|
|
18
|
+
construct `GlassFrame`. This is required, full stop, for the command
|
|
19
|
+
itself *and* every page-navigation button click afterward.
|
|
20
|
+
2. **A password** (optional) - if `config.engine.password` is set, the
|
|
21
|
+
initial `!gf engine <password>` command also needs the correct password.
|
|
22
|
+
Page-navigation button clicks after that don't re-prompt for it, since a
|
|
23
|
+
button click is already tied to a real, verified Discord identity - only
|
|
24
|
+
the text-command entry point needs the second factor.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const frame = new GlassFrame(client, {
|
|
28
|
+
getLogChannel: /* ... */,
|
|
29
|
+
owners: ["your-discord-user-id"],
|
|
30
|
+
config: {
|
|
31
|
+
engine: {
|
|
32
|
+
password: process.env.GLASSFRAME_ENGINE_PASSWORD // never hardcode a real one
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Leave `config.engine.password` unset to skip the second factor and rely on
|
|
39
|
+
the owners list alone.
|
|
40
|
+
|
|
41
|
+
## What happens to the password
|
|
42
|
+
|
|
43
|
+
Typing a password into a normal Discord message means it briefly sits in
|
|
44
|
+
plain text in a channel, visible to anyone watching and to Discord's own
|
|
45
|
+
message history. GlassFrame does two things about that:
|
|
46
|
+
|
|
47
|
+
- The command message is **deleted immediately** after being checked,
|
|
48
|
+
whether the password was right or wrong.
|
|
49
|
+
- Wrong guesses count toward a **lockout** -
|
|
50
|
+
`config.engine.maxAttempts` wrong attempts (default 3) locks that user
|
|
51
|
+
out for `config.engine.lockoutMs` (default 10 minutes), making
|
|
52
|
+
brute-forcing impractical.
|
|
53
|
+
|
|
54
|
+
Use a private channel (or a channel only you can see) for this command
|
|
55
|
+
regardless - deletion happens right after Discord delivers the message, not
|
|
56
|
+
before anyone in the channel could have glimpsed it.
|
|
57
|
+
|
|
58
|
+
## The five pages
|
|
59
|
+
|
|
60
|
+
One row of tab buttons switches between them; the active tab is highlighted.
|
|
61
|
+
|
|
62
|
+
| Page | Shows |
|
|
63
|
+
|---|---|
|
|
64
|
+
| **Overview** | Version, uptime, guild count, layer adoption across every server |
|
|
65
|
+
| **Servers** | The busiest servers ranked by event volume since this process started |
|
|
66
|
+
| **AI** | Groq key pool status, cooldowns, call volume, verdict cache hit rate |
|
|
67
|
+
| **Performance** | Action/AI queue depth, all three cache stats, average latency per operation |
|
|
68
|
+
| **Activity Log** | The last several things logged anywhere, any server, newest first |
|
|
69
|
+
|
|
70
|
+
All of it comes from one call: `frame.getEngineReport()`, if you want the
|
|
71
|
+
raw data instead of the rendered message (for your own dashboard, an API
|
|
72
|
+
endpoint, whatever).
|
|
73
|
+
|
|
74
|
+
## "Busiest" and the activity log, precisely
|
|
75
|
+
|
|
76
|
+
- **Busiest** is a count of events (messages/joins/audit entries) any armed
|
|
77
|
+
layer processed for that server, tracked in memory since the process
|
|
78
|
+
started - it resets on restart and isn't a judgment about which server is
|
|
79
|
+
causing trouble, just which one has the most traffic.
|
|
80
|
+
- The **activity log** is the last 30 events logged anywhere (any server,
|
|
81
|
+
any layer), kept in memory by `SmartLogger` - see
|
|
82
|
+
`docs/CACHE_ARCHITECTURE.md`. The dashboard shows the most recent 10 of
|
|
83
|
+
those.
|
package/docs/PROTOCOL_LAYERS.md
CHANGED
|
@@ -37,11 +37,14 @@ instead of twenty separate ones.
|
|
|
37
37
|
| `antiNuke` | `guildAuditLogEntryCreate`, plus `messageCreate` only when webhook-abuse watching is on | reverts a dangerous permission grant, deletes an abusive webhook | destructive-action bursts per executor across channels/roles/bans/kicks/webhooks/emoji/stickers |
|
|
38
38
|
| `aiModeration` | `messageCreate` (independently of `basicSecurity`) | - | AI-confirmed category + confidence for gray-zone messages |
|
|
39
39
|
|
|
40
|
-
Each layer extends `src/core/Layer.js
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
Each layer extends `src/core/Layer.js`. Its event listeners attach exactly
|
|
41
|
+
once, ever (`attach()`), rather than on every enable/disable - Discord's
|
|
42
|
+
gateway has no concept of a listener scoped to one guild, so the listener
|
|
43
|
+
always fires and each handler's first real check is
|
|
44
|
+
`if (!this.isEnabled(guildId)) return;`. Enabling or disabling a layer is
|
|
45
|
+
per-guild: `layer.enable(guildId)` / `layer.disable(guildId)` track a
|
|
46
|
+
`Set<guildId>` (`enabledGuilds`), not a single boolean - see
|
|
47
|
+
`docs/STATE.md` for the full per-guild state and persistence model.
|
|
45
48
|
|
|
46
49
|
`basicSecurity` and `aiModeration` both listen to `messageCreate`
|
|
47
50
|
independently rather than one calling into the other - either can be
|
|
@@ -75,15 +78,57 @@ how they respond.
|
|
|
75
78
|
## Why AntiNuke also runs a role audit
|
|
76
79
|
|
|
77
80
|
`RoleAnalyzer.scanRoles()` (role name vs. permission mismatch detection)
|
|
78
|
-
runs automatically once
|
|
79
|
-
via `!gf scan`. It reports flags, never
|
|
80
|
-
anything, to do about a role that looks
|
|
81
|
-
holds dangerous permissions.
|
|
82
|
-
|
|
83
|
-
## Adding a
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
runs automatically once for a server the moment AntiNuke is armed *for
|
|
82
|
+
that server*, and again on demand via `!gf scan`. It reports flags, never
|
|
83
|
+
actions - a human decides what, if anything, to do about a role that looks
|
|
84
|
+
like impersonation bait or quietly holds dangerous permissions.
|
|
85
|
+
|
|
86
|
+
## Adding a layer without editing the library
|
|
87
|
+
|
|
88
|
+
`frame.registerLayer(name, layerInstance)` is the supported way to do this
|
|
89
|
+
now - no need to edit `GlassFrame.js`'s `this.layers` map by hand.
|
|
90
|
+
`layerInstance` must extend `core/Layer`: implement `attach()` (register
|
|
91
|
+
your event listener(s) once - it's called exactly once, at registration),
|
|
92
|
+
gate all real work behind `this.isEnabled(guildId)`, and report signals via
|
|
93
|
+
`this.frame.punishmentEngine.report({ guild, member, layer, weight, reason })`.
|
|
94
|
+
Per-guild enable/disable, state persistence, and `!gf status`/`!gf metrics`
|
|
95
|
+
all pick it up automatically, since they iterate `frame.layers` rather than
|
|
96
|
+
a fixed list. It does **not** get a button on the 5-button panel
|
|
97
|
+
automatically (that stays fixed at exactly 5) - toggle it with
|
|
98
|
+
`frame.enableLayer(name, guildId)` in code, or build your own command.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
const Layer = require("glassframe-protocol/src/core/Layer");
|
|
102
|
+
|
|
103
|
+
class LinkAgeLayer extends Layer {
|
|
104
|
+
constructor(frame) { super("linkAge", frame); }
|
|
105
|
+
attach() {
|
|
106
|
+
this._listen(this.client, "messageCreate", (message) => this._handle(message).catch(() => {}));
|
|
107
|
+
}
|
|
108
|
+
async _handle(message) {
|
|
109
|
+
if (!message.guild || message.author.bot) return;
|
|
110
|
+
if (!this.isEnabled(message.guild.id)) return;
|
|
111
|
+
// ... your detection logic, then:
|
|
112
|
+
// await this.frame.punishmentEngine.report({ guild: message.guild, member: message.member, layer: "linkAge", weight: 40, reason: "..." });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
frame.registerLayer("linkAge", new LinkAgeLayer(frame));
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Custom punishment actions
|
|
120
|
+
|
|
121
|
+
`frame.registerAction(name, handler)` adds an action beyond the built-in
|
|
122
|
+
ban/kick/timeout/quarantine, so `config.punishment.ladder` can reference it
|
|
123
|
+
by name. `handler` is `async (member, record) => {}` and runs through the
|
|
124
|
+
same bounded-concurrency action queue as the built-in ones.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
frame.registerAction("addMutedRole", async (member) => {
|
|
128
|
+
const role = member.guild.roles.cache.find((r) => r.name === "Muted");
|
|
129
|
+
if (role) await member.roles.add(role);
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
```js
|
|
133
|
+
config: { punishment: { ladder: { medium: "addMutedRole" } } }
|
|
134
|
+
```
|
package/docs/STATE.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Per-Guild State and Persistence
|
|
2
|
+
|
|
3
|
+
One GlassFrame instance can serve many Discord servers at once. Two things
|
|
4
|
+
are tracked independently per server, and both survive a bot restart if you
|
|
5
|
+
give GlassFrame a persistent `stateStore`:
|
|
6
|
+
|
|
7
|
+
- **Which layers are on.** Server A can run AntiRaid + AntiNuke while Server
|
|
8
|
+
B runs nothing, or everything, independently. There is no such thing as
|
|
9
|
+
"GlassFrame is on" globally - only "layer X is on for guild Y."
|
|
10
|
+
- **The whitelist.** Exempting a user in one server does not exempt them
|
|
11
|
+
anywhere else.
|
|
12
|
+
|
|
13
|
+
Everything else that's naturally guild-scoped already was before this -
|
|
14
|
+
threat scores, open cases, join-rate tracking, audit-log burst tracking are
|
|
15
|
+
all keyed by `guildId` internally. The two items above are the ones that
|
|
16
|
+
used to be accidentally global; see `CHANGELOG.md` for the 2.0.0 entry if
|
|
17
|
+
you're upgrading from an earlier version.
|
|
18
|
+
|
|
19
|
+
## How it's stored
|
|
20
|
+
|
|
21
|
+
`Layer.enabledGuilds` is a `Set<guildId>` per layer (not one shared
|
|
22
|
+
boolean). `GlassFrame.whitelist` is a `Map<guildId, Set<userId>>`. Both are
|
|
23
|
+
read through the frame's own methods rather than touched directly:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
frame.enableLayer("antiRaid", guildId);
|
|
27
|
+
frame.disableLayer("antiRaid", guildId);
|
|
28
|
+
frame.getStatus(guildId); // { basicSecurity: true, antiRaid: false, ... }
|
|
29
|
+
|
|
30
|
+
frame.addToWhitelist(guildId, userId);
|
|
31
|
+
frame.removeFromWhitelist(guildId, userId);
|
|
32
|
+
frame.isWhitelisted(guildId, userId);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Persistence across restarts
|
|
36
|
+
|
|
37
|
+
Pass a `stateStore` when constructing GlassFrame - `MemoryStateStore`
|
|
38
|
+
(default, lost on restart) or `JSONFileStateStore` (a single JSON file, no
|
|
39
|
+
database, ARM64/Termux-friendly):
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const { JSONFileStateStore } = require("glassframe-protocol");
|
|
43
|
+
|
|
44
|
+
const frame = new GlassFrame(client, {
|
|
45
|
+
getLogChannel: /* ... */,
|
|
46
|
+
stateStore: new JSONFileStateStore("./glassframe-state.json"),
|
|
47
|
+
autoStart: ["basicSecurity", "antiRaid"]
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Every time a layer is toggled or the whitelist changes for a guild,
|
|
52
|
+
GlassFrame writes that guild's full state (which layers are on, its
|
|
53
|
+
whitelist) to the store. At startup, for every guild the bot is already in,
|
|
54
|
+
and again automatically whenever the bot joins a new guild
|
|
55
|
+
(`guildCreate`), GlassFrame:
|
|
56
|
+
|
|
57
|
+
1. Checks the store for saved state for that guild.
|
|
58
|
+
2. If found, restores exactly that - the layers that were on stay on.
|
|
59
|
+
3. If nothing is saved yet (a guild GlassFrame has never seen before),
|
|
60
|
+
applies `autoStart` and the constructor's `whitelist` option (see below)
|
|
61
|
+
as that guild's starting defaults.
|
|
62
|
+
|
|
63
|
+
Restoring is async (a real `stateStore` might read from disk or a
|
|
64
|
+
database), so it can't finish before the constructor returns. In practice
|
|
65
|
+
this resolves well before a Discord event could possibly arrive, so most
|
|
66
|
+
code never needs to think about it - but if you want a guarantee, `await
|
|
67
|
+
frame.ready` after construction; it resolves once every guild the bot was
|
|
68
|
+
already in has been restored (guilds joined later via `guildCreate`
|
|
69
|
+
restore independently and aren't part of this promise, since there's
|
|
70
|
+
nothing they could race against).
|
|
71
|
+
|
|
72
|
+
`autoStart` and the `whitelist` option are only ever *defaults for a
|
|
73
|
+
guild's first run* - once a guild has any saved state (even "everything
|
|
74
|
+
off"), neither applies to it again; the saved state is the source of truth
|
|
75
|
+
from then on.
|
|
76
|
+
|
|
77
|
+
## The `whitelist` constructor option
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
new GlassFrame(client, { getLogChannel: /* ... */, whitelist: ["123456789012345678"] });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This seeds a starting whitelist for every guild - both the ones already in
|
|
84
|
+
`client.guilds.cache` and any the bot joins later - the first time
|
|
85
|
+
GlassFrame sees that guild (i.e., it never overrides a guild's own saved
|
|
86
|
+
whitelist). Think of it as "these users are trusted everywhere by default";
|
|
87
|
+
`frame.removeFromWhitelist(guildId, userId)` still works normally
|
|
88
|
+
per-guild afterward.
|
|
89
|
+
|
|
90
|
+
## Writing your own store
|
|
91
|
+
|
|
92
|
+
Implement two async methods and pass an instance as `stateStore`:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
class MyStore {
|
|
96
|
+
async get(guildId) { /* return the saved state object, or null */ }
|
|
97
|
+
async set(guildId, state) { /* persist `state` for this guildId */ }
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`state` is `{ layers: { basicSecurity: true, antiRaid: false, ... }, whitelist: ["userId1", "userId2"] }`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glassframe-protocol",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.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": [
|