glassframe-protocol 1.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 +197 -0
- package/GETTING_STARTED.md +133 -0
- package/LICENSE +56 -0
- package/README.md +145 -0
- package/dist/config.js +138 -0
- package/dist/index.js +13 -0
- package/dist/src/GlassFrame.js +195 -0
- package/dist/src/ai/GroqClient.js +161 -0
- package/dist/src/commands/PrefixRouter.js +177 -0
- package/dist/src/core/Cache.js +98 -0
- package/dist/src/core/EventQueue.js +62 -0
- package/dist/src/core/Layer.js +68 -0
- package/dist/src/core/PerformanceMonitor.js +52 -0
- package/dist/src/core/StateStore.js +69 -0
- package/dist/src/core/ThreatEngine.js +86 -0
- package/dist/src/core/VersionInfo.js +32 -0
- package/dist/src/layers/AIModerationLayer.js +77 -0
- package/dist/src/layers/AntiNukeLayer.js +278 -0
- package/dist/src/layers/AntiRaidLayer.js +144 -0
- package/dist/src/layers/BasicSecurityLayer.js +111 -0
- package/dist/src/logging/ComponentsV2.js +44 -0
- package/dist/src/logging/SmartLogger.js +84 -0
- package/dist/src/moderation/PunishmentEngine.js +175 -0
- package/dist/src/moderation/RoleAnalyzer.js +82 -0
- package/dist/src/security/PhishingDatabase.js +71 -0
- package/dist/src/ui/ControlPanel.js +58 -0
- package/dist/src/utils/nlpEngine.js +149 -0
- package/docs/CACHE_ARCHITECTURE.md +78 -0
- package/docs/COMMANDS.md +38 -0
- package/docs/PERFORMANCE.md +59 -0
- package/docs/PROTOCOL_LAYERS.md +89 -0
- package/docs/PUBLISHING.md +93 -0
- package/examples/basic-usage.js +44 -0
- package/package.json +44 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
GlassFrame Protocol is built to stay fast under exactly the conditions that
|
|
4
|
+
make speed hardest: a raid, a nuke attempt, or a wave of borderline messages
|
|
5
|
+
all create a burst of work at the same moment. Two small pieces exist
|
|
6
|
+
specifically for that.
|
|
7
|
+
|
|
8
|
+
## EventQueue - bounded concurrency
|
|
9
|
+
|
|
10
|
+
`src/core/EventQueue.js` is a plain async queue with a concurrency cap.
|
|
11
|
+
Every actual Discord mutation `PunishmentEngine` performs (ban/kick/timeout/
|
|
12
|
+
role removal) and every outbound Groq request `GroqClient` makes goes
|
|
13
|
+
through one of two queues instead of firing directly:
|
|
14
|
+
|
|
15
|
+
- `frame.actionQueue` - Discord mutations, default concurrency 4
|
|
16
|
+
(`config.performance.actionConcurrency`).
|
|
17
|
+
- `frame.aiQueue` - Groq requests, default concurrency 3
|
|
18
|
+
(`config.performance.aiConcurrency`).
|
|
19
|
+
|
|
20
|
+
Nothing is ever dropped - tasks queue instead of running unbounded. This
|
|
21
|
+
matters most during a burst: twenty raid removals firing at once is exactly
|
|
22
|
+
what trips Discord's per-route rate limit and makes every request slower,
|
|
23
|
+
including the ones that matter. Capping concurrency keeps steady throughput
|
|
24
|
+
instead of a stall-then-flood pattern. `GroqClient`'s per-request timeout
|
|
25
|
+
clock only starts once a request actually leaves the queue, so a busy
|
|
26
|
+
moment never produces a false "the AI didn't respond in time" the way it
|
|
27
|
+
would if the timer started at the moment of the API call being requested.
|
|
28
|
+
|
|
29
|
+
Both queues expose `getStats()` (`active`, `pending`, `completed`, `failed`,
|
|
30
|
+
`concurrency`), surfaced through `frame.getMetrics()` and `!gf metrics`.
|
|
31
|
+
|
|
32
|
+
## PerformanceMonitor - lightweight internal metrics
|
|
33
|
+
|
|
34
|
+
`src/core/PerformanceMonitor.js` tracks two things, both in memory, neither
|
|
35
|
+
requiring a dependency:
|
|
36
|
+
|
|
37
|
+
- **Event counts** - `recordEvent(name)` increments a counter. Every layer's
|
|
38
|
+
main handler calls this once per event it processes
|
|
39
|
+
(`basicSecurity.messageCreate`, `antiRaid.guildMemberAdd`,
|
|
40
|
+
`antiNuke.auditEntry.<type>`, `groqClient.classify`, ...).
|
|
41
|
+
- **Latency samples** - `time(name)` returns a function; call it when the
|
|
42
|
+
unit of work finishes. The last 50 samples per name are kept and averaged.
|
|
43
|
+
Used around `punishmentEngine.execute` and `groqClient.classify`, the two
|
|
44
|
+
places most likely to be slow (Discord API round trips, network calls).
|
|
45
|
+
|
|
46
|
+
`frame.getMetrics()` returns all of this plus every cache's stats in one
|
|
47
|
+
snapshot:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
{
|
|
51
|
+
performance: { uptimeMs, events: { "basicSecurity.messageCreate": 412, ... }, avgLatencyMs: { "groqClient.classify": 340.2, ... } },
|
|
52
|
+
queues: { actions: { active, pending, completed, failed, concurrency }, ai: { ... } },
|
|
53
|
+
caches: { threatScores: { size, hits, misses, evictions }, openCases: { ... }, groqVerdicts: { ... } }
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`!gf metrics` renders the same data as a Components V2 message. Neither
|
|
58
|
+
`EventQueue` nor `PerformanceMonitor` phones out anywhere - everything here
|
|
59
|
+
stays in the process.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Protocol Layers
|
|
2
|
+
|
|
3
|
+
GlassFrame Protocol splits every reaction into two kinds, and keeping them
|
|
4
|
+
separate is the core design decision of the whole library.
|
|
5
|
+
|
|
6
|
+
## Containment vs. punishment
|
|
7
|
+
|
|
8
|
+
**Containment** is a guild-wide, reversible, time-critical measure a layer
|
|
9
|
+
takes on its own the moment it detects trouble - raising verification level
|
|
10
|
+
during a join spike, reverting a role that just gained Administrator. These
|
|
11
|
+
can't wait for a shared decision pipeline; a raid in progress needs an
|
|
12
|
+
answer in milliseconds, not after a round trip through scoring.
|
|
13
|
+
|
|
14
|
+
**Punishment** is anything aimed at one member - timeout, kick, ban,
|
|
15
|
+
quarantine. Every layer funnels this through the same two pieces instead of
|
|
16
|
+
deciding on its own:
|
|
17
|
+
|
|
18
|
+
1. **`ThreatEngine`** - turns a signal (`{ layer, weight, reason }`) into a
|
|
19
|
+
decaying score per member, and a tier (`none/low/medium/high/critical`).
|
|
20
|
+
2. **`PunishmentEngine`** - turns a tier into an action, after checking
|
|
21
|
+
`RoleAnalyzer` for hierarchy and trust, and checking for an already-open
|
|
22
|
+
case on that member so a second signal a moment later merges into the
|
|
23
|
+
first incident instead of firing again.
|
|
24
|
+
|
|
25
|
+
This is what makes the protocol "smart" rather than reflexive: a moderator
|
|
26
|
+
account that trips one soft signal doesn't get banned, a member the bot has
|
|
27
|
+
no hierarchy over doesn't get a failed API call retried on every message,
|
|
28
|
+
and twenty raid joins in ten seconds produce one aggregated log line
|
|
29
|
+
instead of twenty separate ones.
|
|
30
|
+
|
|
31
|
+
## The four layers
|
|
32
|
+
|
|
33
|
+
| Layer | Listens to | Containment it can do alone | Signals it reports |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `basicSecurity` | `messageCreate` | deletes the offending message | spam rate, duplicate flood, mention spam, scam/phishing language, suspicious links (via `PhishingDatabase`) |
|
|
36
|
+
| `antiRaid` | `guildMemberAdd` | raises verification level, ends lockdown automatically | fast join-rate spikes, alt-account scoring, and creation-time cluster correlation (see below) |
|
|
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
|
+
| `aiModeration` | `messageCreate` (independently of `basicSecurity`) | - | AI-confirmed category + confidence for gray-zone messages |
|
|
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.
|
|
45
|
+
|
|
46
|
+
`basicSecurity` and `aiModeration` both listen to `messageCreate`
|
|
47
|
+
independently rather than one calling into the other - either can be
|
|
48
|
+
enabled without the other, and if both happen to flag the same message,
|
|
49
|
+
`PunishmentEngine`'s per-member case debounce (see above) merges the two
|
|
50
|
+
signals into one incident rather than double-punishing.
|
|
51
|
+
|
|
52
|
+
## Evasion-resistant detections
|
|
53
|
+
|
|
54
|
+
Two additions specifically target attacks designed to slip past the
|
|
55
|
+
straightforward rate-counters above:
|
|
56
|
+
|
|
57
|
+
- **Join clustering** (`AntiRaidLayer._clusterScore`) - a bulk-registered
|
|
58
|
+
account farm can trickle members in slowly enough that the join-rate
|
|
59
|
+
counter never trips. Clustering instead asks whether several recent
|
|
60
|
+
joiners' accounts were all *created* within the same narrow window
|
|
61
|
+
(`antiRaid.cluster`), which doesn't depend on how fast they join at all.
|
|
62
|
+
- **Webhook message flooding** (`AntiNukeLayer._handleWebhookMessage`) - a
|
|
63
|
+
webhook message carries no guild member, so a "create a webhook, then
|
|
64
|
+
spam through it" nuke is invisible to every per-member spam check in the
|
|
65
|
+
library. AntiNuke remembers who created a webhook (`recentWebhooks`) for
|
|
66
|
+
a short window and, if that webhook then floods messages
|
|
67
|
+
(`antiNuke.webhookAbuse`), deletes the webhook and reports the original
|
|
68
|
+
creator to `PunishmentEngine` - the only path in the library that reaches
|
|
69
|
+
a member through something *other* than a direct per-member signal.
|
|
70
|
+
|
|
71
|
+
Both feed the same `ThreatEngine` / `PunishmentEngine` pipeline as
|
|
72
|
+
everything else; they're evasion-resistant in what they watch for, not in
|
|
73
|
+
how they respond.
|
|
74
|
+
|
|
75
|
+
## Why AntiNuke also runs a role audit
|
|
76
|
+
|
|
77
|
+
`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.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Publishing to npm
|
|
2
|
+
|
|
3
|
+
This covers turning this folder into a real `npm install glassframe-protocol`
|
|
4
|
+
package, and what actually happens to the code when you do.
|
|
5
|
+
|
|
6
|
+
## What "publish to npm" actually means for the code
|
|
7
|
+
|
|
8
|
+
`npm publish` uploads whatever `package.json`'s `"files"` field points at
|
|
9
|
+
(here: `dist/`, `docs/`, `examples/`, and the root `README.md` / `LICENSE` /
|
|
10
|
+
`GETTING_STARTED.md` / `CHANGELOG.md`) to the public npm registry. Anyone
|
|
11
|
+
who runs `npm install glassframe-protocol` downloads that and it lands as
|
|
12
|
+
plain text in their `node_modules/glassframe-protocol/` folder - Node has to
|
|
13
|
+
be able to parse it to run it, so there's no way to publish something
|
|
14
|
+
installable that isn't also readable on their disk. `src/`, the root
|
|
15
|
+
`config.js`, and the root `index.js` are **not** in `files`, so they never
|
|
16
|
+
get uploaded - only `dist/` does, which the build script (below) produces.
|
|
17
|
+
That's the real, honest ceiling of what "protection" a package publish can
|
|
18
|
+
offer; the [LICENSE](../LICENSE) is what actually restricts copying and
|
|
19
|
+
modifying it - that's a legal boundary, not a technical one.
|
|
20
|
+
|
|
21
|
+
## One-time setup
|
|
22
|
+
|
|
23
|
+
1. **Create an npm account** at https://www.npmjs.com/signup if you don't
|
|
24
|
+
have one, and verify your email.
|
|
25
|
+
2. **Fill in the placeholders** in `package.json`: `author`, `repository`,
|
|
26
|
+
`homepage`, `bugs`. If you don't have a public git repo for this, you can
|
|
27
|
+
delete those three fields entirely rather than leave `TODO` text in a
|
|
28
|
+
published package.
|
|
29
|
+
3. **Check the name is still free**: open
|
|
30
|
+
`https://www.npmjs.com/package/glassframe-protocol` in a browser. If it's
|
|
31
|
+
taken, either pick a different `name` in `package.json` or use a scoped
|
|
32
|
+
name like `@yourusername/glassframe-protocol` (scoped names are
|
|
33
|
+
basically always available, and `publishConfig.access: "public"` is
|
|
34
|
+
already set so a scoped package still publishes publicly rather than
|
|
35
|
+
defaulting to private).
|
|
36
|
+
4. **(Optional) Install terser** for real minification of what gets
|
|
37
|
+
published: `npm install --save-dev terser`. Without it, `npm run build`
|
|
38
|
+
still works, it just copies files through unminified - see
|
|
39
|
+
`scripts/build.js` for why a shortcut minifier isn't used instead.
|
|
40
|
+
|
|
41
|
+
## Publishing (same steps on Termux, Windows, and Linux - npm doesn't care)
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
npm login
|
|
45
|
+
```
|
|
46
|
+
Enter your npm username, password, and the one-time code from your
|
|
47
|
+
authenticator app (npm requires 2FA for publishing now). On Termux this
|
|
48
|
+
works exactly the same as any other terminal - it opens a login flow in
|
|
49
|
+
whatever browser is available, or accepts credentials directly depending on
|
|
50
|
+
your npm version.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
npm run build
|
|
54
|
+
```
|
|
55
|
+
Regenerates `dist/` from the current source. This also runs automatically
|
|
56
|
+
before publish (see `prepublishOnly` in `package.json`), but running it
|
|
57
|
+
yourself first lets you check the output in `dist/` before it goes out.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
npm publish
|
|
61
|
+
```
|
|
62
|
+
That's it - this uploads the package. `npm view glassframe-protocol` right
|
|
63
|
+
after should show your new version.
|
|
64
|
+
|
|
65
|
+
## Verifying it from a completely clean install
|
|
66
|
+
|
|
67
|
+
On any machine (Termux, Windows PowerShell/cmd, Linux terminal):
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
mkdir test-install && cd test-install
|
|
71
|
+
npm init -y
|
|
72
|
+
npm install glassframe-protocol discord.js
|
|
73
|
+
node -e "console.log(require('glassframe-protocol'))"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If that prints the `GlassFrame` class with no errors, the published package
|
|
77
|
+
resolves and loads correctly end to end.
|
|
78
|
+
|
|
79
|
+
## Publishing an update later
|
|
80
|
+
|
|
81
|
+
1. Make your changes in `src/` / `config.js` / `index.js` (never in `dist/`
|
|
82
|
+
directly - it gets overwritten).
|
|
83
|
+
2. Add an entry to `CHANGELOG.md` and bump `VERSION` in
|
|
84
|
+
`src/core/VersionInfo.js` to match.
|
|
85
|
+
3. Bump `"version"` in `package.json` to the same number
|
|
86
|
+
(`npm version patch` / `minor` / `major` does both the bump and creates a
|
|
87
|
+
git tag for you, if you're using git).
|
|
88
|
+
4. `npm publish` again - `prepublishOnly` rebuilds `dist/` automatically.
|
|
89
|
+
|
|
90
|
+
npm permanently reserves every version number you publish - you can
|
|
91
|
+
`npm unpublish` within a short window after publishing, but you can never
|
|
92
|
+
reuse the same version number afterward, so double-check `dist/` before
|
|
93
|
+
publishing rather than after.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { Client, GatewayIntentBits } = require("discord.js");
|
|
4
|
+
const GlassFrame = require("../index");
|
|
5
|
+
|
|
6
|
+
const client = new Client({
|
|
7
|
+
intents: [
|
|
8
|
+
GatewayIntentBits.Guilds,
|
|
9
|
+
GatewayIntentBits.GuildMembers,
|
|
10
|
+
GatewayIntentBits.GuildMessages,
|
|
11
|
+
GatewayIntentBits.MessageContent,
|
|
12
|
+
GatewayIntentBits.GuildModeration
|
|
13
|
+
]
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
client.once("ready", () => {
|
|
17
|
+
const frame = new GlassFrame(client, {
|
|
18
|
+
getLogChannel: async (guild) => {
|
|
19
|
+
return guild.channels.cache.find((c) => c.name === "security-logs") ?? null;
|
|
20
|
+
},
|
|
21
|
+
whitelist: [],
|
|
22
|
+
// Layers not listed here stay off until someone presses the panel button
|
|
23
|
+
// or you call frame.enableLayer(name) yourself.
|
|
24
|
+
autoStart: ["basicSecurity", "antiRaid", "antiNuke"],
|
|
25
|
+
debug: false,
|
|
26
|
+
config: {
|
|
27
|
+
aiModeration: {
|
|
28
|
+
// Supply one key or several - GlassFrame rotates across the pool and
|
|
29
|
+
// benches any key that comes back rate-limited.
|
|
30
|
+
apiKeys: (process.env.GROQ_API_KEYS || "").split(",").filter(Boolean)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
frame.on("warning", (w) => console.warn(`[GlassFrame:${w.layer}]`, w.message));
|
|
36
|
+
frame.punishmentEngine.on("case", (record) => {
|
|
37
|
+
console.log(`[GlassFrame] ${record.action} on ${record.userId} (tier ${record.tier})`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(`GlassFrame Protocol ready in ${client.guilds.cache.size} guild(s).`);
|
|
41
|
+
console.log('Send "!gf panel" in a channel (Manage Server permission) to open the control panel.');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
client.login(process.env.BOT_TOKEN);
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "glassframe-protocol",
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
+
"main": "dist/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"docs",
|
|
9
|
+
"examples",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"GETTING_STARTED.md",
|
|
13
|
+
"CHANGELOG.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node scripts/build.js",
|
|
17
|
+
"prepublishOnly": "node scripts/build.js",
|
|
18
|
+
"test": "echo \"See GETTING_STARTED.md - this package has no bundled test runner\" && exit 0"
|
|
19
|
+
},
|
|
20
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
21
|
+
"author": "Sarthak & Sashreek, suvrodey9@gmail.com",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"discord",
|
|
24
|
+
"discord.js",
|
|
25
|
+
"security",
|
|
26
|
+
"anti-raid",
|
|
27
|
+
"anti-nuke",
|
|
28
|
+
"moderation",
|
|
29
|
+
"groq",
|
|
30
|
+
"ai-moderation"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"discord.js": "^14.16.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"terser": "^5.31.0"
|
|
43
|
+
}
|
|
44
|
+
}
|