discordjs-security 1.4.0 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discordjs-security",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Security utilities for discord.js v14+ (Pure CommonJS JavaScript).",
5
5
  "keywords": [
6
6
  "discord",
@@ -11,6 +11,9 @@
11
11
  "redaction",
12
12
  "anti-spam"
13
13
  ],
14
+ "repository": {
15
+ "url": "https://github.com/WRLD-ENT/discordjs-security.git"
16
+ },
14
17
  "license": "MIT",
15
18
  "main": "index.js",
16
19
  "engines": {
@@ -0,0 +1,50 @@
1
+ function createCooldownManager() {
2
+ const buckets = new Map();
3
+
4
+ function makeKey(ctx, key) {
5
+ const u = ctx.userId || "0";
6
+ const g = ctx.guildId || "0";
7
+ const c = ctx.channelId || "0";
8
+
9
+ switch (key) {
10
+ case "guild": return `g:${g}`;
11
+ case "channel": return `c:${c}`;
12
+ case "user:guild": return `u:${u}:g:${g}`;
13
+ case "user:channel": return `u:${u}:c:${c}`;
14
+ default: return `u:${u}`;
15
+ }
16
+ }
17
+
18
+ function hit(ctx, opts) {
19
+ const now = Date.now();
20
+ const limit = opts.limit || 1;
21
+ const key = opts.key || "user";
22
+ const id = makeKey(ctx, key);
23
+
24
+ const existing = buckets.get(id);
25
+ if (!existing || existing.resetAt <= now) {
26
+ const resetAt = now + opts.durationMs;
27
+ buckets.set(id, { count: 1, resetAt });
28
+ return { ok: true, remaining: limit - 1, resetAt };
29
+ }
30
+
31
+ if (existing.count >= limit) {
32
+ return { ok: false, retryAfterMs: existing.resetAt - now, resetAt: existing.resetAt };
33
+ }
34
+
35
+ existing.count++;
36
+ return { ok: true, remaining: limit - existing.count, resetAt: existing.resetAt };
37
+ }
38
+
39
+ function reset(ctx, key = "user") {
40
+ buckets.delete(makeKey(ctx, key));
41
+ }
42
+
43
+ function clearAll() {
44
+ buckets.clear();
45
+ }
46
+
47
+ return { hit, reset, clearAll };
48
+ }
49
+
50
+ module.exports = { createCooldownManager };
package/src/filters.js ADDED
@@ -0,0 +1,14 @@
1
+ const INVITE_RE = /(discord\.gg|discord\.com\/invite)/i;
2
+ const EVERYONE_RE = /@everyone|@here/;
3
+
4
+ function filterContent(content, opts = {}) {
5
+ if (opts.blockInvites && INVITE_RE.test(content))
6
+ return { ok: false, reason: "Invites not allowed." };
7
+
8
+ if (opts.blockEveryoneMentions && EVERYONE_RE.test(content))
9
+ return { ok: false, reason: "Mass mentions not allowed." };
10
+
11
+ return { ok: true };
12
+ }
13
+
14
+ module.exports = { filterContent };
package/src/guards.js ADDED
@@ -0,0 +1,40 @@
1
+ function guardGuildOnly(interaction) {
2
+ if (!interaction.inGuild()) return { ok: false, reason: "Guild only." };
3
+ return { ok: true };
4
+ }
5
+
6
+ function guardOwner(interaction, opts) {
7
+ if (!opts.ownerIds.includes(interaction.user.id))
8
+ return { ok: false, reason: "Not authorized." };
9
+ return { ok: true };
10
+ }
11
+
12
+ function guardPermissions(interaction, opts) {
13
+ if (!interaction.inGuild()) return { ok: true };
14
+ const member = interaction.member;
15
+ const perms = member && member.permissions;
16
+
17
+ if (!perms) return { ok: false, reason: "Missing permission context." };
18
+
19
+ const required = opts.permissions || [];
20
+ const ok = required.every(p => perms.has(p));
21
+ if (!ok) return { ok: false, reason: "Missing permissions." };
22
+
23
+ return { ok: true };
24
+ }
25
+
26
+ function guardRoles(interaction, opts) {
27
+ if (!interaction.inGuild()) return { ok: true };
28
+ const roles = interaction.member && interaction.member.roles && interaction.member.roles.cache;
29
+ if (!roles) return { ok: false, reason: "Missing role context." };
30
+
31
+ const requireAll = opts.requireAll === true;
32
+ const ok = requireAll
33
+ ? opts.roleIds.every(id => roles.has(id))
34
+ : opts.roleIds.some(id => roles.has(id));
35
+
36
+ if (!ok) return { ok: false, reason: "Missing role." };
37
+ return { ok: true };
38
+ }
39
+
40
+ module.exports = { guardGuildOnly, guardOwner, guardPermissions, guardRoles };
package/src/logger.js ADDED
@@ -0,0 +1,23 @@
1
+ const util = require("node:util");
2
+ const { redact } = require("./redact");
3
+
4
+ function createSecureLogger(opts = {}) {
5
+ const name = opts.name || "discordjs-security";
6
+
7
+ function log(level, fmt, ...args) {
8
+ const msg = redact(util.format(fmt, ...args), opts);
9
+ const out = `[${new Date().toISOString()}] [${name}] [${level}] ${msg}`;
10
+ if (level === "ERROR") console.error(out);
11
+ else if (level === "WARN") console.warn(out);
12
+ else console.log(out);
13
+ }
14
+
15
+ return {
16
+ info: (f, ...a) => log("INFO", f, ...a),
17
+ warn: (f, ...a) => log("WARN", f, ...a),
18
+ error: (f, ...a) => log("ERROR", f, ...a),
19
+ debug: (f, ...a) => log("DEBUG", f, ...a)
20
+ };
21
+ }
22
+
23
+ module.exports = { createSecureLogger };
package/src/redact.js ADDED
@@ -0,0 +1,47 @@
1
+ const DEFAULT_MASK = "[REDACTED]";
2
+
3
+ const TOKEN_PATTERNS = [
4
+ /\b(mfa\.[\w-]{80,})\b/g,
5
+ /\b([\w-]{24}\.[\w-]{6}\.[\w-]{27})\b/g,
6
+ /\b(sk-[A-Za-z0-9]{20,})\b/g
7
+ ];
8
+
9
+ const JWT_LIKE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g;
10
+
11
+ function escapeRegExp(s) {
12
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }
14
+
15
+ function redact(input, options = {}) {
16
+ const mask = options.mask || DEFAULT_MASK;
17
+ const heuristic = options.heuristic !== false;
18
+ let out = input;
19
+
20
+ if (options.extraSecrets && Array.isArray(options.extraSecrets)) {
21
+ for (const secret of options.extraSecrets) {
22
+ if (!secret) continue;
23
+ const re = new RegExp(escapeRegExp(secret), "g");
24
+ out = out.replace(re, mask);
25
+ }
26
+ }
27
+
28
+ if (heuristic) {
29
+ for (const re of TOKEN_PATTERNS) out = out.replace(re, mask);
30
+ out = out.replace(JWT_LIKE, mask);
31
+ }
32
+
33
+ return out;
34
+ }
35
+
36
+ function redactAny(value, options = {}) {
37
+ if (typeof value === "string") return redact(value, options);
38
+ if (Array.isArray(value)) return value.map(v => redactAny(v, options));
39
+ if (value && typeof value === "object") {
40
+ const out = {};
41
+ for (const k in value) out[k] = redactAny(value[k], options);
42
+ return out;
43
+ }
44
+ return value;
45
+ }
46
+
47
+ module.exports = { redact, redactAny };
@@ -0,0 +1,26 @@
1
+ function normalizeAllowedMentions(am) {
2
+ return {
3
+ parse: (am && am.parse) || [],
4
+ users: am && am.users,
5
+ roles: am && am.roles,
6
+ repliedUser: am && am.repliedUser === true
7
+ };
8
+ }
9
+
10
+ async function safeReply(interaction, options) {
11
+ const allowedMentions = normalizeAllowedMentions(options.allowedMentions);
12
+ const payload = { ...options, allowedMentions };
13
+
14
+ if (interaction.deferred || interaction.replied) {
15
+ return interaction.followUp(payload);
16
+ }
17
+ return interaction.reply(payload);
18
+ }
19
+
20
+ async function safeSend(target, options) {
21
+ const allowedMentions = normalizeAllowedMentions(options.allowedMentions);
22
+ const payload = { ...options, allowedMentions };
23
+ return target.send(payload);
24
+ }
25
+
26
+ module.exports = { safeReply, safeSend };