@bigfootds/bigfootds-service-utils 0.1.0 → 1.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 BigfootDS
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BigfootDS
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 CHANGED
@@ -4,14 +4,14 @@ Reusable service-side utilities for BigfootDS microservices.
4
4
 
5
5
  This package helps services apply the same request, error, and internal-caller conventions without copying helper code into each service. It is a package, not a service, and it does not own runtime data.
6
6
 
7
- ## First-Pass Scope
7
+ ## To-Do List
8
8
 
9
- - request ID generation, validation, inbound resolution, active request metadata, and response-header wiring
10
- - standard JSON error helpers built from the global error catalogue in `@bigfootds/bigfootds-shared-data`
11
- - framework-neutral bearer service-token verification and service caller policy results
12
- - thin Express-style adapters for request IDs, service-token verification, and standard error responses
9
+ - [x] Morgan logging helpers
10
+ - [x] Profanity matching/normalisation helpers.
11
+ - [ ] Broad validation helpers
12
+ - [ ] Audit helpers
13
+ - [ ] Admin/operator bulk helpers
13
14
 
14
- Deferred areas include broad validation helpers, audit helpers, Morgan logging helpers, admin/operator bulk helpers, and profanity matching/normalisation helpers.
15
15
 
16
16
  ## Public Package Safety
17
17
 
@@ -25,6 +25,8 @@ Service-token helpers accept token values at runtime from the consuming service.
25
25
 
26
26
  ## Installation
27
27
 
28
+ Install using the organisatio-scoped name like below:
29
+
28
30
  ```sh
29
31
  npm install @bigfootds/bigfootds-service-utils
30
32
  ```
