enerthya.dev-web-common 1.0.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/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # enerthya.dev-web-common
2
+
3
+ > Shared web layer for the Enerthya ecosystem.
4
+ > Inspired by Loritta's `loritta-website:web-common` + `loritta-dashboard:dashboard-common` modules.
5
+
6
+ ---
7
+
8
+ ## What's inside
9
+
10
+ | Export | What it does |
11
+ |--------|-------------|
12
+ | `API_ROUTES` | Central source of truth for all API route paths — never type route strings twice |
13
+ | `HTTP_STATUS` | Named HTTP status code constants (no more magic numbers) |
14
+ | `createSuccess(data)` | Wraps a payload in `{ ok: true, data }` |
15
+ | `createError(status, message, code?)` | Wraps an error in `{ ok: false, status, message, code? }` |
16
+ | `isApiSuccess(value)` | Returns true if value is a success envelope |
17
+ | `isApiError(value)` | Returns true if value is an error envelope |
18
+ | `API_ERRORS` | Pre-built common errors: `NOT_AUTHENTICATED`, `FORBIDDEN`, `NOT_FOUND`, `INTERNAL`, `BOT_OFFLINE` |
19
+ | `UrlUtils` | Dashboard URL builders + snowflake helpers |
20
+ | `ParamValidator` | Validates route params and request bodies — returns `{ valid, error? }` |
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install enerthya.dev-web-common
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Usage
33
+
34
+ ### API_ROUTES
35
+
36
+ ```js
37
+ const { API_ROUTES } = require("enerthya.dev-web-common");
38
+
39
+ // Static routes
40
+ API_ROUTES.AUTH.USER // "/auth/user"
41
+ API_ROUTES.AUTH.LOGOUT // "/auth/logout"
42
+ API_ROUTES.GUILDS // "/guilds"
43
+ API_ROUTES.STATUS // "/status"
44
+ API_ROUTES.COMMANDS // "/commands"
45
+ API_ROUTES.DAILY.STATUS // "/daily"
46
+ API_ROUTES.DAILY.CAPTCHA // "/daily/captcha"
47
+ API_ROUTES.DAILY.CLAIM // "/daily/claim"
48
+
49
+ // Dynamic routes
50
+ API_ROUTES.GUILD_CONFIG("123456") // "/guilds/123456/config"
51
+ API_ROUTES.GUILD_WARNS("123456") // "/guilds/123456/warns"
52
+ API_ROUTES.GUILD_WARN("123456", "warn_id") // "/guilds/123456/warns/warn_id"
53
+ API_ROUTES.GUILD_COMMAND_SETTINGS("123456") // "/guilds/123456/command-settings"
54
+ ```
55
+
56
+ ### Response envelopes
57
+
58
+ ```js
59
+ const { createSuccess, createError, isApiError } = require("enerthya.dev-web-common");
60
+
61
+ // In an Express route handler:
62
+ res.json(createSuccess({ guilds: [...] }));
63
+ res.status(403).json(createError(403, "Forbidden", "MISSING_PERMISSION"));
64
+
65
+ // In a React component:
66
+ const { data } = await axios.get(API_ROUTES.GUILD_CONFIG(guildId));
67
+ if (isApiError(data)) throw new Error(data.message);
68
+ const config = data.data; // strongly typed payload
69
+ ```
70
+
71
+ ### ParamValidator
72
+
73
+ ```js
74
+ const { ParamValidator, createError, HTTP_STATUS } = require("enerthya.dev-web-common");
75
+
76
+ router.get("/guilds/:id/config", CheckAuth, async (req, res) => {
77
+ const check = ParamValidator.guildId(req.params.id);
78
+ if (!check.valid) {
79
+ return res.status(HTTP_STATUS.BAD_REQUEST).json(createError(400, check.error));
80
+ }
81
+ // ... continue safely
82
+ });
83
+ ```
84
+
85
+ Available validators:
86
+ - `ParamValidator.guildId(id)` — Discord snowflake
87
+ - `ParamValidator.objectId(id)` — MongoDB ObjectId (24 hex chars)
88
+ - `ParamValidator.requiredString(value, fieldName, { minLength?, maxLength? })`
89
+ - `ParamValidator.optionalString(value, fieldName, { maxLength? })`
90
+ - `ParamValidator.boolean(value, fieldName)`
91
+ - `ParamValidator.nonNegativeInt(value, fieldName, { max? })`
92
+ - `ParamValidator.hexColor(value, fieldName)`
93
+ - `ParamValidator.snowflake(id, fieldName?)`
94
+ - `ParamValidator.all(...results)` — runs multiple validators, returns first failure
95
+
96
+ ### UrlUtils
97
+
98
+ ```js
99
+ const { UrlUtils } = require("enerthya.dev-web-common");
100
+
101
+ UrlUtils.guildBase("123456") // "/dashboard/123456"
102
+ UrlUtils.guildModule("123456", "welcome") // "/dashboard/123456/welcome"
103
+ UrlUtils.parseGuildId("/dashboard/123456/overview") // "123456"
104
+ UrlUtils.isValidSnowflake("123456789012345678") // true
105
+ UrlUtils.absolute("https://enerthya.dev", "/dashboard/123456") // full URL
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Test
111
+
112
+ ```bash
113
+ node test.js
114
+ # 84/84 passing
115
+ ```
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "enerthya.dev-web-common",
3
+ "version": "1.0.0",
4
+ "description": "Shared web layer for the Enerthya ecosystem: API routes, response envelopes, URL helpers and validators. Inspired by Loritta's web-common and dashboard-common modules.",
5
+ "main": "src/index.js",
6
+ "keywords": [
7
+ "enerthya",
8
+ "discord",
9
+ "web",
10
+ "api",
11
+ "routes",
12
+ "common"
13
+ ],
14
+ "author": "Enerthya",
15
+ "license": "MIT",
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "files": [
20
+ "src/",
21
+ "README.md"
22
+ ],
23
+ "dependencies": {}
24
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * HTTP_STATUS — Named HTTP status codes used across the Enerthya stack.
3
+ *
4
+ * Use these instead of magic numbers in route handlers and client error checks.
5
+ *
6
+ * @example
7
+ * res.status(HTTP_STATUS.UNAUTHORIZED).json(createError(401, "Not authenticated"));
8
+ */
9
+ const HTTP_STATUS = {
10
+ OK: 200,
11
+ CREATED: 201,
12
+ NO_CONTENT: 204,
13
+ BAD_REQUEST: 400,
14
+ UNAUTHORIZED: 401,
15
+ FORBIDDEN: 403,
16
+ NOT_FOUND: 404,
17
+ CONFLICT: 409,
18
+ UNPROCESSABLE: 422,
19
+ TOO_MANY_REQUESTS: 429,
20
+ INTERNAL_SERVER_ERROR: 500,
21
+ SERVICE_UNAVAILABLE: 503,
22
+ };
23
+
24
+ module.exports = { HTTP_STATUS };
@@ -0,0 +1,4 @@
1
+ const { API_ROUTES } = require("./routes");
2
+ const { HTTP_STATUS } = require("./httpStatus");
3
+
4
+ module.exports = { API_ROUTES, HTTP_STATUS };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * API_ROUTES — Central source of truth for all API route paths.
3
+ *
4
+ * Every route is defined once here and imported everywhere else.
5
+ * Prevents strings from diverging between server/routes/ and client/src/.
6
+ *
7
+ * Usage:
8
+ * const { API_ROUTES } = require("enerthya.dev-web-common");
9
+ * API_ROUTES.GUILD_CONFIG("123456") // => "/guilds/123456/config"
10
+ * API_ROUTES.AUTH.USER // => "/auth/user"
11
+ *
12
+ * All paths are relative (no leading "/api/") — the client's axios baseURL
13
+ * already includes "/api", and the server mounts on "/api" as well.
14
+ */
15
+
16
+ const API_ROUTES = {
17
+ // ─── Auth ────────────────────────────────────────────────────────────────
18
+ AUTH: {
19
+ /** GET — OAuth2 login redirect */
20
+ LOGIN: "/auth/login",
21
+ /** GET — OAuth2 callback */
22
+ CALLBACK: "/auth/callback",
23
+ /** POST — Logout and destroy session */
24
+ LOGOUT: "/auth/logout",
25
+ /** GET — Current authenticated user */
26
+ USER: "/auth/user",
27
+ },
28
+
29
+ // ─── Guilds ──────────────────────────────────────────────────────────────
30
+ /** GET — All guilds the user manages */
31
+ GUILDS: "/guilds",
32
+ /**
33
+ * GET / POST — Guild config
34
+ * @param {string} id - Discord guild ID
35
+ */
36
+ GUILD_CONFIG: (id) => `/guilds/${id}/config`,
37
+ /**
38
+ * GET — Warns for a guild
39
+ * @param {string} id - Discord guild ID
40
+ */
41
+ GUILD_WARNS: (id) => `/guilds/${id}/warns`,
42
+ /**
43
+ * DELETE — Remove a specific warn
44
+ * @param {string} id - Discord guild ID
45
+ * @param {string} warnId - Warn document ID
46
+ */
47
+ GUILD_WARN: (id, warnId) => `/guilds/${id}/warns/${warnId}`,
48
+ /**
49
+ * GET — Command-level permission settings for a guild
50
+ * @param {string} id - Discord guild ID
51
+ */
52
+ GUILD_COMMAND_SETTINGS: (id) => `/guilds/${id}/command-settings`,
53
+
54
+ // ─── Daily ───────────────────────────────────────────────────────────────
55
+ DAILY: {
56
+ /** GET — Daily status (coins, streak, next claim) */
57
+ STATUS: "/daily",
58
+ /** GET — Captcha for daily claim */
59
+ CAPTCHA: "/daily/captcha",
60
+ /** POST — Claim daily reward */
61
+ CLAIM: "/daily/claim",
62
+ },
63
+
64
+ // ─── Meta ─────────────────────────────────────────────────────────────────
65
+ /** GET — Bot + shard status */
66
+ STATUS: "/status",
67
+ /** GET — All registered commands */
68
+ COMMANDS: "/commands",
69
+ /** GET — Dev-only diagnostics */
70
+ DEV: "/dev",
71
+ };
72
+
73
+ module.exports = { API_ROUTES };
package/src/index.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * enerthya.dev-web-common — Shared web layer for the Enerthya ecosystem.
3
+ *
4
+ * Inspired by Loritta's `loritta-website:web-common` and
5
+ * `loritta-dashboard:dashboard-common` modules (LorittaBot/Loritta).
6
+ * Adapted for Node.js / JavaScript.
7
+ *
8
+ * What this package provides:
9
+ * - API_ROUTES : Central source of truth for all API route paths
10
+ * - HTTP_STATUS : Named HTTP status code constants
11
+ * - createSuccess / createError : Standardised API response envelopes
12
+ * - isApiSuccess / isApiError : Type guards for API responses
13
+ * - API_ERRORS : Pre-built common error responses
14
+ * - UrlUtils : Dashboard URL builders and snowflake helpers
15
+ * - ParamValidator: Validates route params and request bodies
16
+ *
17
+ * @example
18
+ * // Server (Express route handler)
19
+ * const { API_ROUTES, createSuccess, createError, ParamValidator, HTTP_STATUS } =
20
+ * require("enerthya.dev-web-common");
21
+ *
22
+ * router.get(API_ROUTES.GUILD_CONFIG(":id"), CheckAuth, async (req, res) => {
23
+ * const check = ParamValidator.guildId(req.params.id);
24
+ * if (!check.valid) return res.status(HTTP_STATUS.BAD_REQUEST).json(createError(400, check.error));
25
+ * const config = await Guild.findOne({ guildId: req.params.id });
26
+ * res.json(createSuccess(config));
27
+ * });
28
+ *
29
+ * // Client (React / axios)
30
+ * const { API_ROUTES, isApiError } = require("enerthya.dev-web-common");
31
+ * const { data } = await axios.get(API_ROUTES.GUILD_CONFIG(guildId));
32
+ * if (isApiError(data)) throw new Error(data.message);
33
+ */
34
+
35
+ const { API_ROUTES, HTTP_STATUS } = require("./constants/index");
36
+ const { UrlUtils, ParamValidator } = require("./utils/index");
37
+ const {
38
+ createSuccess,
39
+ createError,
40
+ isApiSuccess,
41
+ isApiError,
42
+ API_ERRORS,
43
+ } = require("./structures/index");
44
+
45
+ module.exports = {
46
+ // Constants
47
+ API_ROUTES,
48
+ HTTP_STATUS,
49
+
50
+ // Response helpers
51
+ createSuccess,
52
+ createError,
53
+ isApiSuccess,
54
+ isApiError,
55
+ API_ERRORS,
56
+
57
+ // URL + validation utilities
58
+ UrlUtils,
59
+ ParamValidator,
60
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Response factories — Standardised API response shapes.
3
+ *
4
+ * Inspired by Loritta's `loritta-website:web-common` typed response wrappers.
5
+ *
6
+ * All server routes should use these factories so the client always receives
7
+ * a predictable envelope. The client uses `isApiError` / `isApiSuccess` to
8
+ * branch on the result without checking HTTP status codes manually.
9
+ *
10
+ * @example
11
+ * // In an Express route handler:
12
+ * res.json(createSuccess({ guilds: [...] }));
13
+ * res.status(403).json(createError(403, "Forbidden", "MISSING_PERMISSION"));
14
+ *
15
+ * // In a React component:
16
+ * const data = await axios.get(...);
17
+ * if (isApiError(data.data)) { ... }
18
+ */
19
+
20
+ /**
21
+ * Builds a successful API response envelope.
22
+ *
23
+ * @template T
24
+ * @param {T} data - Payload to embed in the response.
25
+ * @returns {{ ok: true, data: T }}
26
+ */
27
+ function createSuccess(data) {
28
+ return { ok: true, data };
29
+ }
30
+
31
+ /**
32
+ * Builds an error API response envelope.
33
+ *
34
+ * @param {number} status - HTTP status code (for reference in the body).
35
+ * @param {string} message - Human-readable error message.
36
+ * @param {string} [code] - Optional machine-readable error code (UPPER_SNAKE).
37
+ * @returns {{ ok: false, status: number, message: string, code?: string }}
38
+ */
39
+ function createError(status, message, code) {
40
+ const response = { ok: false, status, message };
41
+ if (code !== undefined) response.code = code;
42
+ return response;
43
+ }
44
+
45
+ /**
46
+ * Returns true if the value looks like a successful API response.
47
+ *
48
+ * @param {unknown} value
49
+ * @returns {boolean}
50
+ */
51
+ function isApiSuccess(value) {
52
+ return (
53
+ value !== null &&
54
+ typeof value === "object" &&
55
+ value.ok === true &&
56
+ "data" in value
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Returns true if the value looks like an error API response.
62
+ *
63
+ * @param {unknown} value
64
+ * @returns {boolean}
65
+ */
66
+ function isApiError(value) {
67
+ return (
68
+ value !== null &&
69
+ typeof value === "object" &&
70
+ value.ok === false &&
71
+ typeof value.message === "string"
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Common pre-built error responses.
77
+ * Use these constants to avoid typos in repeated error messages.
78
+ */
79
+ const API_ERRORS = {
80
+ NOT_AUTHENTICATED: createError(401, "Not authenticated", "NOT_AUTHENTICATED"),
81
+ FORBIDDEN: createError(403, "Access forbidden", "FORBIDDEN"),
82
+ NOT_FOUND: createError(404, "Resource not found", "NOT_FOUND"),
83
+ INTERNAL: createError(500, "Internal server error", "INTERNAL_SERVER_ERROR"),
84
+ BOT_OFFLINE: createError(503, "Bot is offline or unreachable", "BOT_OFFLINE"),
85
+ };
86
+
87
+ module.exports = {
88
+ createSuccess,
89
+ createError,
90
+ isApiSuccess,
91
+ isApiError,
92
+ API_ERRORS,
93
+ };
@@ -0,0 +1,15 @@
1
+ const {
2
+ createSuccess,
3
+ createError,
4
+ isApiSuccess,
5
+ isApiError,
6
+ API_ERRORS,
7
+ } = require("./ApiResponse");
8
+
9
+ module.exports = {
10
+ createSuccess,
11
+ createError,
12
+ isApiSuccess,
13
+ isApiError,
14
+ API_ERRORS,
15
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * ParamValidator — Validates route parameters and request bodies.
3
+ *
4
+ * Used in Express route handlers to validate inputs before touching
5
+ * the database. Returns structured results instead of throwing, so
6
+ * routes can decide how to respond.
7
+ *
8
+ * @example
9
+ * const { ParamValidator } = require("enerthya.dev-web-common");
10
+ *
11
+ * const result = ParamValidator.guildId(req.params.id);
12
+ * if (!result.valid) return res.status(400).json(createError(400, result.error));
13
+ */
14
+
15
+ const ParamValidator = {
16
+ /**
17
+ * Validates a Discord guild ID (snowflake).
18
+ * @param {unknown} id
19
+ * @returns {{ valid: boolean, error?: string }}
20
+ */
21
+ guildId(id) {
22
+ if (typeof id !== "string" || !/^\d{17,20}$/.test(id)) {
23
+ return { valid: false, error: "Invalid guild ID — must be a 17-20 digit Discord snowflake" };
24
+ }
25
+ return { valid: true };
26
+ },
27
+
28
+ /**
29
+ * Validates a MongoDB ObjectId string (24 hex chars).
30
+ * @param {unknown} id
31
+ * @returns {{ valid: boolean, error?: string }}
32
+ */
33
+ objectId(id) {
34
+ if (typeof id !== "string" || !/^[a-f0-9]{24}$/i.test(id)) {
35
+ return { valid: false, error: "Invalid ID — must be a 24-character hex string" };
36
+ }
37
+ return { valid: true };
38
+ },
39
+
40
+ /**
41
+ * Validates a required string field in a request body.
42
+ * @param {unknown} value
43
+ * @param {string} fieldName - Used in the error message.
44
+ * @param {{ minLength?: number, maxLength?: number }} [options]
45
+ * @returns {{ valid: boolean, error?: string }}
46
+ */
47
+ requiredString(value, fieldName, options = {}) {
48
+ if (typeof value !== "string" || value.trim() === "") {
49
+ return { valid: false, error: `"${fieldName}" is required and must be a non-empty string` };
50
+ }
51
+ const { minLength, maxLength } = options;
52
+ if (minLength !== undefined && value.length < minLength) {
53
+ return { valid: false, error: `"${fieldName}" must be at least ${minLength} characters` };
54
+ }
55
+ if (maxLength !== undefined && value.length > maxLength) {
56
+ return { valid: false, error: `"${fieldName}" must be at most ${maxLength} characters` };
57
+ }
58
+ return { valid: true };
59
+ },
60
+
61
+ /**
62
+ * Validates an optional string field — passes if undefined/null, fails if wrong type.
63
+ * @param {unknown} value
64
+ * @param {string} fieldName
65
+ * @param {{ maxLength?: number }} [options]
66
+ * @returns {{ valid: boolean, error?: string }}
67
+ */
68
+ optionalString(value, fieldName, options = {}) {
69
+ if (value === undefined || value === null) return { valid: true };
70
+ if (typeof value !== "string") {
71
+ return { valid: false, error: `"${fieldName}" must be a string or null` };
72
+ }
73
+ const { maxLength } = options;
74
+ if (maxLength !== undefined && value.length > maxLength) {
75
+ return { valid: false, error: `"${fieldName}" must be at most ${maxLength} characters` };
76
+ }
77
+ return { valid: true };
78
+ },
79
+
80
+ /**
81
+ * Validates a boolean field.
82
+ * @param {unknown} value
83
+ * @param {string} fieldName
84
+ * @returns {{ valid: boolean, error?: string }}
85
+ */
86
+ boolean(value, fieldName) {
87
+ if (typeof value !== "boolean") {
88
+ return { valid: false, error: `"${fieldName}" must be a boolean (true or false)` };
89
+ }
90
+ return { valid: true };
91
+ },
92
+
93
+ /**
94
+ * Validates a non-negative integer field.
95
+ * @param {unknown} value
96
+ * @param {string} fieldName
97
+ * @param {{ max?: number }} [options]
98
+ * @returns {{ valid: boolean, error?: string }}
99
+ */
100
+ nonNegativeInt(value, fieldName, options = {}) {
101
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
102
+ return { valid: false, error: `"${fieldName}" must be a non-negative integer` };
103
+ }
104
+ const { max } = options;
105
+ if (max !== undefined && value > max) {
106
+ return { valid: false, error: `"${fieldName}" must be at most ${max}` };
107
+ }
108
+ return { valid: true };
109
+ },
110
+
111
+ /**
112
+ * Validates a hex color string (e.g. "#5865F2" or "#FFF").
113
+ * @param {unknown} value
114
+ * @param {string} fieldName
115
+ * @returns {{ valid: boolean, error?: string }}
116
+ */
117
+ hexColor(value, fieldName) {
118
+ if (typeof value !== "string" || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value)) {
119
+ return { valid: false, error: `"${fieldName}" must be a valid hex color (e.g. #5865F2)` };
120
+ }
121
+ return { valid: true };
122
+ },
123
+
124
+ /**
125
+ * Validates a Discord snowflake (channel ID, role ID, user ID, etc.).
126
+ * Alias for guildId — same format.
127
+ * @param {unknown} id
128
+ * @param {string} [fieldName]
129
+ * @returns {{ valid: boolean, error?: string }}
130
+ */
131
+ snowflake(id, fieldName = "ID") {
132
+ if (typeof id !== "string" || !/^\d{17,20}$/.test(id)) {
133
+ return { valid: false, error: `"${fieldName}" must be a valid Discord snowflake ID` };
134
+ }
135
+ return { valid: true };
136
+ },
137
+
138
+ /**
139
+ * Runs multiple validators and returns the first failure, or { valid: true }.
140
+ * @param {...{ valid: boolean, error?: string }} results
141
+ * @returns {{ valid: boolean, error?: string }}
142
+ */
143
+ all(...results) {
144
+ for (const result of results) {
145
+ if (!result.valid) return result;
146
+ }
147
+ return { valid: true };
148
+ },
149
+ };
150
+
151
+ module.exports = { ParamValidator };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * UrlUtils — URL building and parsing helpers for the Enerthya dashboard.
3
+ *
4
+ * Centralises all path construction so route strings never drift between
5
+ * the server and the client.
6
+ *
7
+ * @example
8
+ * UrlUtils.guildBase("123456") // "/dashboard/123456"
9
+ * UrlUtils.guildModule("123456", "xp-rates") // "/dashboard/123456/xp-rates"
10
+ * UrlUtils.parseGuildId("/dashboard/123456/overview") // "123456"
11
+ */
12
+
13
+ const DASHBOARD_PREFIX = "/dashboard";
14
+
15
+ const UrlUtils = {
16
+ /**
17
+ * Root path for a guild in the dashboard.
18
+ * @param {string} guildId
19
+ * @returns {string}
20
+ */
21
+ guildBase(guildId) {
22
+ if (!guildId || typeof guildId !== "string") {
23
+ throw new TypeError("guildId must be a non-empty string");
24
+ }
25
+ return `${DASHBOARD_PREFIX}/${guildId}`;
26
+ },
27
+
28
+ /**
29
+ * Path to a specific module page inside a guild.
30
+ * @param {string} guildId
31
+ * @param {string} module - e.g. "welcome", "xp-rates", "ticket"
32
+ * @returns {string}
33
+ */
34
+ guildModule(guildId, module) {
35
+ if (!module || typeof module !== "string") {
36
+ throw new TypeError("module must be a non-empty string");
37
+ }
38
+ return `${UrlUtils.guildBase(guildId)}/${module}`;
39
+ },
40
+
41
+ /**
42
+ * Extracts the guild ID from a dashboard URL path.
43
+ * Returns null if the path is not a valid guild path.
44
+ * @param {string} pathname
45
+ * @returns {string|null}
46
+ */
47
+ parseGuildId(pathname) {
48
+ if (typeof pathname !== "string") return null;
49
+ const match = pathname.match(/^\/dashboard\/(\d{17,20})/);
50
+ return match ? match[1] : null;
51
+ },
52
+
53
+ /**
54
+ * Returns true if the given string looks like a valid Discord snowflake ID.
55
+ * @param {string|unknown} id
56
+ * @returns {boolean}
57
+ */
58
+ isValidSnowflake(id) {
59
+ return typeof id === "string" && /^\d{17,20}$/.test(id);
60
+ },
61
+
62
+ /**
63
+ * Builds a full absolute URL given a base and a relative path.
64
+ * Safely handles trailing slashes on the base.
65
+ * @param {string} base - e.g. "https://enerthya.dev"
66
+ * @param {string} path - e.g. "/dashboard/123456/welcome"
67
+ * @returns {string}
68
+ */
69
+ absolute(base, path) {
70
+ const cleanBase = base.replace(/\/+$/, "");
71
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
72
+ return `${cleanBase}${cleanPath}`;
73
+ },
74
+ };
75
+
76
+ module.exports = { UrlUtils };
@@ -0,0 +1,4 @@
1
+ const { UrlUtils } = require("./UrlUtils");
2
+ const { ParamValidator } = require("./ParamValidator");
3
+
4
+ module.exports = { UrlUtils, ParamValidator };