core-mb 1.0.0 → 1.0.2

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 (79) hide show
  1. package/.github/workflows/node.js.yml +58 -0
  2. package/.nyc_output/0393f58b-d3a0-4d83-8010-334fc96e8c86.json +1 -0
  3. package/.nyc_output/0edf14d3-019a-4204-aae9-8d7d2491500b.json +1 -0
  4. package/.nyc_output/1db7cb73-4e51-46c0-96e8-3d1edd65f98c.json +1 -0
  5. package/.nyc_output/29ead10f-a723-4574-aeaf-5eff2ebcafac.json +1 -0
  6. package/.nyc_output/30473bab-10b9-4d55-8f30-42cbd631a50e.json +1 -0
  7. package/.nyc_output/4514ee29-1cb6-4243-abab-937ae44e87fe.json +1 -0
  8. package/.nyc_output/5268e9ea-7320-41e0-8dbf-37139a2b07e9.json +1 -0
  9. package/.nyc_output/581a75aa-21fc-470f-a998-ad9752b568ff.json +1 -0
  10. package/.nyc_output/6492905d-a035-44ac-b40d-84d04d64d3c2.json +1 -0
  11. package/.nyc_output/67ba5e3c-ca1d-4dbf-9155-6a154788cb55.json +1 -0
  12. package/.nyc_output/688e0703-cbfb-4f94-ac42-66472ac00a0c.json +1 -0
  13. package/.nyc_output/6c1e2427-a36b-407d-96d2-929a885e565d.json +1 -0
  14. package/.nyc_output/781a9e98-6dff-42dc-9bb2-126b65619cf3.json +1 -0
  15. package/.nyc_output/960ffee6-798c-4670-b1fa-39f6cebaa70b.json +1 -0
  16. package/.nyc_output/9661b441-490d-4793-881a-570493cf4d77.json +1 -0
  17. package/.nyc_output/9fe3622e-5b2a-42b2-a7ec-6f42c052afc9.json +1 -0
  18. package/.nyc_output/c37e92ff-6103-4b4b-be8e-2e23adc44ba1.json +1 -0
  19. package/.nyc_output/cfc973a7-b63b-4ed5-a568-019c4813486f.json +1 -0
  20. package/.nyc_output/d4490352-b4a2-45e0-beb3-a146203ce7a1.json +1 -0
  21. package/.nyc_output/da65eb21-2244-42c2-a24a-136a49caa10b.json +1 -0
  22. package/.nyc_output/e58bd293-d9b2-4a7a-bd45-b6f603743e81.json +1 -0
  23. package/.nyc_output/e5ef7a91-bcbd-46a0-848e-423e6aec9090.json +1 -0
  24. package/.nyc_output/f73cc576-f3eb-476a-bb88-9030aaaca351.json +1 -0
  25. package/.nyc_output/processinfo/0393f58b-d3a0-4d83-8010-334fc96e8c86.json +1 -0
  26. package/.nyc_output/processinfo/0edf14d3-019a-4204-aae9-8d7d2491500b.json +1 -0
  27. package/.nyc_output/processinfo/1db7cb73-4e51-46c0-96e8-3d1edd65f98c.json +1 -0
  28. package/.nyc_output/processinfo/29ead10f-a723-4574-aeaf-5eff2ebcafac.json +1 -0
  29. package/.nyc_output/processinfo/30473bab-10b9-4d55-8f30-42cbd631a50e.json +1 -0
  30. package/.nyc_output/processinfo/4514ee29-1cb6-4243-abab-937ae44e87fe.json +1 -0
  31. package/.nyc_output/processinfo/5268e9ea-7320-41e0-8dbf-37139a2b07e9.json +1 -0
  32. package/.nyc_output/processinfo/581a75aa-21fc-470f-a998-ad9752b568ff.json +1 -0
  33. package/.nyc_output/processinfo/6492905d-a035-44ac-b40d-84d04d64d3c2.json +1 -0
  34. package/.nyc_output/processinfo/67ba5e3c-ca1d-4dbf-9155-6a154788cb55.json +1 -0
  35. package/.nyc_output/processinfo/688e0703-cbfb-4f94-ac42-66472ac00a0c.json +1 -0
  36. package/.nyc_output/processinfo/6c1e2427-a36b-407d-96d2-929a885e565d.json +1 -0
  37. package/.nyc_output/processinfo/781a9e98-6dff-42dc-9bb2-126b65619cf3.json +1 -0
  38. package/.nyc_output/processinfo/960ffee6-798c-4670-b1fa-39f6cebaa70b.json +1 -0
  39. package/.nyc_output/processinfo/9661b441-490d-4793-881a-570493cf4d77.json +1 -0
  40. package/.nyc_output/processinfo/9fe3622e-5b2a-42b2-a7ec-6f42c052afc9.json +1 -0
  41. package/.nyc_output/processinfo/c37e92ff-6103-4b4b-be8e-2e23adc44ba1.json +1 -0
  42. package/.nyc_output/processinfo/cfc973a7-b63b-4ed5-a568-019c4813486f.json +1 -0
  43. package/.nyc_output/processinfo/d4490352-b4a2-45e0-beb3-a146203ce7a1.json +1 -0
  44. package/.nyc_output/processinfo/da65eb21-2244-42c2-a24a-136a49caa10b.json +1 -0
  45. package/.nyc_output/processinfo/e58bd293-d9b2-4a7a-bd45-b6f603743e81.json +1 -0
  46. package/.nyc_output/processinfo/e5ef7a91-bcbd-46a0-848e-423e6aec9090.json +1 -0
  47. package/.nyc_output/processinfo/f73cc576-f3eb-476a-bb88-9030aaaca351.json +1 -0
  48. package/.nyc_output/processinfo/index.json +1 -0
  49. package/README.md +12 -4
  50. package/__unitest__/phone.number.crypto.spec.ts +9 -9
  51. package/coverage/config.ts.html +178 -0
  52. package/coverage/index.html +31 -16
  53. package/coverage/lcov-report/config.ts.html +178 -0
  54. package/coverage/lcov-report/index.html +31 -16
  55. package/coverage/lcov-report/phone.number.helper.ts.html +1 -1
  56. package/coverage/lcov-report/security.helper.ts.html +19 -46
  57. package/coverage/lcov.info +58 -36
  58. package/coverage/phone.number.helper.ts.html +1 -1
  59. package/coverage/security.helper.ts.html +19 -46
  60. package/dist/config.d.ts +12 -0
  61. package/dist/config.js +24 -0
  62. package/dist/date.helper.d.ts +1 -0
  63. package/dist/date.helper.js +4 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +1 -0
  66. package/dist/security.helper.d.ts +6 -2
  67. package/dist/security.helper.js +10 -14
  68. package/dist/validation.helper.d.ts +6 -0
  69. package/dist/validation.helper.js +32 -0
  70. package/jest.config.js +2 -1
  71. package/jest.setup.ts +8 -0
  72. package/package.json +6 -2
  73. package/src/config.ts +31 -0
  74. package/src/date.helper.ts +3 -0
  75. package/src/index.ts +1 -0
  76. package/src/security.helper.ts +9 -18
  77. package/src/validation.helper.ts +36 -0
  78. package/test-report.html +1 -1
  79. package/src/aes.helper.ts +0 -101