@@ -120,6 +122,48 @@ app.post(
120
122
  app.use(standardErrorHandler());
121
123
  ```
122
124
 
125
+ ## Morgan Logging
126
+
127
+ Use the BigfootDS Morgan logger to emit one-line request logs similar to the current microservice convention, with request ID support added:
128
+
129
+ ```ts
130
+ import {
131
+ createBigfootDSMorganLogger,
132
+ requestIdMiddleware
133
+ } from "@bigfootds/bigfootds-service-utils";
134
+
135
+ app.use(requestIdMiddleware());
136
+ app.use(createBigfootDSMorganLogger());
137
+ ```
138
+
139
+ Logging is disabled by default when `NODE_ENV` is `test`. The default format is exported as `BIGFOOTDS_MORGAN_FORMAT` if a service needs to pass it to Morgan directly.
140
+
141
+ ## Profanity And Restricted Words
142
+
143
+ Runtime profanity handling lives here, while the static word lists and metadata stay in `@bigfootds/bigfootds-shared-data`.
144
+
145
+ ```ts
146
+ import {
147
+ chatProfanityHandler,
148
+ playerNameProfanityHandler
149
+ } from "@bigfootds/bigfootds-service-utils";
150
+
151
+ const chatHasProfanity = chatProfanityHandler.exists("I like big butts and I cannot lie");
152
+ const nameResult = playerNameProfanityHandler.check("BigfootDS_Admin");
153
+ ```
154
+
155
+ Use the lower-level helpers when a service needs direct list matching or normalisation:
156
+
157
+ ```ts
158
+ import {
159
+ findProfanityListMatches,
160
+ normalizeModerationText
161
+ } from "@bigfootds/bigfootds-service-utils";
162
+
163
+ const normalised = normalizeModerationText(" BigfootDS\tAdmin ");
164
+ const matches = findProfanityListMatches(normalised);
165
+ ```
166
+
123
167
  ## Package Boundary
124
168
 
125
169
  `pkg-service-utils` depends on `@bigfootds/bigfootds-shared-data` for stable Project Definitions, error-code metadata, and shared response data shapes.
package/dist/index.d.ts CHANGED
@@ -2,3 +2,5 @@ export * from "./requestIds";
2
2
  export * from "./errors";
3
3
  export * from "./serviceTokens";
4
4
  export * from "./express";
5
+ export * from "./logging";
6
+ export * from "./profanity";
package/dist/index.js CHANGED
@@ -18,3 +18,5 @@ __exportStar(require("./requestIds"), exports);
18
18
  __exportStar(require("./errors"), exports);
19
19
  __exportStar(require("./serviceTokens"), exports);
20
20
  __exportStar(require("./express"), exports);
21
+ __exportStar(require("./logging"), exports);
22
+ __exportStar(require("./profanity"), exports);
@@ -0,0 +1,53 @@
1
+ import morgan, { type Options as MorganOptions } from "morgan";
2
+ import type { IncomingMessage, ServerResponse } from "node:http";
3
+ import { type HeaderRecord } from "./requestIds";
4
+ /**
5
+ * Morgan token name for BigfootDS request IDs.
6
+ */
7
+ export declare const MORGAN_REQUEST_ID_TOKEN_NAME = "bigfootds-request-id";
8
+ /**
9
+ * Single-line Morgan format for BigfootDS microservice console logs.
10
+ */
11
+ export declare const BIGFOOTDS_MORGAN_FORMAT: string;
12
+ /**
13
+ * Minimal request shape used by the request ID Morgan token.
14
+ */
15
+ export interface MorganRequestIdSource {
16
+ readonly headers?: HeaderRecord;
17
+ readonly bigfootds?: {
18
+ readonly requestId?: string;
19
+ };
20
+ }
21
+ /**
22
+ * Options for creating the BigfootDS Morgan request logger.
23
+ */
24
+ export interface BigfootDSMorganOptions<Request extends IncomingMessage, Response extends ServerResponse> extends MorganOptions<Request, Response> {
25
+ /**
26
+ * Whether to create an active Morgan logger. Defaults to disabled in `NODE_ENV=test`.
27
+ */
28
+ readonly enabled?: boolean;
29
+ /**
30
+ * Morgan format string. Defaults to the BigfootDS single-line format.
31
+ */
32
+ readonly format?: string;
33
+ /**
34
+ * Whether to register BigfootDS custom Morgan tokens before creating the logger.
35
+ */
36
+ readonly registerTokens?: boolean;
37
+ }
38
+ /**
39
+ * Returns whether request logging should be active for an environment.
40
+ */
41
+ export declare function shouldLogRequests(nodeEnv?: string | undefined): boolean;
42
+ /**
43
+ * Gets the request ID value used by the BigfootDS Morgan token.
44
+ */
45
+ export declare function getMorganRequestIdToken(request: MorganRequestIdSource): string;
46
+ /**
47
+ * Registers BigfootDS custom Morgan tokens on a Morgan-compatible instance.
48
+ */
49
+ export declare function registerBigfootDSMorganTokens(morganInstance?: typeof morgan): void;
50
+ /**
51
+ * Creates a Morgan request logger configured for BigfootDS microservices.
52
+ */
53
+ export declare function createBigfootDSMorganLogger<Request extends IncomingMessage = IncomingMessage, Response extends ServerResponse = ServerResponse>(options?: BigfootDSMorganOptions<Request, Response>): ReturnType<typeof morgan<Request, Response>>;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BIGFOOTDS_MORGAN_FORMAT = exports.MORGAN_REQUEST_ID_TOKEN_NAME = void 0;
7
+ exports.shouldLogRequests = shouldLogRequests;
8
+ exports.getMorganRequestIdToken = getMorganRequestIdToken;
9
+ exports.registerBigfootDSMorganTokens = registerBigfootDSMorganTokens;
10
+ exports.createBigfootDSMorganLogger = createBigfootDSMorganLogger;
11
+ const morgan_1 = __importDefault(require("morgan"));
12
+ const requestIds_1 = require("./requestIds");
13
+ /**
14
+ * Morgan token name for BigfootDS request IDs.
15
+ */
16
+ exports.MORGAN_REQUEST_ID_TOKEN_NAME = "bigfootds-request-id";
17
+ /**
18
+ * Single-line Morgan format for BigfootDS microservice console logs.
19
+ */
20
+ exports.BIGFOOTDS_MORGAN_FORMAT = [
21
+ ":date[iso]",
22
+ "RequestId :bigfootds-request-id,",
23
+ "Method :method,",
24
+ "URL :url,",
25
+ "Status :status,",
26
+ "ResponseBytes :res[content-length],",
27
+ "ResponseTime :response-time ms,",
28
+ "Referrer :referrer,",
29
+ "UserAgent :user-agent"
30
+ ].join(" ");
31
+ /**
32
+ * Returns whether request logging should be active for an environment.
33
+ */
34
+ function shouldLogRequests(nodeEnv = process.env.NODE_ENV) {
35
+ return nodeEnv !== "test";
36
+ }
37
+ /**
38
+ * Gets the request ID value used by the BigfootDS Morgan token.
39
+ */
40
+ function getMorganRequestIdToken(request) {
41
+ return request.bigfootds?.requestId
42
+ ?? (0, requestIds_1.getHeaderValue)(request.headers, requestIds_1.REQUEST_ID_HEADER_NAME)
43
+ ?? "-";
44
+ }
45
+ /**
46
+ * Registers BigfootDS custom Morgan tokens on a Morgan-compatible instance.
47
+ */
48
+ function registerBigfootDSMorganTokens(morganInstance = morgan_1.default) {
49
+ morganInstance.token(exports.MORGAN_REQUEST_ID_TOKEN_NAME, (request) => {
50
+ return getMorganRequestIdToken(request);
51
+ });
52
+ }
53
+ /**
54
+ * Creates a Morgan request logger configured for BigfootDS microservices.
55
+ */
56
+ function createBigfootDSMorganLogger(options = {}) {
57
+ const { enabled = shouldLogRequests(), format = exports.BIGFOOTDS_MORGAN_FORMAT, registerTokens = true, ...morganOptions } = options;
58
+ if (!enabled) {
59
+ return ((_request, _response, next) => {
60
+ next();
61
+ });
62
+ }
63
+ if (registerTokens) {
64
+ registerBigfootDSMorganTokens();
65
+ }
66
+ return (0, morgan_1.default)(format, morganOptions);
67
+ }
@@ -0,0 +1,164 @@
1
+ import { CensorType } from "@2toad/profanity";
2
+ import { type ProfanityListDefinition, type ProfanityListId } from "@bigfootds/bigfootds-shared-data";
3
+ export { CensorType, Profanity, ProfanityOptions, profaneWords, profanity } from "@2toad/profanity";
4
+ /**
5
+ * Language codes that `@2toad/profanity` supports without BigfootDS custom datasets.
6
+ */
7
+ export declare const supportedProfanityLanguages: readonly ["ar", "zh", "en", "fr", "de", "hi", "it", "ja", "ko", "pt", "ru", "es"];
8
+ /**
9
+ * Union of language codes supported directly by the upstream profanity package.
10
+ */
11
+ export type SupportedProfanityLanguage = typeof supportedProfanityLanguages[number];
12
+ /**
13
+ * Metadata for a BigfootDS localisation target and its profanity support status.
14
+ */
15
+ export interface TargetProfanityLanguage {
16
+ readonly name: string;
17
+ readonly locale: string;
18
+ readonly profanityLanguage?: SupportedProfanityLanguage;
19
+ readonly supportedByDefault: boolean;
20
+ }
21
+ /**
22
+ * BigfootDS localisation targets and their current profanity-detection coverage.
23
+ */
24
+ export declare const targetProfanityLanguages: readonly TargetProfanityLanguage[];
25
+ /**
26
+ * Default profanity language set used when callers do not provide a language option.
27
+ */
28
+ export declare const defaultProfanityLanguages: readonly SupportedProfanityLanguage[];
29
+ /**
30
+ * Shared language options accepted by profanity handlers.
31
+ */
32
+ export interface ProfanityLanguageOptions {
33
+ readonly languages?: readonly SupportedProfanityLanguage[];
34
+ }
35
+ /**
36
+ * Options for censoring chat text.
37
+ */
38
+ export interface ChatCensorOptions extends ProfanityLanguageOptions {
39
+ readonly censorType?: CensorType;
40
+ }
41
+ /**
42
+ * Options for checking player-facing names.
43
+ */
44
+ export interface PlayerNameCheckOptions extends ProfanityLanguageOptions {
45
+ readonly includeReservedWords?: boolean;
46
+ readonly includeDevWords?: boolean;
47
+ }
48
+ /**
49
+ * Detailed outcome from checking a player/user-controlled name.
50
+ */
51
+ export interface PlayerNameCheckResult {
52
+ readonly isAllowed: boolean;
53
+ readonly hasProfanity: boolean;
54
+ readonly hasReservedWord: boolean;
55
+ readonly hasDevWord: boolean;
56
+ readonly matches: {
57
+ readonly reservedWords: readonly string[];
58
+ readonly devWords: readonly string[];
59
+ };
60
+ }
61
+ /**
62
+ * Unicode normalisation forms supported by JavaScript.
63
+ */
64
+ export type ModerationUnicodeNormalForm = "NFC" | "NFD" | "NFKC" | "NFKD";
65
+ /**
66
+ * Text normalisation options for moderation and restricted-word checks.
67
+ */
68
+ export interface NormalizeModerationTextOptions {
69
+ readonly unicodeForm?: ModerationUnicodeNormalForm;
70
+ readonly trim?: boolean;
71
+ readonly collapseWhitespace?: boolean;
72
+ readonly lowerCase?: boolean;
73
+ readonly locale?: string | readonly string[];
74
+ }
75
+ /**
76
+ * Matching mode for static profanity/restricted-word list checks.
77
+ */
78
+ export type ProfanityMatchMode = "substring" | "whole_word";
79
+ /**
80
+ * Options for static profanity/restricted-word list matching.
81
+ */
82
+ export interface FindProfanityListMatchesOptions {
83
+ readonly listIds?: readonly ProfanityListId[];
84
+ readonly lists?: readonly ProfanityListDefinition[];
85
+ readonly mode?: ProfanityMatchMode;
86
+ readonly normalization?: NormalizeModerationTextOptions;
87
+ }
88
+ /**
89
+ * Static profanity/restricted-word match.
90
+ */
91
+ export interface ProfanityListMatch {
92
+ readonly listId: ProfanityListId;
93
+ readonly listDisplayName: string;
94
+ readonly word: string;
95
+ readonly normalizedWord: string;
96
+ readonly index: number;
97
+ readonly mode: ProfanityMatchMode;
98
+ }
99
+ /**
100
+ * Result from checking a value against static BigfootDS profanity/restricted-word lists.
101
+ */
102
+ export interface ProfanityListCheckResult {
103
+ readonly isAllowed: boolean;
104
+ readonly matches: readonly ProfanityListMatch[];
105
+ }
106
+ /**
107
+ * Checks whether a string is a language code supported directly by `@2toad/profanity`.
108
+ */
109
+ export declare function isSupportedProfanityLanguage(language: string): language is SupportedProfanityLanguage;
110
+ /**
111
+ * Resolves an application locale to an upstream profanity language when one is available.
112
+ */
113
+ export declare function getSupportedProfanityLanguageForLocale(locale: string): SupportedProfanityLanguage | undefined;
114
+ /**
115
+ * Normalises text for moderation-sensitive comparisons.
116
+ */
117
+ export declare function normalizeModerationText(value: string, options?: NormalizeModerationTextOptions): string;
118
+ /**
119
+ * Normalises a value before comparing it with BigfootDS reserved or developer word lists.
120
+ */
121
+ export declare function normalizeRestrictedWord(value: string): string;
122
+ /**
123
+ * Finds BigfootDS restricted words contained within a candidate value.
124
+ */
125
+ export declare function findRestrictedWords(value: string, words: readonly string[]): string[];
126
+ /**
127
+ * Finds static profanity/restricted-word list matches in a candidate value.
128
+ */
129
+ export declare function findProfanityListMatches(value: string, options?: FindProfanityListMatchesOptions): readonly ProfanityListMatch[];
130
+ /**
131
+ * Checks whether a value has any static profanity/restricted-word list matches.
132
+ */
133
+ export declare function hasProfanityListMatch(value: string, options?: FindProfanityListMatchesOptions): boolean;
134
+ /**
135
+ * Returns a pass/fail result plus static list matches.
136
+ */
137
+ export declare function checkProfanityList(value: string, options?: FindProfanityListMatchesOptions): ProfanityListCheckResult;
138
+ /**
139
+ * Profanity-only handler for chat and other free-text surfaces.
140
+ */
141
+ export declare const chatProfanityHandler: {
142
+ exists(text: string, options?: ProfanityLanguageOptions): boolean;
143
+ censor(text: string, options?: ChatCensorOptions): string;
144
+ };
145
+ /**
146
+ * Handler for player/user-controlled names.
147
+ */
148
+ export declare const playerNameProfanityHandler: {
149
+ check(value: string, options?: PlayerNameCheckOptions): PlayerNameCheckResult;
150
+ isAllowed(value: string, options?: PlayerNameCheckOptions): boolean;
151
+ };
152
+ /**
153
+ * Grouped handlers for consumers that prefer context-based property access.
154
+ */
155
+ export declare const ProfanityHandlers: {
156
+ chat: {
157
+ exists(text: string, options?: ProfanityLanguageOptions): boolean;
158
+ censor(text: string, options?: ChatCensorOptions): string;
159
+ };
160
+ playerName: {
161
+ check(value: string, options?: PlayerNameCheckOptions): PlayerNameCheckResult;
162
+ isAllowed(value: string, options?: PlayerNameCheckOptions): boolean;
163
+ };
164
+ };
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProfanityHandlers = exports.playerNameProfanityHandler = exports.chatProfanityHandler = exports.defaultProfanityLanguages = exports.targetProfanityLanguages = exports.supportedProfanityLanguages = exports.profanity = exports.profaneWords = exports.ProfanityOptions = exports.Profanity = exports.CensorType = void 0;
4
+ exports.isSupportedProfanityLanguage = isSupportedProfanityLanguage;
5
+ exports.getSupportedProfanityLanguageForLocale = getSupportedProfanityLanguageForLocale;
6
+ exports.normalizeModerationText = normalizeModerationText;
7
+ exports.normalizeRestrictedWord = normalizeRestrictedWord;
8
+ exports.findRestrictedWords = findRestrictedWords;
9
+ exports.findProfanityListMatches = findProfanityListMatches;
10
+ exports.hasProfanityListMatch = hasProfanityListMatch;
11
+ exports.checkProfanityList = checkProfanityList;
12
+ const profanity_1 = require("@2toad/profanity");
13
+ const bigfootds_shared_data_1 = require("@bigfootds/bigfootds-shared-data");
14
+ var profanity_2 = require("@2toad/profanity");
15
+ Object.defineProperty(exports, "CensorType", { enumerable: true, get: function () { return profanity_2.CensorType; } });
16
+ Object.defineProperty(exports, "Profanity", { enumerable: true, get: function () { return profanity_2.Profanity; } });
17
+ Object.defineProperty(exports, "ProfanityOptions", { enumerable: true, get: function () { return profanity_2.ProfanityOptions; } });
18
+ Object.defineProperty(exports, "profaneWords", { enumerable: true, get: function () { return profanity_2.profaneWords; } });
19
+ Object.defineProperty(exports, "profanity", { enumerable: true, get: function () { return profanity_2.profanity; } });
20
+ /**
21
+ * Language codes that `@2toad/profanity` supports without BigfootDS custom datasets.
22
+ */
23
+ exports.supportedProfanityLanguages = [
24
+ "ar",
25
+ "zh",
26
+ "en",
27
+ "fr",
28
+ "de",
29
+ "hi",
30
+ "it",
31
+ "ja",
32
+ "ko",
33
+ "pt",
34
+ "ru",
35
+ "es"
36
+ ];
37
+ /**
38
+ * BigfootDS localisation targets and their current profanity-detection coverage.
39
+ */
40
+ exports.targetProfanityLanguages = [
41
+ { name: "English", locale: "en", profanityLanguage: "en", supportedByDefault: true },
42
+ { name: "French", locale: "fr", profanityLanguage: "fr", supportedByDefault: true },
43
+ { name: "Italian", locale: "it", profanityLanguage: "it", supportedByDefault: true },
44
+ { name: "German", locale: "de", profanityLanguage: "de", supportedByDefault: true },
45
+ { name: "Spanish", locale: "es", profanityLanguage: "es", supportedByDefault: true },
46
+ { name: "Portuguese", locale: "pt", profanityLanguage: "pt", supportedByDefault: true },
47
+ { name: "Portuguese Brazilian", locale: "pt-BR", profanityLanguage: "pt", supportedByDefault: true },
48
+ { name: "Dutch", locale: "nl", supportedByDefault: false },
49
+ { name: "Turkish", locale: "tr", supportedByDefault: false },
50
+ { name: "Japanese", locale: "ja", profanityLanguage: "ja", supportedByDefault: true },
51
+ { name: "Korean", locale: "ko", profanityLanguage: "ko", supportedByDefault: true },
52
+ { name: "Mandarin Chinese", locale: "zh", profanityLanguage: "zh", supportedByDefault: true },
53
+ { name: "Russian", locale: "ru", profanityLanguage: "ru", supportedByDefault: true },
54
+ { name: "Ukrainian", locale: "uk", supportedByDefault: false },
55
+ { name: "Malay", locale: "ms", supportedByDefault: false },
56
+ { name: "Indonesian", locale: "id", supportedByDefault: false },
57
+ { name: "Vietnamese", locale: "vi", supportedByDefault: false },
58
+ { name: "Tagalog", locale: "tl", supportedByDefault: false },
59
+ { name: "Hindi", locale: "hi", profanityLanguage: "hi", supportedByDefault: true },
60
+ { name: "Urdu", locale: "ur", supportedByDefault: false },
61
+ { name: "Bengali", locale: "bn", supportedByDefault: false },
62
+ { name: "Marathi", locale: "mr", supportedByDefault: false },
63
+ { name: "Telugu", locale: "te", supportedByDefault: false },
64
+ { name: "Tamil", locale: "ta", supportedByDefault: false },
65
+ { name: "Arabic", locale: "ar", profanityLanguage: "ar", supportedByDefault: true }
66
+ ];
67
+ /**
68
+ * Default profanity language set used when callers do not provide a language option.
69
+ */
70
+ exports.defaultProfanityLanguages = ["en"];
71
+ const chatProfanity = new profanity_1.Profanity({
72
+ languages: [...exports.defaultProfanityLanguages],
73
+ wholeWord: true,
74
+ unicodeWordBoundaries: true
75
+ });
76
+ const playerNameProfanity = new profanity_1.Profanity({
77
+ languages: [...exports.defaultProfanityLanguages],
78
+ wholeWord: false,
79
+ unicodeWordBoundaries: true
80
+ });
81
+ /**
82
+ * Checks whether a string is a language code supported directly by `@2toad/profanity`.
83
+ */
84
+ function isSupportedProfanityLanguage(language) {
85
+ return exports.supportedProfanityLanguages.includes(language);
86
+ }
87
+ /**
88
+ * Resolves an application locale to an upstream profanity language when one is available.
89
+ */
90
+ function getSupportedProfanityLanguageForLocale(locale) {
91
+ const normalizedLocale = locale.trim().toLowerCase();
92
+ const exactMatch = exports.targetProfanityLanguages.find((language) => language.locale.toLowerCase() === normalizedLocale);
93
+ if (exactMatch) {
94
+ return exactMatch.profanityLanguage;
95
+ }
96
+ const baseLocale = normalizedLocale.split("-")[0];
97
+ if (isSupportedProfanityLanguage(baseLocale)) {
98
+ return baseLocale;
99
+ }
100
+ return undefined;
101
+ }
102
+ /**
103
+ * Normalises text for moderation-sensitive comparisons.
104
+ */
105
+ function normalizeModerationText(value, options = {}) {
106
+ const { unicodeForm = "NFKC", trim = true, collapseWhitespace = true, lowerCase = true, locale } = options;
107
+ let normalized = value.normalize(unicodeForm);
108
+ if (trim) {
109
+ normalized = normalized.trim();
110
+ }
111
+ if (collapseWhitespace) {
112
+ normalized = normalized.replace(/\s+/g, " ");
113
+ }
114
+ if (lowerCase) {
115
+ normalized = normalized.toLocaleLowerCase(locale);
116
+ }
117
+ return normalized;
118
+ }
119
+ /**
120
+ * Normalises a value before comparing it with BigfootDS reserved or developer word lists.
121
+ */
122
+ function normalizeRestrictedWord(value) {
123
+ return value.normalize("NFKC").trim().toLowerCase();
124
+ }
125
+ /**
126
+ * Finds BigfootDS restricted words contained within a candidate value.
127
+ */
128
+ function findRestrictedWords(value, words) {
129
+ const normalizedValue = normalizeRestrictedWord(value);
130
+ if (!normalizedValue) {
131
+ return [];
132
+ }
133
+ return words.filter((word) => {
134
+ const normalizedWord = normalizeRestrictedWord(word);
135
+ return normalizedWord.length > 0 && normalizedValue.includes(normalizedWord);
136
+ });
137
+ }
138
+ /**
139
+ * Finds static profanity/restricted-word list matches in a candidate value.
140
+ */
141
+ function findProfanityListMatches(value, options = {}) {
142
+ const normalizedValue = normalizeModerationText(value, options.normalization);
143
+ if (normalizedValue === "") {
144
+ return [];
145
+ }
146
+ const lists = resolveProfanityLists(options);
147
+ const matches = [];
148
+ for (const list of lists) {
149
+ const mode = options.mode ?? resolveDefaultMatchMode(list);
150
+ for (const word of list.words) {
151
+ const normalizedWord = normalizeModerationText(word, options.normalization);
152
+ if (normalizedWord === "") {
153
+ continue;
154
+ }
155
+ const index = mode === "whole_word"
156
+ ? findWholeWordIndex(normalizedValue, normalizedWord)
157
+ : normalizedValue.indexOf(normalizedWord);
158
+ if (index >= 0) {
159
+ matches.push({
160
+ listId: list.id,
161
+ listDisplayName: list.displayName,
162
+ word,
163
+ normalizedWord,
164
+ index,
165
+ mode
166
+ });
167
+ }
168
+ }
169
+ }
170
+ return matches;
171
+ }
172
+ /**
173
+ * Checks whether a value has any static profanity/restricted-word list matches.
174
+ */
175
+ function hasProfanityListMatch(value, options = {}) {
176
+ return findProfanityListMatches(value, options).length > 0;
177
+ }
178
+ /**
179
+ * Returns a pass/fail result plus static list matches.
180
+ */
181
+ function checkProfanityList(value, options = {}) {
182
+ const matches = findProfanityListMatches(value, options);
183
+ return {
184
+ isAllowed: matches.length === 0,
185
+ matches
186
+ };
187
+ }
188
+ /**
189
+ * Profanity-only handler for chat and other free-text surfaces.
190
+ */
191
+ exports.chatProfanityHandler = {
192
+ exists(text, options = {}) {
193
+ return chatProfanity.exists(text, resolveLanguages(options.languages));
194
+ },
195
+ censor(text, options = {}) {
196
+ return chatProfanity.censor(text, options.censorType ?? profanity_1.CensorType.Word, resolveLanguages(options.languages));
197
+ }
198
+ };
199
+ /**
200
+ * Handler for player/user-controlled names.
201
+ */
202
+ exports.playerNameProfanityHandler = {
203
+ check(value, options = {}) {
204
+ const hasProfanity = playerNameProfanity.exists(value, resolveLanguages(options.languages));
205
+ const reservedWords = options.includeReservedWords === false
206
+ ? []
207
+ : findRestrictedWords(value, bigfootds_shared_data_1.reservedWordsArray);
208
+ const devWords = options.includeDevWords === false
209
+ ? []
210
+ : findRestrictedWords(value, bigfootds_shared_data_1.devWordsArray);
211
+ return {
212
+ isAllowed: !hasProfanity && reservedWords.length === 0 && devWords.length === 0,
213
+ hasProfanity,
214
+ hasReservedWord: reservedWords.length > 0,
215
+ hasDevWord: devWords.length > 0,
216
+ matches: {
217
+ reservedWords,
218
+ devWords
219
+ }
220
+ };
221
+ },
222
+ isAllowed(value, options = {}) {
223
+ return exports.playerNameProfanityHandler.check(value, options).isAllowed;
224
+ }
225
+ };
226
+ /**
227
+ * Grouped handlers for consumers that prefer context-based property access.
228
+ */
229
+ exports.ProfanityHandlers = {
230
+ chat: exports.chatProfanityHandler,
231
+ playerName: exports.playerNameProfanityHandler
232
+ };
233
+ function resolveLanguages(languages) {
234
+ return languages ? [...languages] : undefined;
235
+ }
236
+ function resolveProfanityLists(options) {
237
+ if (options.lists !== undefined) {
238
+ return options.lists;
239
+ }
240
+ if (options.listIds !== undefined) {
241
+ return options.listIds.map((listId) => bigfootds_shared_data_1.PROFANITY_LISTS_BY_ID[listId]);
242
+ }
243
+ return bigfootds_shared_data_1.PROFANITY_LISTS;
244
+ }
245
+ function resolveDefaultMatchMode(list) {
246
+ return list.matchingDefault === "substring_for_names" ? "substring" : "substring";
247
+ }
248
+ function findWholeWordIndex(value, word) {
249
+ const pattern = new RegExp(`(^|[^\\p{L}\\p{N}_])(${escapeRegExp(word)})(?=$|[^\\p{L}\\p{N}_])`, "u");
250
+ const match = pattern.exec(value);
251
+ if (match === null) {
252
+ return -1;
253
+ }
254
+ return match.index + (match[1]?.length ?? 0);
255
+ }
256
+ function escapeRegExp(value) {
257
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
258
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigfootds/bigfootds-service-utils",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Reusable service-side utilities for BigfootDS microservices.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,6 +33,16 @@
33
33
  "require": "./dist/express.js",
34
34
  "default": "./dist/express.js"
35
35
  },
36
+ "./logging": {
37
+ "types": "./dist/logging.d.ts",
38
+ "require": "./dist/logging.js",
39
+ "default": "./dist/logging.js"
40
+ },
41
+ "./profanity": {
42
+ "types": "./dist/profanity.d.ts",
43
+ "require": "./dist/profanity.js",
44
+ "default": "./dist/profanity.js"
45
+ },
36
46
  "./package.json": "./package.json"
37
47
  },
38
48
  "scripts": {
@@ -53,9 +63,12 @@
53
63
  },
54
64
  "homepage": "https://github.com/BigfootDS/pkg-service-utils#readme",
55
65
  "dependencies": {
56
- "@bigfootds/bigfootds-shared-data": "^2.1.0"
66
+ "@2toad/profanity": "^3.3.0",
67
+ "@bigfootds/bigfootds-shared-data": "^3.0.0",
68
+ "morgan": "^1.11.0"
57
69
  },
58
70
  "devDependencies": {
71
+ "@types/morgan": "^1.9.10",
59
72
  "@types/node": "^25.9.2",
60
73
  "typescript": "^6.0.3"
61
74
  }