discord-guardian 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 discord-guardian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # discord-guardian
2
+
3
+ Test your Discord bot against simulated nukes without breaking anything.
4
+
5
+ [![npm](https://img.shields.io/npm/v/discord-guardian)](https://www.npmjs.com/package/discord-guardian)
6
+ [![license](https://img.shields.io/npm/l/discord-guardian)](./LICENSE)
7
+
8
+ ## Setup
9
+
10
+ ```js
11
+ import { guardian } from "discord-guardian"
12
+
13
+ guardian(client).auto()
14
+ ```
15
+
16
+ ## Simulation
17
+
18
+ Run detection logic and UI without real actions.
19
+
20
+ ```js
21
+ // simulate bans to trigger detection
22
+ guardian(client).simulateAttack({ type: "ban", count: 3 })
23
+ ```
24
+
25
+ Output
26
+ ```text
27
+ [guardian] simulation start (antiBan)
28
+ [guardian] step 1/3
29
+ [guardian] step 2/3
30
+ [guardian] antiBan -> count=3/3
31
+ [guardian] antiBan -> threshold reached
32
+ [guardian] simulation complete
33
+ ```
34
+
35
+ ## Logging
36
+
37
+ Logs threshold progress and triggers.
38
+
39
+ ```text
40
+ [guardian] antiKick -> count=2/3
41
+ [guardian] antiKick -> threshold reached
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - simulate attacks without real actions
47
+ - zero config setup via `.auto()`
48
+ - minimal, structured logs
49
+ - clear simulation indicators in Discord
50
+
51
+ ## Modules
52
+
53
+ | module | trigger |
54
+ |--------|---------|
55
+ | `antiBan` | mass member bans |
56
+ | `antiKick` | mass member kicks |
57
+ | `antiBotAdd` | unauthorized bot adds |
58
+ | `antiChannelDelete` | mass channel removals |
59
+
60
+ ## Permissions
61
+
62
+ - View Audit Log
63
+ - Ban Members
64
+ - Kick Members
65
+ - Manage Roles
66
+ - Send Messages
67
+
68
+ [MIT](./LICENSE)
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "discord-guardian",
3
+ "version": "1.0.1",
4
+ "description": "Test your Discord bot against simulated nukes and protect it with minimal setup.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node test/smoke.js"
12
+ },
13
+ "keywords": [
14
+ "discord",
15
+ "discordjs",
16
+ "discord-bot",
17
+ "antinuke",
18
+ "raid-protection",
19
+ "security",
20
+ "testing",
21
+ "sdk",
22
+ "nodejs"
23
+ ],
24
+ "author": "demondev_",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/demondevx/discord-guardian.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/demondevx/discord-guardian/issues"
32
+ },
33
+ "homepage": "https://github.com/demondevx/discord-guardian#readme",
34
+ "peerDependencies": {
35
+ "discord.js": "^14.26.2"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "files": [
41
+ "src/**/*",
42
+ "README.md",
43
+ "LICENSE"
44
+ ]
45
+ }
@@ -0,0 +1,24 @@
1
+ import { EventEmitter } from "events"
2
+
3
+ export class EventBus extends EventEmitter {
4
+ /**
5
+ * Emit an event.
6
+ */
7
+ emit(event, ...args) {
8
+ return super.emit(event, ...args)
9
+ }
10
+
11
+ /**
12
+ * Listen for an event.
13
+ */
14
+ on(event, listener) {
15
+ return super.on(event, listener)
16
+ }
17
+
18
+ /**
19
+ * Remove a listener.
20
+ */
21
+ off(event, listener) {
22
+ return super.removeListener(event, listener)
23
+ }
24
+ }
@@ -0,0 +1,104 @@
1
+ import { EventBus } from "./EventBus.js"
2
+ import { setupInteractionHandler } from "../utils/interactionHandler.js"
3
+
4
+ const MODULE_REGISTRY = {
5
+ antiBan: () => import("../modules/antiBan.js").then((m) => m.antiBan),
6
+ antiKick: () => import("../modules/antiKick.js").then((m) => m.antiKick),
7
+ antiBotAdd: () => import("../modules/antiBotAdd.js").then((m) => m.antiBotAdd),
8
+ antiChannelDelete: () => import("../modules/antiChannelDelete.js").then((m) => m.antiChannelDelete),
9
+ }
10
+
11
+ export class Guardian {
12
+ /**
13
+ * @param {import("discord.js").Client} client
14
+ */
15
+ constructor(client) {
16
+ if (!client) throw new Error("[guardian] client is required")
17
+
18
+ this.client = client
19
+ this.eventBus = new EventBus()
20
+ this._activeModules = new Map()
21
+
22
+ setupInteractionHandler(this.client, this.eventBus)
23
+ }
24
+
25
+ /**
26
+ * Initialize with default protections.
27
+ */
28
+ auto() {
29
+ return this.protectAll()
30
+ }
31
+
32
+ /**
33
+ * Enable all built-in modules.
34
+ */
35
+ protectAll() {
36
+ for (const name of Object.keys(MODULE_REGISTRY)) {
37
+ this.use(name)
38
+ }
39
+ return this
40
+ }
41
+
42
+ /**
43
+ * Enable a specific protection module.
44
+ * @param {string} moduleName
45
+ * @param {object} [config]
46
+ */
47
+ use(moduleName, config = {}) {
48
+ if (!MODULE_REGISTRY[moduleName]) return this
49
+ if (this._activeModules.has(moduleName)) return this
50
+
51
+ this._activeModules.set(moduleName, config)
52
+
53
+ MODULE_REGISTRY[moduleName]().then((initFn) => {
54
+ initFn(this.client, this.eventBus, config)
55
+ }).catch((err) => {
56
+ console.error(`[guardian] failed to load ${moduleName}:`, err)
57
+ })
58
+
59
+ return this
60
+ }
61
+
62
+ /**
63
+ * Simulate an attack for testing detection logic.
64
+ * @param {object} options
65
+ */
66
+ async simulateAttack({ type, count = 3 }) {
67
+ const moduleName = `anti${String(type).charAt(0).toUpperCase() + String(type).slice(1)}`
68
+
69
+ this._log(`simulation start (${moduleName})`)
70
+
71
+ for (let i = 1; i <= count; i++) {
72
+ await new Promise(r => setTimeout(r, 100))
73
+ this._log(`step ${i}/${count}`)
74
+
75
+ this.eventBus.emit(`simulate:${moduleName}`, {
76
+ executorId: "000000000000000000",
77
+ guild: {
78
+ id: "000000000000000000",
79
+ name: "simulation server",
80
+ members: this.client.users,
81
+ channels: { fetch: async () => null },
82
+ fetchAuditLogs: async () => ({ entries: new Map() })
83
+ }
84
+ })
85
+ }
86
+
87
+ this._log("simulation complete")
88
+ return this
89
+ }
90
+
91
+ /**
92
+ * Listen for guardian events.
93
+ * @param {string} event
94
+ * @param {Function} callback
95
+ */
96
+ on(event, callback) {
97
+ this.eventBus.on(event, callback)
98
+ return this
99
+ }
100
+
101
+ _log(message) {
102
+ console.log(`[guardian] ${message}`)
103
+ }
104
+ }
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { Guardian } from "./core/Guardian.js"
2
+
3
+ /**
4
+ * Initialize a new Guardian instance.
5
+ * @param {import("discord.js").Client} client
6
+ */
7
+ export function guardian(client) {
8
+ return new Guardian(client)
9
+ }
10
+
11
+ export { Guardian }
12
+ export { EventBus } from "./core/EventBus.js"
@@ -0,0 +1,69 @@
1
+ import { AuditLogEvent } from "discord.js"
2
+ import { RateLimiter } from "../utils/rateLimiter.js"
3
+ import { executePunishment } from "../utils/punishments.js"
4
+ import { createThreatMessage } from "../utils/threatMessage.js"
5
+
6
+ const DEFAULTS = {
7
+ maxBans: 3,
8
+ windowMs: 15_000,
9
+ punishment: "ban",
10
+ logChannelId: null,
11
+ whitelist: [],
12
+ }
13
+
14
+ /**
15
+ * Anti-ban protection module.
16
+ * @param {import("discord.js").Client} client
17
+ * @param {import("../core/EventBus.js").EventBus} eventBus
18
+ * @param {object} [config]
19
+ */
20
+ export function antiBan(client, eventBus, config = {}) {
21
+ const opts = { ...DEFAULTS, ...config }
22
+ const limiter = new RateLimiter(opts.maxBans, opts.windowMs)
23
+
24
+ const handle = async (executorId, guild, isSimulated = false) => {
25
+ if (opts.whitelist.includes(executorId)) return
26
+
27
+ if (!limiter.hit(executorId) && !isSimulated) return
28
+
29
+ if (limiter.count(executorId) >= opts.maxBans || isSimulated) {
30
+ if (limiter.count(executorId) === opts.maxBans || isSimulated) {
31
+ console.log(`[guardian] antiBan -> count=${limiter.count(executorId)}/${opts.maxBans}`)
32
+ console.log(`[guardian] antiBan -> threshold reached`)
33
+ }
34
+
35
+ eventBus.emit("threatDetected", {
36
+ type: "antiBan",
37
+ executorId,
38
+ guild,
39
+ isSimulated
40
+ })
41
+
42
+ const member = await guild.members.fetch(executorId).catch(() => null)
43
+ if (member) {
44
+ await executePunishment(guild, member, opts.punishment, "antiBan threshold exceeded", isSimulated)
45
+ }
46
+
47
+ const logChannelId = opts.logChannelId
48
+ if (logChannelId) {
49
+ const channel = await guild.channels.fetch(logChannelId).catch(() => null)
50
+ if (channel) {
51
+ await channel.send(createThreatMessage({
52
+ userId: executorId,
53
+ threatType: "antiBan",
54
+ isSimulated
55
+ })).catch(() => {})
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ client.on("guildBanAdd", async (ban) => {
62
+ const logs = await ban.guild.fetchAuditLogs({ type: AuditLogEvent.MemberBanAdd, limit: 1 }).catch(() => null)
63
+ const entry = logs?.entries.first()
64
+ if (!entry || entry.executor.id === client.user.id) return
65
+ handle(entry.executor.id, ban.guild)
66
+ })
67
+
68
+ eventBus.on("simulate:antiBan", (data) => handle(data.executorId, data.guild, true))
69
+ }
@@ -0,0 +1,71 @@
1
+ import { AuditLogEvent } from "discord.js"
2
+ import { RateLimiter } from "../utils/rateLimiter.js"
3
+ import { executePunishment } from "../utils/punishments.js"
4
+ import { createThreatMessage } from "../utils/threatMessage.js"
5
+
6
+ const DEFAULTS = {
7
+ maxBots: 2,
8
+ windowMs: 30_000,
9
+ punishment: "ban",
10
+ logChannelId: null,
11
+ whitelist: [],
12
+ }
13
+
14
+ /**
15
+ * Anti-bot protection module.
16
+ * @param {import("discord.js").Client} client
17
+ * @param {import("../core/EventBus.js").EventBus} eventBus
18
+ * @param {object} [config]
19
+ */
20
+ export function antiBotAdd(client, eventBus, config = {}) {
21
+ const opts = { ...DEFAULTS, ...config }
22
+ const limiter = new RateLimiter(opts.maxBots, opts.windowMs)
23
+
24
+ const handle = async (executorId, guild, isSimulated = false) => {
25
+ if (opts.whitelist.includes(executorId)) return
26
+
27
+ if (!limiter.hit(executorId) && !isSimulated) return
28
+
29
+ if (limiter.count(executorId) >= opts.maxBots || isSimulated) {
30
+ if (limiter.count(executorId) === opts.maxBots || isSimulated) {
31
+ console.log(`[guardian] antiBotAdd -> count=${limiter.count(executorId)}/${opts.maxBots}`)
32
+ console.log(`[guardian] antiBotAdd -> threshold reached`)
33
+ }
34
+
35
+ eventBus.emit("threatDetected", {
36
+ type: "antiBotAdd",
37
+ executorId,
38
+ guild,
39
+ isSimulated
40
+ })
41
+
42
+ const member = await guild.members.fetch(executorId).catch(() => null)
43
+ if (member) {
44
+ await executePunishment(guild, member, opts.punishment, "antiBotAdd threshold exceeded", isSimulated)
45
+ }
46
+
47
+ const logChannelId = opts.logChannelId
48
+ if (logChannelId) {
49
+ const channel = await guild.channels.fetch(logChannelId).catch(() => null)
50
+ if (channel) {
51
+ await channel.send(createThreatMessage({
52
+ userId: executorId,
53
+ threatType: "antiBotAdd",
54
+ isSimulated
55
+ })).catch(() => {})
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ client.on("guildMemberAdd", async (member) => {
62
+ if (!member.user.bot) return
63
+ const logs = await member.guild.fetchAuditLogs({ type: AuditLogEvent.BotAdd, limit: 1 }).catch(() => null)
64
+ const entry = logs?.entries.first()
65
+ if (!entry || (Date.now() - entry.createdTimestamp > 10000)) return
66
+ if (entry.target?.id !== member.id || entry.executor.id === client.user.id) return
67
+ handle(entry.executor.id, member.guild)
68
+ })
69
+
70
+ eventBus.on("simulate:antiBotAdd", (data) => handle(data.executorId, data.guild, true))
71
+ }
@@ -0,0 +1,70 @@
1
+ import { AuditLogEvent } from "discord.js"
2
+ import { RateLimiter } from "../utils/rateLimiter.js"
3
+ import { executePunishment } from "../utils/punishments.js"
4
+ import { createThreatMessage } from "../utils/threatMessage.js"
5
+
6
+ const DEFAULTS = {
7
+ maxDeletes: 3,
8
+ windowMs: 15_000,
9
+ punishment: "ban",
10
+ logChannelId: null,
11
+ whitelist: [],
12
+ }
13
+
14
+ /**
15
+ * Anti-channel-delete protection module.
16
+ * @param {import("discord.js").Client} client
17
+ * @param {import("../core/EventBus.js").EventBus} eventBus
18
+ * @param {object} [config]
19
+ */
20
+ export function antiChannelDelete(client, eventBus, config = {}) {
21
+ const opts = { ...DEFAULTS, ...config }
22
+ const limiter = new RateLimiter(opts.maxDeletes, opts.windowMs)
23
+
24
+ const handle = async (executorId, guild, isSimulated = false) => {
25
+ if (opts.whitelist.includes(executorId)) return
26
+
27
+ if (!limiter.hit(executorId) && !isSimulated) return
28
+
29
+ if (limiter.count(executorId) >= opts.maxDeletes || isSimulated) {
30
+ if (limiter.count(executorId) === opts.maxDeletes || isSimulated) {
31
+ console.log(`[guardian] antiChannelDelete -> count=${limiter.count(executorId)}/${opts.maxDeletes}`)
32
+ console.log(`[guardian] antiChannelDelete -> threshold reached`)
33
+ }
34
+
35
+ eventBus.emit("threatDetected", {
36
+ type: "antiChannelDelete",
37
+ executorId,
38
+ guild,
39
+ isSimulated
40
+ })
41
+
42
+ const member = await guild.members.fetch(executorId).catch(() => null)
43
+ if (member) {
44
+ await executePunishment(guild, member, opts.punishment, "antiChannelDelete threshold exceeded", isSimulated)
45
+ }
46
+
47
+ const logChannelId = opts.logChannelId
48
+ if (logChannelId) {
49
+ const channel = await guild.channels.fetch(logChannelId).catch(() => null)
50
+ if (channel) {
51
+ await channel.send(createThreatMessage({
52
+ userId: executorId,
53
+ threatType: "antiChannelDelete",
54
+ isSimulated
55
+ })).catch(() => {})
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ client.on("channelDelete", async (channel) => {
62
+ if (!channel.guild) return
63
+ const logs = await channel.guild.fetchAuditLogs({ type: AuditLogEvent.ChannelDelete, limit: 1 }).catch(() => null)
64
+ const entry = logs?.entries.first()
65
+ if (!entry || (Date.now() - entry.createdTimestamp > 5000) || entry.executor.id === client.user.id) return
66
+ handle(entry.executor.id, channel.guild)
67
+ })
68
+
69
+ eventBus.on("simulate:antiChannelDelete", (data) => handle(data.executorId, data.guild, true))
70
+ }
@@ -0,0 +1,70 @@
1
+ import { AuditLogEvent } from "discord.js"
2
+ import { RateLimiter } from "../utils/rateLimiter.js"
3
+ import { executePunishment } from "../utils/punishments.js"
4
+ import { createThreatMessage } from "../utils/threatMessage.js"
5
+
6
+ const DEFAULTS = {
7
+ maxKicks: 3,
8
+ windowMs: 15_000,
9
+ punishment: "ban",
10
+ logChannelId: null,
11
+ whitelist: [],
12
+ }
13
+
14
+ /**
15
+ * Anti-kick protection module.
16
+ * @param {import("discord.js").Client} client
17
+ * @param {import("../core/EventBus.js").EventBus} eventBus
18
+ * @param {object} [config]
19
+ */
20
+ export function antiKick(client, eventBus, config = {}) {
21
+ const opts = { ...DEFAULTS, ...config }
22
+ const limiter = new RateLimiter(opts.maxKicks, opts.windowMs)
23
+
24
+ const handle = async (executorId, guild, isSimulated = false) => {
25
+ if (opts.whitelist.includes(executorId)) return
26
+
27
+ if (!limiter.hit(executorId) && !isSimulated) return
28
+
29
+ if (limiter.count(executorId) >= opts.maxKicks || isSimulated) {
30
+ if (limiter.count(executorId) === opts.maxKicks || isSimulated) {
31
+ console.log(`[guardian] antiKick -> count=${limiter.count(executorId)}/${opts.maxKicks}`)
32
+ console.log(`[guardian] antiKick -> threshold reached`)
33
+ }
34
+
35
+ eventBus.emit("threatDetected", {
36
+ type: "antiKick",
37
+ executorId,
38
+ guild,
39
+ isSimulated
40
+ })
41
+
42
+ const member = await guild.members.fetch(executorId).catch(() => null)
43
+ if (member) {
44
+ await executePunishment(guild, member, opts.punishment, "antiKick threshold exceeded", isSimulated)
45
+ }
46
+
47
+ const logChannelId = opts.logChannelId
48
+ if (logChannelId) {
49
+ const channel = await guild.channels.fetch(logChannelId).catch(() => null)
50
+ if (channel) {
51
+ await channel.send(createThreatMessage({
52
+ userId: executorId,
53
+ threatType: "antiKick",
54
+ isSimulated
55
+ })).catch(() => {})
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ client.on("guildMemberRemove", async (member) => {
62
+ const logs = await member.guild.fetchAuditLogs({ type: AuditLogEvent.MemberKick, limit: 1 }).catch(() => null)
63
+ const entry = logs?.entries.first()
64
+ if (!entry || (Date.now() - entry.createdTimestamp > 5000)) return
65
+ if (entry.target?.id !== member.id || entry.executor.id === client.user.id) return
66
+ handle(entry.executor.id, member.guild)
67
+ })
68
+
69
+ eventBus.on("simulate:antiKick", (data) => handle(data.executorId, data.guild, true))
70
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Parses a Discord snowflake from varying input formats.
3
+ * @param {string} input
4
+ * @returns {string|null}
5
+ */
6
+ export function parseId(input) {
7
+ if (!input || typeof input !== "string") return null
8
+
9
+ const match = input.match(/^<[@#][!&]?(\d+)>$/)
10
+ if (match) return match[1]
11
+
12
+ const raw = input.trim()
13
+ if (/^\d{17,20}$/.test(raw)) return raw
14
+
15
+ return null
16
+ }
@@ -0,0 +1,74 @@
1
+ import { safeReply } from "./safeReply.js"
2
+ import { executePunishment } from "./punishments.js"
3
+
4
+ export function setupInteractionHandler(client, eventBus) {
5
+ client.on("interactionCreate", async (interaction) => {
6
+ try {
7
+ if (interaction.isButton()) {
8
+ const [scope, action, targetId] = interaction.customId.split(":")
9
+ if (scope !== "guardian") return
10
+
11
+ await interaction.deferReply({ ephemeral: true })
12
+
13
+ if (action === "punish") {
14
+ const guild = interaction.guild
15
+ if (!guild) return await safeReply(interaction, { content: "❌ Not in a guild." })
16
+
17
+ const member = await guild.members.fetch(targetId).catch(() => null)
18
+ if (!member) return await safeReply(interaction, { content: "❌ Member not found." })
19
+
20
+ await executePunishment(guild, member, "ban")
21
+ eventBus.emit("punishmentExecuted", {
22
+ type: "ban",
23
+ targetId,
24
+ executorId: interaction.user.id,
25
+ guild,
26
+ })
27
+
28
+ await safeReply(interaction, { content: `✅ **${member.user.tag}** banned.` })
29
+ }
30
+
31
+ if (action === "ignore") {
32
+ eventBus.emit("threatIgnored", {
33
+ targetId,
34
+ executorId: interaction.user.id,
35
+ guild: interaction.guild,
36
+ })
37
+ await safeReply(interaction, { content: "✅ Ignored." })
38
+ }
39
+ return
40
+ }
41
+
42
+ if (interaction.isStringSelectMenu()) {
43
+ const [scope, action, targetId] = interaction.customId.split(":")
44
+ if (scope !== "guardian") return
45
+
46
+ await interaction.deferReply({ ephemeral: true })
47
+
48
+ const type = interaction.values[0]
49
+ const guild = interaction.guild
50
+ if (!guild) return await safeReply(interaction, { content: "❌ Not in a guild." })
51
+
52
+ const member = await guild.members.fetch(targetId).catch(() => null)
53
+ if (!member) return await safeReply(interaction, { content: "❌ Member not found." })
54
+
55
+ await executePunishment(guild, member, type)
56
+ eventBus.emit("punishmentExecuted", {
57
+ type,
58
+ targetId,
59
+ executorId: interaction.user.id,
60
+ guild,
61
+ })
62
+
63
+ const labels = { ban: "banned", kick: "kicked", roles: "roles removed" }
64
+ await safeReply(interaction, { content: `✅ **${member.user.tag}** ${labels[type] || type}.` })
65
+ return
66
+ }
67
+ } catch (err) {
68
+ console.error("[Guardian] Interaction error:", err)
69
+ if (!interaction.replied && !interaction.deferred) {
70
+ await interaction.reply({ content: "❌ Internal error.", ephemeral: true }).catch(() => {})
71
+ }
72
+ }
73
+ })
74
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Execute a punishment on a member.
3
+ * @param {import("discord.js").Guild} guild
4
+ * @param {import("discord.js").GuildMember} member
5
+ * @param {"ban"|"kick"|"roles"} type
6
+ * @param {string} [reason]
7
+ * @param {boolean} [isSimulated]
8
+ */
9
+ export async function executePunishment(
10
+ guild,
11
+ member,
12
+ type,
13
+ reason = "Guardian Action",
14
+ isSimulated = false
15
+ ) {
16
+ if (isSimulated) return
17
+
18
+ switch (type) {
19
+ case "ban":
20
+ await guild.members.ban(member.id, { reason, deleteMessageSeconds: 0 })
21
+ break
22
+ case "kick":
23
+ await member.kick(reason)
24
+ break
25
+ case "roles":
26
+ await member.roles.set([], reason)
27
+ break
28
+ default:
29
+ console.warn(`[guardian] unknown punishment: ${type}`)
30
+ }
31
+ }
@@ -0,0 +1,47 @@
1
+ export class RateLimiter {
2
+ /**
3
+ * @param {number} limit
4
+ * @param {number} windowMs
5
+ */
6
+ constructor(limit, windowMs) {
7
+ this.limit = limit
8
+ this.windowMs = windowMs
9
+ this.hits = new Map()
10
+ this.intervals = new Map()
11
+ }
12
+
13
+ /**
14
+ * Register a hit and return true if limit is reached.
15
+ * @param {string} key
16
+ */
17
+ hit(key) {
18
+ const now = Date.now()
19
+ let userHits = this.hits.get(key) || []
20
+
21
+ userHits = userHits.filter(timestamp => now - timestamp < this.windowMs)
22
+ userHits.push(now)
23
+ this.hits.set(key, userHits)
24
+
25
+ return userHits.length >= this.limit
26
+ }
27
+
28
+ /**
29
+ * Get current hit count for a key.
30
+ * @param {string} key
31
+ */
32
+ count(key) {
33
+ return this.hits.get(key)?.length || 0
34
+ }
35
+
36
+ reset(key) {
37
+ this.hits.delete(key)
38
+ }
39
+
40
+ destroy() {
41
+ this.hits.clear()
42
+ for (const interval of this.intervals.values()) {
43
+ clearInterval(interval)
44
+ }
45
+ this.intervals.clear()
46
+ }
47
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Safely reply to a discord.js interaction.
3
+ * @param {import("discord.js").Interaction} interaction
4
+ * @param {import("discord.js").InteractionReplyOptions} options
5
+ */
6
+ export async function safeReply(interaction, options) {
7
+ try {
8
+ if (interaction.replied || interaction.deferred) {
9
+ return await interaction.editReply(options)
10
+ }
11
+ return await interaction.reply(options)
12
+ } catch (err) {
13
+ console.error("[guardian] safeReply error:", err.message)
14
+ }
15
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Build a threat-alert message.
3
+ * @param {object} opts
4
+ */
5
+ export function createThreatMessage({ userId, threatType, details, isSimulated }) {
6
+ const embed = new EmbedBuilder()
7
+ .setColor(0xff3b3b)
8
+ .setTitle(isSimulated ? "⚠️ Simulation Mode" : "Guardian — Threat Detected")
9
+ .setDescription(isSimulated ? "⚠️ Simulation Mode — no real actions will be taken" : "Suspicious activity filtered and blocked.")
10
+ .addFields(
11
+ { name: "User", value: `<@${userId}>`, inline: true },
12
+ { name: "Module", value: `\`${threatType}\``, inline: true },
13
+ )
14
+ .setTimestamp()
15
+
16
+ if (details) embed.addFields({ name: "Details", value: details })
17
+
18
+ const buttons = new ActionRowBuilder().addComponents(
19
+ new ButtonBuilder()
20
+ .setCustomId(`guardian:punish:${userId}`)
21
+ .setLabel(isSimulated ? "Simulate Punish" : "Punish User")
22
+ .setStyle(ButtonStyle.Danger),
23
+ new ButtonBuilder()
24
+ .setCustomId(`guardian:ignore:${userId}`)
25
+ .setLabel("Ignore")
26
+ .setStyle(ButtonStyle.Secondary),
27
+ )
28
+
29
+ const menu = new ActionRowBuilder().addComponents(
30
+ new StringSelectMenuBuilder()
31
+ .setCustomId(`guardian:select:${userId}`)
32
+ .setPlaceholder("Select punishment...")
33
+ .addOptions([
34
+ { label: "Ban", value: "ban" },
35
+ { label: "Kick", value: "kick" },
36
+ { label: "Remove Roles", value: "roles" },
37
+ ]),
38
+ )
39
+
40
+ return {
41
+ embeds: [embed],
42
+ components: [buttons, menu],
43
+ }
44
+ }