@@ -23,16 +23,16 @@
23
23
  <div class='clearfix'>
24
24
 
25
25
  <div class='fl pad1y space-right2'>
26
- <span class="strong">96.29% </span>
26
+ <span class="strong">100% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>26/27</span>
28
+ <span class='fraction'>24/24</span>
29
29
  </div>
30
30
 
31
31
 
32
32
  <div class='fl pad1y space-right2'>
33
- <span class="strong">75% </span>
33
+ <span class="strong">100% </span>
34
34
  <span class="quiet">Branches</span>
35
- <span class='fraction'>3/4</span>
35
+ <span class='fraction'>0/0</span>
36
36
  </div>
37
37
 
38
38
 
@@ -44,9 +44,9 @@
44
44
 
45
45
 
46
46
  <div class='fl pad1y space-right2'>
47
- <span class="strong">96.29% </span>
47
+ <span class="strong">100% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>26/27</span>
49
+ <span class='fraction'>24/24</span>
50
50
  </div>
51
51
 
52
52
 
@@ -142,18 +142,11 @@
142
142
  <a name='L77'></a><a href='#L77'>77</a>
143
143
  <a name='L78'></a><a href='#L78'>78</a>
144
144
  <a name='L79'></a><a href='#L79'>79</a>
145
- <a name='L80'></a><a href='#L80'>80</a>
146
- <a name='L81'></a><a href='#L81'>81</a>
147
- <a name='L82'></a><a href='#L82'>82</a>
148
- <a name='L83'></a><a href='#L83'>83</a>
149
- <a name='L84'></a><a href='#L84'>84</a>
150
- <a name='L85'></a><a href='#L85'>85</a>
151
- <a name='L86'></a><a href='#L86'>86</a>
152
- <a name='L87'></a><a href='#L87'>87</a>
153
- <a name='L88'></a><a href='#L88'>88</a>
154
- <a name='L89'></a><a href='#L89'>89</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
145
+ <a name='L80'></a><a href='#L80'>80</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
146
+ <span class="cline-any cline-yes">1x</span>
155
147
  <span class="cline-any cline-yes">1x</span>
