core-mb 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.
Files changed (57) hide show
  1. package/.env +2 -0
  2. package/README.md +9 -0
  3. package/__unitest__/phone.number.crypto.spec.ts +69 -0
  4. package/__unitest__/phone.number.helper.spec.ts +30 -0
  5. package/coverage/base.css +224 -0
  6. package/coverage/block-navigation.js +87 -0
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +131 -0
  9. package/coverage/lcov-report/base.css +224 -0
  10. package/coverage/lcov-report/block-navigation.js +87 -0
  11. package/coverage/lcov-report/favicon.png +0 -0
  12. package/coverage/lcov-report/index.html +131 -0
  13. package/coverage/lcov-report/phone.number.helper.ts.html +124 -0
  14. package/coverage/lcov-report/prettify.css +1 -0
  15. package/coverage/lcov-report/prettify.js +2 -0
  16. package/coverage/lcov-report/security.helper.ts.html +349 -0
  17. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  18. package/coverage/lcov-report/sorter.js +210 -0
  19. package/coverage/lcov.info +67 -0
  20. package/coverage/phone.number.helper.ts.html +124 -0
  21. package/coverage/prettify.css +1 -0
  22. package/coverage/prettify.js +2 -0
  23. package/coverage/security.helper.ts.html +349 -0
  24. package/coverage/sort-arrow-sprite.png +0 -0
  25. package/coverage/sorter.js +210 -0
  26. package/dist/aes.helper.d.ts +6 -0
  27. package/dist/aes.helper.js +105 -0
  28. package/dist/date.helper.d.ts +0 -0
  29. package/dist/date.helper.js +1 -0
  30. package/dist/document.helper.d.ts +0 -0
  31. package/dist/document.helper.js +1 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +17 -0
  34. package/dist/logging.helper.d.ts +0 -0
  35. package/dist/logging.helper.js +1 -0
  36. package/dist/money.helper.d.ts +0 -0
  37. package/dist/money.helper.js +1 -0
  38. package/dist/phone.number.helper.d.ts +7 -0
  39. package/dist/phone.number.helper.js +17 -0
  40. package/dist/security.helper.d.ts +16 -0
  41. package/dist/security.helper.js +94 -0
  42. package/dist/validation.helper.d.ts +0 -0
  43. package/dist/validation.helper.js +1 -0
  44. package/jest.config.js +17 -0
  45. package/jest.setup.ts +2 -0
  46. package/package.json +32 -0
  47. package/src/aes.helper.ts +101 -0
  48. package/src/date.helper.ts +0 -0
  49. package/src/document.helper.ts +0 -0
  50. package/src/index.ts +1 -0
  51. package/src/logging.helper.ts +0 -0
  52. package/src/money.helper.ts +0 -0
  53. package/src/phone.number.helper.ts +13 -0
  54. package/src/security.helper.ts +88 -0
  55. package/src/validation.helper.ts +0 -0
  56. package/test-report.html +277 -0
  57. package/tsconfig.json +15 -0
