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/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.
@@ -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`, which handles enabling/disabling
41
- cleanly: `enable()` attaches the layer's listeners, `disable()` detaches
42
- every one of them (not just an early-return check inside the handler), so a
43
- disabled layer truly does no work rather than quietly skipping its own
44
- logic on every event.
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, the moment AntiNuke is armed, and again on demand
79
- via `!gf scan`. It reports flags, never actions - a human decides what, if
80
- anything, to do about a role that looks like impersonation bait or quietly
81
- holds dangerous permissions.
82
-
83
- ## Adding a fifth layer
84
-
85
- Extend `Layer`, implement `onEnable()` / `onDisable()`, report signals via
86
- `this.frame.punishmentEngine.report({ guild, member, layer, weight, reason })`,
87
- register the instance in `GlassFrame`'s `this.layers` map, and add a button
88
- in `src/ui/ControlPanel.js` if you want it on the panel. Nothing else in the
89
- pipeline needs to know the new layer exists.
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": "1.2.1",
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": [