156
148
  <span class="cline-any cline-neutral">&nbsp;</span>
149
+ <span class="cline-any cline-yes">1x</span>
157
150
  <span class="cline-any cline-neutral">&nbsp;</span>
158
151
  <span class="cline-any cline-neutral">&nbsp;</span>
159
152
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -162,22 +155,11 @@
162
155
  <span class="cline-any cline-neutral">&nbsp;</span>
163
156
  <span class="cline-any cline-neutral">&nbsp;</span>
164
157
  <span class="cline-any cline-neutral">&nbsp;</span>
165
- <span class="cline-any cline-yes">1x</span>
166
- <span class="cline-any cline-yes">1x</span>
167
- <span class="cline-any cline-neutral">&nbsp;</span>
168
- <span class="cline-any cline-yes">1x</span>
169
- <span class="cline-any cline-no">&nbsp;</span>
170
158
  <span class="cline-any cline-neutral">&nbsp;</span>
171
159
  <span class="cline-any cline-neutral">&nbsp;</span>
172
160
  <span class="cline-any cline-neutral">&nbsp;</span>
173
161
  <span class="cline-any cline-neutral">&nbsp;</span>
174
162
  <span class="cline-any cline-yes">1x</span>
175
- <span class="cline-any cline-yes">1x</span>
176
- <span class="cline-any cline-neutral">&nbsp;</span>
177
- <span class="cline-any cline-neutral">&nbsp;</span>
178
- <span class="cline-any cline-neutral">&nbsp;</span>
179
- <span class="cline-any cline-neutral">&nbsp;</span>
180
- <span class="cline-any cline-neutral">&nbsp;</span>
181
163
  <span class="cline-any cline-yes">8x</span>
182
164
  <span class="cline-any cline-yes">8x</span>
183
165
  <span class="cline-any cline-yes">8x</span>
@@ -240,8 +222,11 @@
240
222
  <span class="cline-any cline-yes">4x</span>
241
223
  <span class="cline-any cline-neutral">&nbsp;</span>
242
224
  <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import * as crypto from "crypto";
225
+ import { CoreMBConfig } from "./config";
243
226
  import { normalize } from "./phone.number.helper";
244
227
  &nbsp;