@@ -0,0 +1,210 @@
1
+ /* eslint-disable */
2
+ var addSorting = (function() {
3
+ 'use strict';
4
+ var cols,
5
+ currentSort = {
6
+ index: 0,
7
+ desc: false
8
+ };
9
+
10
+ // returns the summary table element
11
+ function getTable() {
12
+ return document.querySelector('.coverage-summary');
13
+ }
14
+ // returns the thead element of the summary table
15
+ function getTableHeader() {
16
+ return getTable().querySelector('thead tr');
17
+ }
18
+ // returns the tbody element of the summary table
19
+ function getTableBody() {
20
+ return getTable().querySelector('tbody');
21
+ }
22
+ // returns the th element for nth column
23
+ function getNthColumn(n) {
24
+ return getTableHeader().querySelectorAll('th')[n];
25
+ }
26
+
27
+ function onFilterInput() {
28
+ const searchValue = document.getElementById('fileSearch').value;
29
+ const rows = document.getElementsByTagName('tbody')[0].children;
30
+
31
+ // Try to create a RegExp from the searchValue. If it fails (invalid regex),
32
+ // it will be treated as a plain text search
33
+ let searchRegex;
34
+ try {
35
+ searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
36
+ } catch (error) {
37
+ searchRegex = null;
38
+ }
39
+
40
+ for (let i = 0; i < rows.length; i++) {
41
+ const row = rows[i];
42
+ let isMatch = false;
43
+
44
+ if (searchRegex) {
45
+ // If a valid regex was created, use it for matching
46
+ isMatch = searchRegex.test(row.textContent);
47
+ } else {
48
+ // Otherwise, fall back to the original plain text search
49
+ isMatch = row.textContent
50
+ .toLowerCase()
51
+ .includes(searchValue.toLowerCase());
52
+ }
53
+
54
+ row.style.display = isMatch ? '' : 'none';
55
+ }
56
+ }
57
+
58
+ // loads the search box
59
+ function addSearchBox() {
60
+ var template = document.getElementById('filterTemplate');
61
+ var templateClone = template.content.cloneNode(true);
62
+ templateClone.getElementById('fileSearch').oninput = onFilterInput;
63
+ template.parentElement.appendChild(templateClone);
64
+ }
65
+
66
+ // loads all columns
67
+ function loadColumns() {
68
+ var colNodes = getTableHeader().querySelectorAll('th'),
69
+ colNode,
70
+ cols = [],
71
+ col,
72
+ i;
73
+
74
+ for (i = 0; i < colNodes.length; i += 1) {
75
+ colNode = colNodes[i];
76
+ col = {
77
+ key: colNode.getAttribute('data-col'),
78
+ sortable: !colNode.getAttribute('data-nosort'),
79
+ type: colNode.getAttribute('data-type') || 'string'
80
+ };
81
+ cols.push(col);
82
+ if (col.sortable) {
83
+ col.defaultDescSort = col.type === 'number';
84
+ colNode.innerHTML =
85
+ colNode.innerHTML + '<span class="sorter"></span>';
86
+ }
87
+ }
88
+ return cols;
89
+ }
90
+ // attaches a data attribute to every tr element with an object
91
+ // of data values keyed by column name
92
+ function loadRowData(tableRow) {
93
+ var tableCols = tableRow.querySelectorAll('td'),
94
+ colNode,
95
+ col,
96
+ data = {},
97
+ i,
98
+ val;
99
+ for (i = 0; i < tableCols.length; i += 1) {
100
+ colNode = tableCols[i];
101
+ col = cols[i];
102
+ val = colNode.getAttribute('data-value');
103
+ if (col.type === 'number') {
104
+ val = Number(val);
105
+ }
106
+ data[col.key] = val;
107
+ }
108
+ return data;
109
+ }
110
+ // loads all row data
111
+ function loadData() {
112
+ var rows = getTableBody().querySelectorAll('tr'),
113
+ i;
114
+
115
+ for (i = 0; i < rows.length; i += 1) {
116
+ rows[i].data = loadRowData(rows[i]);
117
+ }
118
+ }
119
+ // sorts the table using the data for the ith column
120
+ function sortByIndex(index, desc) {
121
+ var key = cols[index].key,
122
+ sorter = function(a, b) {
123
+ a = a.data[key];
124
+ b = b.data[key];
125
+ return a < b ? -1 : a > b ? 1 : 0;
126
+ },
127
+ finalSorter = sorter,
128
+ tableBody = document.querySelector('.coverage-summary tbody'),
129
+ rowNodes = tableBody.querySelectorAll('tr'),
130
+ rows = [],
131
+ i;
132
+
133
+ if (desc) {
134
+ finalSorter = function(a, b) {
135
+ return -1 * sorter(a, b);
136
+ };
137
+ }
138
+
139
+ for (i = 0; i < rowNodes.length; i += 1) {
140
+ rows.push(rowNodes[i]);
141
+ tableBody.removeChild(rowNodes[i]);
142
+ }
143
+
144
+ rows.sort(finalSorter);
145
+
146
+ for (i = 0; i < rows.length; i += 1) {
147
+ tableBody.appendChild(rows[i]);
148
+ }
149
+ }
150
+ // removes sort indicators for current column being sorted
151
+ function removeSortIndicators() {
152
+ var col = getNthColumn(currentSort.index),
153
+ cls = col.className;
154
+
155
+ cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
156
+ col.className = cls;
157
+ }
158
+ // adds sort indicators for current column being sorted
159
+ function addSortIndicators() {
160
+ getNthColumn(currentSort.index).className += currentSort.desc
161
+ ? ' sorted-desc'
162
+ : ' sorted';
163
+ }
164
+ // adds event listeners for all sorter widgets
165
+ function enableUI() {
166
+ var i,
167
+ el,
168
+ ithSorter = function ithSorter(i) {
169
+ var col = cols[i];
170
+
171
+ return function() {
172
+ var desc = col.defaultDescSort;
173
+
174
+ if (currentSort.index === i) {
175
+ desc = !currentSort.desc;
176
+ }
177
+ sortByIndex(i, desc);
178
+ removeSortIndicators();
179
+ currentSort.index = i;
180
+ currentSort.desc = desc;
181
+ addSortIndicators();
182
+ };
183
+ };
184
+ for (i = 0; i < cols.length; i += 1) {
185
+ if (cols[i].sortable) {
186
+ // add the click event handler on the th so users
187
+ // dont have to click on those tiny arrows
188
+ el = getNthColumn(i).querySelector('.sorter').parentElement;
189
+ if (el.addEventListener) {
190
+ el.addEventListener('click', ithSorter(i));
191
+ } else {
192
+ el.attachEvent('onclick', ithSorter(i));
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // adds sorting functionality to the UI
198
+ return function() {
199
+ if (!getTable()) {
200
+ return;
201
+ }
202
+ cols = loadColumns();
203
+ loadData();
204
+ addSearchBox();
205
+ addSortIndicators();
206
+ enableUI();
207
+ };
208
+ })();
209
+
210
+ window.addEventListener('load', addSorting);
@@ -0,0 +1,6 @@
1
+ export type PhoneCipherRecord = {
2
+ ciphertext: string;
3
+ iv: string;
4
+ authTag: string;
5
+ hmacIndex: string;
6
+ };
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const crypto = __importStar(require("crypto"));
37
+ const aesKeyB64 = process.env.PHONE_AES_KEY;
38
+ const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
39
+ if (!aesKeyB64 || !hmacKeyB64) {
40
+ throw new Error("PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded).");
41
+ }
42
+ const aesKey = Buffer.from(aesKeyB64, "base64");
43
+ const hmacKey = Buffer.from(hmacKeyB64, "base64");
44
+ /** Normalize to a consistent representation (approx. E.164-like):
45
+ * - Keep leading '+' if present
46
+ * - Remove spaces, dashes, parentheses
47
+ * - Remove any non-digit characters except leading '+'
48
+ * - You can adapt to your locale/validation as needed
49
+ */
50
+ function normalize(phoneRaw) {
51
+ if (!phoneRaw)
52
+ return "";
53
+ const trimmed = phoneRaw.trim();
54
+ const hasPlus = trimmed.startsWith("+");
55
+ const digitsOnly = trimmed.replace(/[^\d]/g, "");
56
+ return hasPlus ? `+${digitsOnly}` : digitsOnly; // store either "+855123..." or "0123..."
57
+ }
58
+ /** Create an HMAC index for lookups without revealing the number.
59
+ * Store this alongside the encrypted data and index it in the DB.
60
+ */
61
+ function hmacIndex(phoneRaw) {
62
+ const normalized = normalize(phoneRaw);
63
+ const h = crypto.createHmac("sha256", hmacKey);
64
+ h.update(normalized, "utf8");
65
+ return h.digest("base64");
66
+ }
67
+ /** Encrypt a phone number using AES-256-GCM.
68
+ * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
69
+ */
70
+ function encrypt(phoneRaw) {
71
+ const normalized = normalize(phoneRaw);
72
+ // 12-byte IV is recommended for GCM
73
+ const iv = crypto.randomBytes(12);
74
+ const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
75
+ // Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
76
+ // cipher.setAAD(Buffer.from(tenantId, 'utf8'));
77
+ const ciphertextBuf = Buffer.concat([
78
+ cipher.update(normalized, "utf8"),
79
+ cipher.final(),
80
+ ]);
81
+ const authTag = cipher.getAuthTag();
82
+ return {
83
+ ciphertext: ciphertextBuf.toString("base64"),
84
+ iv: iv.toString("base64"),
85
+ authTag: authTag.toString("base64"),
86
+ hmacIndex: hmacIndex(normalized),
87
+ };
88
+ }
89
+ /** Decrypt a previously stored record. Throws if tampered.
90
+ * Returns the normalized phone string.
91
+ */
92
+ function decrypt(record) {
93
+ const iv = Buffer.from(record.iv, "base64");
94
+ const authTag = Buffer.from(record.authTag, "base64");
95
+ const ciphertext = Buffer.from(record.ciphertext, "base64");
96
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
97
+ // If you used setAAD on encrypt, you MUST set the same AAD here
98
+ // decipher.setAAD(Buffer.from(tenantId, 'utf8'));
99
+ decipher.setAuthTag(authTag);
100
+ const plaintext = Buffer.concat([
101
+ decipher.update(ciphertext),
102
+ decipher.final(),
103
+ ]).toString("utf8");
104
+ return plaintext; // normalized phone
105
+ }
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1 @@
1
+ export * from "./security.helper";
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./security.helper"), exports);
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,7 @@
1
+ /** Normalize to a consistent representation (approx. E.164-like):
2
+ * - Keep leading '+' if present
3
+ * - Remove spaces, dashes, parentheses
4
+ * - Remove any non-digit characters except leading '+'
5
+ * - You can adapt to your locale/validation as needed
6
+ */
7
+ export declare function normalize(phoneRaw: string): string;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalize = normalize;
4
+ /** Normalize to a consistent representation (approx. E.164-like):
5
+ * - Keep leading '+' if present
6
+ * - Remove spaces, dashes, parentheses
7
+ * - Remove any non-digit characters except leading '+'
8
+ * - You can adapt to your locale/validation as needed
9
+ */
10
+ function normalize(phoneRaw) {
11
+ if (!phoneRaw)
12
+ return "";
13
+ const trimmed = phoneRaw.trim();
14
+ const hasPlus = trimmed.startsWith("+");
15
+ const digitsOnly = trimmed.replace(/[^\d]/g, "");
16
+ return hasPlus ? `+${digitsOnly}` : digitsOnly; // store either "+855123..." or "0123..."
17
+ }
@@ -0,0 +1,16 @@
1
+ export type PhoneCipherRecord = {
2
+ ciphertext: string;
3
+ iv: string;
4
+ authTag: string;
5
+ hmacIndex: string;
6
+ };
7
+ /** Encrypt a phone number using AES-256-GCM.
8
+ * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
9
+ */
10
+ export declare function encrypt(phoneRaw: string): Omit<PhoneCipherRecord, "hmacIndex"> & {
11
+ hmacIndex: string;
12
+ };
13
+ /** Decrypt a previously stored record. Throws if tampered.
14
+ * Returns the normalized phone string.
15
+ */
16
+ export declare function decrypt(record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">): string;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.encrypt = encrypt;
37
+ exports.decrypt = decrypt;
38
+ const crypto = __importStar(require("crypto"));
39
+ const phone_number_helper_1 = require("./phone.number.helper");
40
+ const aesKeyB64 = process.env.PHONE_AES_KEY;
41
+ const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
42
+ if (!aesKeyB64 || !hmacKeyB64) {
43
+ throw new Error("PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded).");
44
+ }
45
+ const aesKey = Buffer.from(aesKeyB64, "base64");
46
+ const hmacKey = Buffer.from(hmacKeyB64, "base64");
47
+ /** Create an HMAC index for lookups without revealing the number.
48
+ * Store this alongside the encrypted data and index it in the DB.
49
+ */
50
+ function hmacIndex(phoneRaw) {
51
+ const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
52
+ const h = crypto.createHmac("sha256", hmacKey);
53
+ h.update(normalized, "utf8");
54
+ return h.digest("base64");
55
+ }
56
+ /** Encrypt a phone number using AES-256-GCM.
57
+ * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
58
+ */
59
+ function encrypt(phoneRaw) {
60
+ const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
61
+ // 12-byte IV is recommended for GCM
62
+ const iv = crypto.randomBytes(12);
63
+ const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
64
+ // Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
65
+ // cipher.setAAD(Buffer.from(tenantId, 'utf8'));
66
+ const ciphertextBuf = Buffer.concat([
67
+ cipher.update(normalized, "utf8"),
68
+ cipher.final(),
69
+ ]);
70
+ const authTag = cipher.getAuthTag();
71
+ return {
72
+ ciphertext: ciphertextBuf.toString("base64"),
73
+ iv: iv.toString("base64"),
74
+ authTag: authTag.toString("base64"),
75
+ hmacIndex: hmacIndex(normalized),
76
+ };
77
+ }
78
+ /** Decrypt a previously stored record. Throws if tampered.
79
+ * Returns the normalized phone string.
80
+ */
81
+ function decrypt(record) {
82
+ const iv = Buffer.from(record.iv, "base64");
83
+ const authTag = Buffer.from(record.authTag, "base64");
84
+ const ciphertext = Buffer.from(record.ciphertext, "base64");
85
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
86
+ // If you used setAAD on encrypt, you MUST set the same AAD here
87
+ // decipher.setAAD(Buffer.from(tenantId, 'utf8'));
88
+ decipher.setAuthTag(authTag);
89
+ const plaintext = Buffer.concat([
90
+ decipher.update(ciphertext),
91
+ decipher.final(),
92
+ ]).toString("utf8");
93
+ return plaintext; // normalized phone
94
+ }
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
package/jest.config.js ADDED
@@ -0,0 +1,17 @@
1
+ module.exports = {
2
+ preset: "ts-jest",
3
+ testEnvironment: "node",
4
+ setupFiles: ["<rootDir>/jest.setup.ts"],
5
+ collectCoverage: true, // enable coverage
6
+ coverageDirectory: "coverage", // output folder
7
+ coverageReporters: ["text", "lcov", "html"], // text in terminal + HTML report
8
+ testMatch: ["**/*.spec.ts"], // only run *.spec.ts files
9
+ reporters: [
10
+ "default",
11
+ ["jest-html-reporter", {
12
+ pageTitle: "Test Report",
13
+ outputPath: "test-report.html",
14
+ includeFailureMsg: true
15
+ }]
16
+ ]
17
+ };
package/jest.setup.ts ADDED
@@ -0,0 +1,2 @@
1
+ import * as dotenv from "dotenv";
2
+ dotenv.config();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "core-mb",
3
+ "version": "1.0.0",
4
+ "description": "Core utility functions for the MB ecosystem in TypeScript",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest",
10
+ "test:watch": "jest --watch",
11
+ "test:coverage": "jest --coverage",
12
+ "coverage": "jest --coverage --ci"
13
+ },
14
+ "keywords": [
15
+ "core",
16
+ "mb",
17
+ "ecosystem",
18
+ "utility",
19
+ "typescript"
20
+ ],
21
+ "author": "Marco Bytes",
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "@types/jest": "^30.0.0",
25
+ "@types/node": "^20.19.11",
26
+ "dotenv": "^17.2.1",
27
+ "jest": "^30.0.5",
28
+ "jest-html-reporter": "^4.3.0",
29
+ "ts-jest": "^29.4.1",
30
+ "typescript": "^5.9.2"
31
+ }
32
+ }
@@ -0,0 +1,101 @@
1
+ import * as crypto from "crypto";
2
+
3
+ export type PhoneCipherRecord = {
4
+ // Base64 strings suitable for storage in text columns
5
+ ciphertext: string; // Encrypted phone number
6
+ iv: string; // Initialization Vector (nonce) for GCM (12 bytes recommended)
7
+ authTag: string; // Authentication tag returned by GCM (ensures integrity)
8
+ hmacIndex: string; // HMAC-SHA256(phoneNormalized) for search/dedup
9
+ };
10
+
11
+ const aesKeyB64 = process.env.PHONE_AES_KEY;
12
+ const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
13
+
14
+ if (!aesKeyB64 || !hmacKeyB64) {
15
+ throw new Error(
16
+ "PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded)."
17
+ );
18
+ }
19
+
20
+ const aesKey = Buffer.from(aesKeyB64, "base64");
21
+ const hmacKey = Buffer.from(hmacKeyB64, "base64");
22
+
23
+ /** Normalize to a consistent representation (approx. E.164-like):
24
+ * - Keep leading '+' if present
25
+ * - Remove spaces, dashes, parentheses
26
+ * - Remove any non-digit characters except leading '+'
27
+ * - You can adapt to your locale/validation as needed
28
+ */
29
+ function normalize(phoneRaw: string): string {
30
+ if (!phoneRaw) return "";
31
+ const trimmed = phoneRaw.trim();
32
+ const hasPlus = trimmed.startsWith("+");
33
+ const digitsOnly = trimmed.replace(/[^\d]/g, "");
34
+ return hasPlus ? `+${digitsOnly}` : digitsOnly; // store either "+855123..." or "0123..."
35
+ }
36
+
37
+ /** Create an HMAC index for lookups without revealing the number.
38
+ * Store this alongside the encrypted data and index it in the DB.
39
+ */
40
+ function hmacIndex(phoneRaw: string): string {
41
+ const normalized = normalize(phoneRaw);
42
+ const h = crypto.createHmac("sha256", hmacKey);
43
+ h.update(normalized, "utf8");
44
+ return h.digest("base64");
45
+ }
46
+
47
+ /** Encrypt a phone number using AES-256-GCM.
48
+ * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
49
+ */
50
+ function encrypt(
51
+ phoneRaw: string
52
+ ): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
53
+ const normalized = normalize(phoneRaw);
54
+
55
+ // 12-byte IV is recommended for GCM
56
+ const iv = crypto.randomBytes(12);
57
+
58
+ const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
59
+
60
+ // Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
61
+ // cipher.setAAD(Buffer.from(tenantId, 'utf8'));
62
+
63
+ const ciphertextBuf = Buffer.concat([
64
+ cipher.update(normalized, "utf8"),
65
+ cipher.final(),
66
+ ]);
67
+
68
+ const authTag = cipher.getAuthTag();
69
+
70
+ return {
71
+ ciphertext: ciphertextBuf.toString("base64"),
72
+ iv: iv.toString("base64"),
73
+ authTag: authTag.toString("base64"),
74
+ hmacIndex: hmacIndex(normalized),
75
+ };
76
+ }
77
+
78
+ /** Decrypt a previously stored record. Throws if tampered.
79
+ * Returns the normalized phone string.
80
+ */
81
+ function decrypt(
82
+ record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
83
+ ): string {
84
+ const iv = Buffer.from(record.iv, "base64");
85
+ const authTag = Buffer.from(record.authTag, "base64");
86
+ const ciphertext = Buffer.from(record.ciphertext, "base64");
87
+
88
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
89
+
90
+ // If you used setAAD on encrypt, you MUST set the same AAD here
91
+ // decipher.setAAD(Buffer.from(tenantId, 'utf8'));
92
+
93
+ decipher.setAuthTag(authTag);
94
+
95
+ const plaintext = Buffer.concat([
96
+ decipher.update(ciphertext),
97
+ decipher.final(),
98
+ ]).toString("utf8");
99
+
100
+ return plaintext; // normalized phone
101
+ }
File without changes
File without changes
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./security.helper"
File without changes