228
+ const config = CoreMBConfig.getInstance();
229
+ &nbsp;
245
230
  export type PhoneCipherRecord = {
246
231
  // Base64 strings suitable for storage in text columns
247
232
  ciphertext: string; // Encrypted phone number
@@ -250,24 +235,12 @@ export type PhoneCipherRecord = {
250
235
  hmacIndex: string; // HMAC-SHA256(phoneNormalized) for search/dedup
251
236
  };
252
237
  &nbsp;
253
- const aesKeyB64 = process.env.PHONE_AES_KEY;
254
- const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
255
- &nbsp;
256
- <span class="missing-if-branch" title="if path not taken" >I</span>if (!aesKeyB64 || !hmacKeyB64) {
257
- <span class="cstat-no" title="statement not covered" > throw new Error(</span>
258
- "PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded)."
259
- );
260
- }
261
- &nbsp;
262
- const aesKey = Buffer.from(aesKeyB64, "base64");
263
- const hmacKey = Buffer.from(hmacKeyB64, "base64");
264
- &nbsp;
265
238
  /** Create an HMAC index for lookups without revealing the number.
266
239
  * Store this alongside the encrypted data and index it in the DB.
267
240
  */
268
- function hmacIndex(phoneRaw: string): string {
241
+ export function hmacIndex(phoneRaw: string): string {
269
242
  const normalized = normalize(phoneRaw);
270
- const h = crypto.createHmac("sha256", hmacKey);
243
+ const h = crypto.createHmac("sha256", config.hmacKey);
271
244
  h.update(normalized, "utf8");
272
245
  return h.digest("base64");
273
246
  }
@@ -275,7 +248,7 @@ function hmacIndex(phoneRaw: string): string {
275
248
  /** Encrypt a phone number using AES-256-GCM.
276
249
  * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
277
250
  */
278
- export function encrypt(
251
+ export function phoneEncrypt(
279
252
  phoneRaw: string
280
253
  ): Omit&lt;PhoneCipherRecord, "hmacIndex"&gt; &amp; { hmacIndex: string } {
281
254
  const normalized = normalize(phoneRaw);
@@ -283,7 +256,7 @@ export function encrypt(
283
256
  // 12-byte IV is recommended for GCM
284
257
  const iv = crypto.randomBytes(12);
285
258
  &nbsp;
286
- const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
259
+ const cipher = crypto.createCipheriv("aes-256-gcm", config.aesKey, iv);
287
260
  &nbsp;
288
261
  // Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
289
262
  // cipher.setAAD(Buffer.from(tenantId, 'utf8'));
@@ -306,14 +279,14 @@ export function encrypt(
306
279
  /** Decrypt a previously stored record. Throws if tampered.
307
280
  * Returns the normalized phone string.
308
281
  */
309
- export function decrypt(
282
+ export function phoneDecrypt(
310
283
  record: Pick&lt;PhoneCipherRecord, "ciphertext" | "iv" | "authTag"&gt;
311
284
  ): string {
312
285
  const iv = Buffer.from(record.iv, "base64");
313
286
  const authTag = Buffer.from(record.authTag, "base64");
314
287
  const ciphertext = Buffer.from(record.ciphertext, "base64");
315
288
  &nbsp;
316
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
289
+ const decipher = crypto.createDecipheriv("aes-256-gcm", config.aesKey, iv);
317
290
  &nbsp;
318
291
  // If you used setAAD on encrypt, you MUST set the same AAD here
319
292
  // decipher.setAAD(Buffer.from(tenantId, 'utf8'));
@@ -334,7 +307,7 @@ export function decrypt(
334
307
  <div class='footer quiet pad2 space-top1 center small'>
335
308
  Code coverage generated by
336
309
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
337
- at 2025-08-23T06:30:01.192Z
310
+ at 2025-08-23T08:06:17.198Z
338
311
  </div>
339
312
  <script src="prettify.js"></script>
340
313
  <script>
@@ -0,0 +1,12 @@
1
+ export interface ConfigProperties {
2
+ aesKey: string;
3
+ hmacKey: string;
4
+ }
5
+ export declare class CoreMBConfig {
6
+ private static instance;
7
+ readonly aesKey: Buffer;
8
+ readonly hmacKey: Buffer;
9
+ private constructor();
10
+ static initialize(props: ConfigProperties): CoreMBConfig;
11
+ static getInstance(): CoreMBConfig;
12
+ }
package/dist/config.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CoreMBConfig = void 0;
4
+ class CoreMBConfig {
5
+ constructor(props) {
6
+ this.aesKey = Buffer.from(props.aesKey, "base64");
7
+ this.hmacKey = Buffer.from(props.hmacKey, "base64");
8
+ }
9
+ static initialize(props) {
10
+ if (CoreMBConfig.instance) {
11
+ throw new Error("CoreMBConfig has already been initialized");
12
+ }
13
+ CoreMBConfig.instance = new CoreMBConfig(props);
14
+ return CoreMBConfig.instance;
15
+ }
16
+ // Access the singleton instance
17
+ static getInstance() {
18
+ if (!CoreMBConfig.instance) {
19
+ throw new Error("CoreMBConfig has not been initialized yet");
20
+ }
21
+ return CoreMBConfig.instance;
22
+ }
23
+ }
24
+ exports.CoreMBConfig = CoreMBConfig;
@@ -0,0 +1 @@
1
+ export declare function sample(): void;
@@ -1 +1,5 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sample = sample;
4
+ function sample() {
5
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
+ export * from "./date.helper";
1
2
  export * from "./security.helper";
package/dist/index.js CHANGED
@@ -14,4 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./date.helper"), exports);
17
18
  __exportStar(require("./security.helper"), exports);
@@ -4,13 +4,17 @@ export type PhoneCipherRecord = {
4
4
  authTag: string;
5
5
  hmacIndex: string;
6
6
  };
7
+ /** Create an HMAC index for lookups without revealing the number.
8
+ * Store this alongside the encrypted data and index it in the DB.
9
+ */
10
+ export declare function hmacIndex(phoneRaw: string): string;
7
11
  /** Encrypt a phone number using AES-256-GCM.
8
12
  * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
9
13
  */
10
- export declare function encrypt(phoneRaw: string): Omit<PhoneCipherRecord, "hmacIndex"> & {
14
+ export declare function phoneEncrypt(phoneRaw: string): Omit<PhoneCipherRecord, "hmacIndex"> & {
11
15
  hmacIndex: string;
12
16
  };
13
17
  /** Decrypt a previously stored record. Throws if tampered.
14
18
  * Returns the normalized phone string.
15
19
  */
16
- export declare function decrypt(record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">): string;
20
+ export declare function phoneDecrypt(record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">): string;
@@ -33,34 +33,30 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.encrypt = encrypt;
37
- exports.decrypt = decrypt;
36
+ exports.hmacIndex = hmacIndex;
37
+ exports.phoneEncrypt = phoneEncrypt;
38
+ exports.phoneDecrypt = phoneDecrypt;
38
39
  const crypto = __importStar(require("crypto"));
40
+ const config_1 = require("./config");
39
41
  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");
42
+ const config = config_1.CoreMBConfig.getInstance();
47
43
  /** Create an HMAC index for lookups without revealing the number.
48
44
  * Store this alongside the encrypted data and index it in the DB.
49
45
  */
50
46
  function hmacIndex(phoneRaw) {
51
47
  const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
52
- const h = crypto.createHmac("sha256", hmacKey);
48
+ const h = crypto.createHmac("sha256", config.hmacKey);
53
49
  h.update(normalized, "utf8");
54
50
  return h.digest("base64");
55
51
  }
56
52
  /** Encrypt a phone number using AES-256-GCM.
57
53
  * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
58
54
  */
59
- function encrypt(phoneRaw) {
55
+ function phoneEncrypt(phoneRaw) {
60
56
  const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
61
57
  // 12-byte IV is recommended for GCM
62
58
  const iv = crypto.randomBytes(12);
63
- const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
59
+ const cipher = crypto.createCipheriv("aes-256-gcm", config.aesKey, iv);
64
60
  // Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
65
61
  // cipher.setAAD(Buffer.from(tenantId, 'utf8'));
66
62
  const ciphertextBuf = Buffer.concat([
@@ -78,11 +74,11 @@ function encrypt(phoneRaw) {
78
74
  /** Decrypt a previously stored record. Throws if tampered.
79
75
  * Returns the normalized phone string.
80
76
  */
81
- function decrypt(record) {
77
+ function phoneDecrypt(record) {
82
78
  const iv = Buffer.from(record.iv, "base64");
83
79
  const authTag = Buffer.from(record.authTag, "base64");
84
80
  const ciphertext = Buffer.from(record.ciphertext, "base64");
85
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
81
+ const decipher = crypto.createDecipheriv("aes-256-gcm", config.aesKey, iv);
86
82
  // If you used setAAD on encrypt, you MUST set the same AAD here
87
83
  // decipher.setAAD(Buffer.from(tenantId, 'utf8'));
88
84
  decipher.setAuthTag(authTag);
@@ -0,0 +1,6 @@
1
+ import { ValidationOptions } from "class-validator";
2
+ /**
3
+ * Checks that the string is likely an encrypted phone number
4
+ * (e.g., base64 or hex encoded string). Adjust pattern as needed.
5
+ */
6
+ export declare function IsEncryptedPhone(validationOptions?: ValidationOptions): (object: Object, propertyName: string) => void;
@@ -1 +1,33 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IsEncryptedPhone = IsEncryptedPhone;
4
+ const class_validator_1 = require("class-validator");
5
+ /**
6
+ * Checks that the string is likely an encrypted phone number
7
+ * (e.g., base64 or hex encoded string). Adjust pattern as needed.
8
+ */
9
+ function IsEncryptedPhone(validationOptions) {
10
+ return function (object, propertyName) {
11
+ (0, class_validator_1.registerDecorator)({
12
+ name: "isEncryptedPhone",
13
+ target: object.constructor,
14
+ propertyName: propertyName,
15
+ options: validationOptions,
16
+ validator: {
17
+ validate(value, args) {
18
+ if (typeof value !== "string")
19
+ return false;
20
+ // Example: allow hex or base64 strings
21
+ // Hex pattern (32+ chars, adjust length as needed)
22
+ const hexPattern = /^[a-fA-F0-9]+$/;
23
+ // Base64 pattern
24
+ const base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
25
+ return hexPattern.test(value) || base64Pattern.test(value);
26
+ },
27
+ defaultMessage(args) {
28
+ return `${args.property} must be an encrypted phone string (hex or base64)`;
29
+ },
30
+ },
31
+ });
32
+ };
33
+ }
package/jest.config.js CHANGED
@@ -4,8 +4,9 @@ module.exports = {
4
4
  setupFiles: ["<rootDir>/jest.setup.ts"],
5
5
  collectCoverage: true, // enable coverage
6
6
  coverageDirectory: "coverage", // output folder
7
- coverageReporters: ["text", "lcov", "html"], // text in terminal + HTML report
7
+ coverageReporters: ["text-summary", "lcov", "html"], // text in terminal + HTML report
8
8
  testMatch: ["**/*.spec.ts"], // only run *.spec.ts files
9
+ coveragePathIgnorePatterns: ["/node_modules/"],
9
10
  reporters: [
10
11
  "default",
11
12
  ["jest-html-reporter", {
package/jest.setup.ts CHANGED
@@ -1,2 +1,10 @@
1
1
  import * as dotenv from "dotenv";
2
+ import { CoreMBConfig } from "./src/config";
3
+
2
4
  dotenv.config();
5
+ CoreMBConfig.initialize({
6
+ aesKey: process.env.PHONE_AES_KEY || "N/A",
7
+ hmacKey: process.env.PHONE_HMAC_KEY || "N/A",
8
+ });
9
+
10
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-mb",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Core utility functions for the MB ecosystem in TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "test": "jest",
10
10
  "test:watch": "jest --watch",
11
11
  "test:coverage": "jest --coverage",
12
- "coverage": "jest --coverage --ci"
12
+ "coverage": "nyc jest"
13
13
  },
14
14
  "keywords": [
15
15
  "core",
@@ -26,7 +26,11 @@
26
26
  "dotenv": "^17.2.1",
27
27
  "jest": "^30.0.5",
28
28
  "jest-html-reporter": "^4.3.0",
29
+ "nyc": "^17.1.0",
29
30
  "ts-jest": "^29.4.1",
30
31
  "typescript": "^5.9.2"
32
+ },
33
+ "dependencies": {
34
+ "class-validator": "^0.14.2"
31
35
  }
32
36
  }
package/src/config.ts ADDED
@@ -0,0 +1,31 @@
1
+ export interface ConfigProperties {
2
+ aesKey: string;
3
+ hmacKey: string;
4
+ }
5
+
6
+ export class CoreMBConfig {
7
+ private static instance: CoreMBConfig;
8
+ public readonly aesKey: Buffer;
9
+ public readonly hmacKey: Buffer;
10
+
11
+ private constructor(props: ConfigProperties) {
12
+ this.aesKey = Buffer.from(props.aesKey, "base64");
13
+ this.hmacKey = Buffer.from(props.hmacKey, "base64");
14
+ }
15
+
16
+ public static initialize(props: ConfigProperties): CoreMBConfig {
17
+ if (CoreMBConfig.instance) {
18
+ throw new Error("CoreMBConfig has already been initialized");
19
+ }
20
+ CoreMBConfig.instance = new CoreMBConfig(props);
21
+ return CoreMBConfig.instance;
22
+ }
23
+
24
+ // Access the singleton instance
25
+ public static getInstance(): CoreMBConfig {
26
+ if (!CoreMBConfig.instance) {
27
+ throw new Error("CoreMBConfig has not been initialized yet");
28
+ }
29
+ return CoreMBConfig.instance;
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ export function sample() {
2
+
3
+ }
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
+ export * from "./date.helper"
1
2
  export * from "./security.helper"
@@ -1,6 +1,9 @@
1
1
  import * as crypto from "crypto";
2
+ import { CoreMBConfig } from "./config";
2
3
  import { normalize } from "./phone.number.helper";
3
4
 
5
+ const config = CoreMBConfig.getInstance();
6
+
4
7
  export type PhoneCipherRecord = {
5
8
  // Base64 strings suitable for storage in text columns
6
9
  ciphertext: string; // Encrypted phone number
@@ -9,24 +12,12 @@ export type PhoneCipherRecord = {
9
12
  hmacIndex: string; // HMAC-SHA256(phoneNormalized) for search/dedup
10
13
  };
11
14
 
12
- const aesKeyB64 = process.env.PHONE_AES_KEY;
13
- const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
14
-
15
- if (!aesKeyB64 || !hmacKeyB64) {
16
- throw new Error(
17
- "PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded)."
18
- );
19
- }
20
-
21
- const aesKey = Buffer.from(aesKeyB64, "base64");
22
- const hmacKey = Buffer.from(hmacKeyB64, "base64");
23
-
24
15
  /** Create an HMAC index for lookups without revealing the number.
25
16
  * Store this alongside the encrypted data and index it in the DB.
26
17
  */
27
- function hmacIndex(phoneRaw: string): string {
18
+ export function hmacIndex(phoneRaw: string): string {
28
19
  const normalized = normalize(phoneRaw);
29
- const h = crypto.createHmac("sha256", hmacKey);
20
+ const h = crypto.createHmac("sha256", config.hmacKey);
30
21
  h.update(normalized, "utf8");
31
22
  return h.digest("base64");
32
23
  }
@@ -34,7 +25,7 @@ function hmacIndex(phoneRaw: string): string {
34
25
  /** Encrypt a phone number using AES-256-GCM.
35
26
  * Returns base64-encoded ciphertext, iv and authTag suitable for storage.
36
27
  */
37
- export function encrypt(
28
+ export function phoneEncrypt(
38
29
  phoneRaw: string
39
30
  ): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
40
31
  const normalized = normalize(phoneRaw);
@@ -42,7 +33,7 @@ export function encrypt(
42
33
  // 12-byte IV is recommended for GCM
43
34
  const iv = crypto.randomBytes(12);
44
35
 
45
- const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
36
+ const cipher = crypto.createCipheriv("aes-256-gcm", config.aesKey, iv);
46
37
 
47
38
  // Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
48
39
  // cipher.setAAD(Buffer.from(tenantId, 'utf8'));
@@ -65,14 +56,14 @@ export function encrypt(
65
56
  /** Decrypt a previously stored record. Throws if tampered.
66
57
  * Returns the normalized phone string.
67
58
  */
68
- export function decrypt(
59
+ export function phoneDecrypt(
69
60
  record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
70
61
  ): string {
71
62
  const iv = Buffer.from(record.iv, "base64");
72
63
  const authTag = Buffer.from(record.authTag, "base64");
73
64
  const ciphertext = Buffer.from(record.ciphertext, "base64");
74
65
 
75
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
66
+ const decipher = crypto.createDecipheriv("aes-256-gcm", config.aesKey, iv);
76
67
 
77
68
  // If you used setAAD on encrypt, you MUST set the same AAD here
78
69
  // decipher.setAAD(Buffer.from(tenantId, 'utf8'));
@@ -0,0 +1,36 @@
1
+ import {
2
+ registerDecorator,
3
+ ValidationOptions,
4
+ ValidationArguments,
5
+ } from "class-validator";
6
+
7
+ /**
8
+ * Checks that the string is likely an encrypted phone number
9
+ * (e.g., base64 or hex encoded string). Adjust pattern as needed.
10
+ */
11
+ export function IsEncryptedPhone(validationOptions?: ValidationOptions) {
12
+ return function (object: Object, propertyName: string) {
13
+ registerDecorator({
14
+ name: "isEncryptedPhone",
15
+ target: object.constructor,
16
+ propertyName: propertyName,
17
+ options: validationOptions,
18
+ validator: {
19
+ validate(value: any, args: ValidationArguments) {
20
+ if (typeof value !== "string") return false;
21
+
22
+ // Example: allow hex or base64 strings
23
+ // Hex pattern (32+ chars, adjust length as needed)
24
+ const hexPattern = /^[a-fA-F0-9]+$/;
25
+ // Base64 pattern
26
+ const base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
27
+
28
+ return hexPattern.test(value) || base64Pattern.test(value);
29
+ },
30
+ defaultMessage(args: ValidationArguments) {
31
+ return `${args.property} must be an encrypted phone string (hex or base64)`;
32
+ },
33
+ },
34
+ });
35
+ };
36
+ }
package/test-report.html CHANGED
@@ -274,4 +274,4 @@ header {
274
274
  font-size: 1rem;
275
275
  padding: 0 0.5rem;
276
276
  }
277
- </style></head><body><main class="jesthtml-content"><header><h1 id="title">Test Report</h1></header><section id="metadata-container"><div id="timestamp">Started: 2025-08-23 13:29:58</div><div id="summary"><div id="suite-summary"><div class="summary-total">Suites (2)</div><div class="summary-passed ">2 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div><div id="test-summary"><div class="summary-total">Tests (9)</div><div class="summary-passed ">9 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div></div></section><details id="suite-1" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Volumes/Projects/RND/microservices/vieltalk-chat/core-mb/__unitest__/phone.number.helper.spec.ts</div><div class="suite-time">1.441s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should return empty string for null/undefined/empty input</div><div class="test-status">passed</div><div class="test-duration">0.005s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should keep leading + and strip non-digit characters</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should remove all non-digits if no leading +</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should handle already clean input</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should not duplicate + signs</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div></div></details><details id="suite-2" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Volumes/Projects/RND/microservices/vieltalk-chat/core-mb/__unitest__/phone.number.crypto.spec.ts</div><div class="suite-time">1.469s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should encrypt and decrypt correctly</div><div class="test-status">passed</div><div class="test-duration">0.009s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should produce different ciphertexts for same phone (unique IV)</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should throw when tampering with ciphertext</div><div class="test-status">passed</div><div class="test-duration">0.015s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should throw when tampering with authTag</div><div class="test-status">passed</div><div class="test-duration">0.002s</div></div></div></div></details></main></body></html>
277
+ </style></head><body><main class="jesthtml-content"><header><h1 id="title">Test Report</h1></header><section id="metadata-container"><div id="timestamp">Started: 2025-08-23 15:06:13</div><div id="summary"><div id="suite-summary"><div class="summary-total">Suites (2)</div><div class="summary-passed ">2 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div><div id="test-summary"><div class="summary-total">Tests (9)</div><div class="summary-passed ">9 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div></div></section><details id="suite-1" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Volumes/Projects/RND/microservices/vieltalk-chat/core-mb/__unitest__/phone.number.helper.spec.ts</div><div class="suite-time">2.333s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should return empty string for null/undefined/empty input</div><div class="test-status">passed</div><div class="test-duration">0.006s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should keep leading + and strip non-digit characters</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should remove all non-digits if no leading +</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should handle already clean input</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; normalize</div><div class="test-title">should not duplicate + signs</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div></div></details><details id="suite-2" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Volumes/Projects/RND/microservices/vieltalk-chat/core-mb/__unitest__/phone.number.crypto.spec.ts</div><div class="suite-time">2.611s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should encrypt and decrypt correctly</div><div class="test-status">passed</div><div class="test-duration">0.009s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should produce different ciphertexts for same phone (unique IV)</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should throw when tampering with ciphertext</div><div class="test-status">passed</div><div class="test-duration">0.009s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should throw when tampering with authTag</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div></div></details></main></body></html>