core-services-sdk 1.3.28 → 1.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.28",
3
+ "version": "1.3.30",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -0,0 +1,81 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+
3
+ const als = new AsyncLocalStorage()
4
+
5
+ /**
6
+ * Context utility built on top of Node.js AsyncLocalStorage.
7
+ * Provides a per-request (or per-async-chain) storage mechanism that
8
+ * allows passing metadata (like correlation IDs, user info, tenant ID, etc.)
9
+ * without explicitly threading it through every function call.
10
+ */
11
+
12
+ export const Context = {
13
+ /**
14
+ * Run a callback within a given context store.
15
+ * Everything `await`ed or invoked inside this callback will have access
16
+ * to the provided store via {@link Context.get}, {@link Context.set}, or {@link Context.all}.
17
+ *
18
+ * @template T
19
+ * @param {Record<string, any>} store
20
+ * @param {() => T} callback - Function to execute inside the context.
21
+ * @returns {T} The return value of the callback (sync or async).
22
+ *
23
+ * @example
24
+ * Context.run(
25
+ * { correlationId: 'abc123' },
26
+ * async () => {
27
+ * console.log(Context.get('correlationId')) // "abc123"
28
+ * }
29
+ * )
30
+ */
31
+ run(store, callback) {
32
+ return als.run(store, callback)
33
+ },
34
+
35
+ /**
36
+ * Retrieve a single value from the current async context store.
37
+ *
38
+ * @template T
39
+ * @param {string} key - The key of the value to retrieve.
40
+ * @returns {T|undefined} The stored value, or `undefined` if no store exists or key not found.
41
+ *
42
+ * @example
43
+ * const userId = Context.get('userId')
44
+ */
45
+ get(key) {
46
+ const store = als.getStore()
47
+ return store?.[key]
48
+ },
49
+
50
+ /**
51
+ * Set a single key-value pair in the current async context store.
52
+ * If there is no active store (i.e. outside of a {@link Context.run}),
53
+ * this function does nothing.
54
+ *
55
+ * @param {string} key - The key under which to store the value.
56
+ * @param {any} value - The value to store.
57
+ *
58
+ * @example
59
+ * Context.set('tenantId', 'tnt_1234')
60
+ */
61
+ set(key, value) {
62
+ const store = als.getStore()
63
+ if (store) {
64
+ store[key] = value
65
+ }
66
+ },
67
+
68
+ /**
69
+ * Get the entire store object for the current async context.
70
+ *
71
+ * @returns {Record<string, any>} The current store object,
72
+ * or an empty object if no store exists.
73
+ *
74
+ * @example
75
+ * const all = Context.all()
76
+ * console.log(all) // { correlationId: 'abc123', userId: 'usr_789' }
77
+ */
78
+ all() {
79
+ return als.getStore() || {}
80
+ },
81
+ }
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Mask middle of a primitive value while keeping left/right edges.
3
3
  * @param {string|number|boolean|null|undefined} value
4
- * @param {string} [fill='']
5
- * @param {number} [maskLen=3]
4
+ * @param {string} [fill='.']
5
+ * @param {number} [maskLen=2]
6
6
  * @param {number} [left=4]
7
7
  * @param {number} [right=4]
8
8
  * @returns {string}
9
9
  */
10
10
  export const maskSingle = (
11
11
  value,
12
- fill = '',
13
- maskLen = 3,
14
- left = 4,
15
- right = 4,
12
+ fill = '.',
13
+ maskLen = null,
14
+ left = 2,
15
+ right = 2,
16
16
  ) => {
17
17
  if (value == null) {
18
18
  return ''
@@ -21,14 +21,19 @@ export const maskSingle = (
21
21
  if (str.length === 0) {
22
22
  return ''
23
23
  }
24
- const m = Math.max(1, maskLen)
24
+
25
+ if (typeof value === 'boolean') {
26
+ return str
27
+ }
28
+ const m =
29
+ null === maskLen ? Math.max(1, str.length - (right + left)) : maskLen
25
30
 
26
31
  if (str.length <= left + right) {
27
32
  if (str.length === 1) {
28
33
  return fill
29
34
  }
30
35
  if (str.length === 2) {
31
- return str[0] + fill.repeat(2) // "ab" -> "a••"
36
+ return str[0] + fill.repeat(1) // "ab" -> "a.."
32
37
  }
33
38
  return str.slice(0, 1) + fill.repeat(m) + str.slice(-1)
34
39
  }
@@ -39,13 +44,19 @@ export const maskSingle = (
39
44
  /**
40
45
  * Recursively mask values in strings, numbers, booleans, arrays, and objects.
41
46
  * @param {string|number|boolean|Array|Object|null|undefined} value
42
- * @param {string} [fill='']
43
- * @param {number} [maskLen=3]
47
+ * @param {string} [fill='.']
48
+ * @param {number} [maskLen=2]
44
49
  * @param {number} [left=4]
45
50
  * @param {number} [right=4]
46
51
  * @returns {string|Array|Object}
47
52
  */
48
- export const mask = (value, fill = '•', maskLen = 3, left = 4, right = 4) => {
53
+ export const mask = (
54
+ value,
55
+ fill = '.',
56
+ maskLen = null,
57
+ left = 2,
58
+ right = 2,
59
+ ) => {
49
60
  const type = typeof value
50
61
 
51
62
  if (value instanceof Date) {
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { Context } from '../../src/util/context.js'
3
+
4
+ describe('Context utility', () => {
5
+ it('should return empty object and undefined outside run()', () => {
6
+ expect(Context.all()).toEqual({})
7
+ expect(Context.get('foo')).toBeUndefined()
8
+ })
9
+
10
+ it('should provide store values inside run()', () => {
11
+ Context.run({ correlationId: 'abc123', userId: 'usr_1' }, () => {
12
+ expect(Context.get('correlationId')).toBe('abc123')
13
+ expect(Context.get('userId')).toBe('usr_1')
14
+ expect(Context.all()).toEqual({
15
+ correlationId: 'abc123',
16
+ userId: 'usr_1',
17
+ })
18
+ })
19
+ })
20
+
21
+ it('should allow setting new values inside run()', () => {
22
+ Context.run({ initial: true }, () => {
23
+ expect(Context.get('initial')).toBe(true)
24
+ Context.set('added', 42)
25
+ expect(Context.get('added')).toBe(42)
26
+ expect(Context.all()).toEqual({ initial: true, added: 42 })
27
+ })
28
+ })
29
+
30
+ it('should isolate different runs()', () => {
31
+ Context.run({ value: 'first' }, () => {
32
+ expect(Context.get('value')).toBe('first')
33
+ })
34
+ Context.run({ value: 'second' }, () => {
35
+ expect(Context.get('value')).toBe('second')
36
+ })
37
+ })
38
+
39
+ it('should preserve context across async/await', async () => {
40
+ await Context.run({ requestId: 'req-1' }, async () => {
41
+ expect(Context.get('requestId')).toBe('req-1')
42
+
43
+ await new Promise((resolve) => setTimeout(resolve, 10))
44
+ expect(Context.get('requestId')).toBe('req-1')
45
+
46
+ await Promise.resolve().then(() => {
47
+ expect(Context.get('requestId')).toBe('req-1')
48
+ })
49
+ })
50
+ })
51
+
52
+ it('should not leak context between async runs', async () => {
53
+ const results = await Promise.all([
54
+ Context.run({ user: 'alice' }, async () => {
55
+ await new Promise((resolve) => setTimeout(resolve, 5))
56
+ return Context.get('user')
57
+ }),
58
+ Context.run({ user: 'bob' }, async () => {
59
+ await new Promise((resolve) => setTimeout(resolve, 1))
60
+ return Context.get('user')
61
+ }),
62
+ ])
63
+
64
+ expect(results).toContain('alice')
65
+ expect(results).toContain('bob')
66
+ })
67
+ })
@@ -4,30 +4,37 @@ import { mask, maskSingle } from '../../src/util/index.js'
4
4
 
5
5
  describe('maskSingle', () => {
6
6
  it('masks middle of a regular string (length == left+right)', () => {
7
- expect(maskSingle('abcdefgh')).toBe('a•••h')
7
+ const value = maskSingle('abcdefgh')
8
+ expect(value).toBe('ab....gh')
8
9
  })
9
10
 
10
11
  it('masks numbers with default settings', () => {
11
- expect(maskSingle(12345678)).toBe('1•••8')
12
+ expect(maskSingle(12345678)).toBe('12....78')
12
13
  })
13
14
 
14
15
  it('masks booleans', () => {
15
- expect(maskSingle(true)).toBe('t•••e')
16
- expect(maskSingle(false)).toBe('f•••e')
16
+ const trueValue = maskSingle(true)
17
+ expect(trueValue).toBe('true')
18
+ const falseValue = maskSingle(false)
19
+ expect(falseValue).toBe('false')
17
20
  })
18
21
 
19
22
  it('masks very short strings correctly', () => {
20
- expect(maskSingle('ab')).toBe('a••b'.slice(0, 3)) // will produce 'a••'
21
- expect(maskSingle('a')).toBe('•')
23
+ const value1 = maskSingle('ab')
24
+ expect(value1).toBe('a.') // will produce 'a.'
25
+ const value2 = maskSingle('a')
26
+ expect(value2).toBe('.')
22
27
  expect(maskSingle('')).toBe('')
23
28
  })
24
29
 
25
30
  it('respects custom fill and mask length', () => {
26
- expect(maskSingle('abcdefgh', '*', 5)).toBe('a*****h')
31
+ const value = maskSingle('abcdefgh', '*', 5)
32
+ expect(value).toBe('ab*****gh')
27
33
  })
28
34
 
29
35
  it('ensures maskLen is at least 1', () => {
30
- expect(maskSingle('abcdefgh', '*', 0)).toBe('a*h')
36
+ const value = maskSingle('abcdefgh', '*', 1, 1, 1)
37
+ expect(value).toBe('a*h')
31
38
  })
32
39
 
33
40
  it('returns empty string for null/undefined', () => {
@@ -38,9 +45,10 @@ describe('maskSingle', () => {
38
45
 
39
46
  describe('mask', () => {
40
47
  it('masks primitives (string, number, boolean)', () => {
41
- expect(mask('abcdefgh')).toBe('a•••h')
42
- expect(mask(12345678)).toBe('1•••8')
43
- expect(mask(true)).toBe('true')
48
+ expect(mask('abcdefgh')).toBe('ab....gh')
49
+ expect(mask(12345678)).toBe('12....78')
50
+ const trueValue = mask(true)
51
+ expect(trueValue).toBe('true')
44
52
  expect(mask(false)).toBe('false')
45
53
  })
46
54
 
@@ -50,20 +58,22 @@ describe('mask', () => {
50
58
  })
51
59
 
52
60
  it('masks arrays recursively', () => {
53
- expect(mask(['abcdefgh', 12345678])).toEqual(['a•••h', '1•••8'])
61
+ const value = mask(['abcdefgh', 12345678])
62
+ expect(value).toEqual(['ab....gh', '12....78'])
54
63
  })
55
64
 
56
65
  it('masks objects recursively', () => {
57
66
  expect(mask({ a: 'abcdefgh', b: 12345678 })).toEqual({
58
- a: 'a•••h',
59
- b: '1•••8',
67
+ a: 'ab....gh',
68
+ b: '12....78',
60
69
  })
61
70
  })
62
71
 
63
72
  it('masks nested objects/arrays recursively', () => {
64
73
  const input = { arr: ['abcdefgh', { num: 12345678 }] }
65
- const expected = { arr: ['a•••h', { num: '1•••8' }] }
66
- expect(mask(input)).toEqual(expected)
74
+ const expected = { arr: ['ab....gh', { num: '12....78' }] }
75
+ const value = mask(input)
76
+ expect(value).toEqual(expected)
67
77
  })
68
78
 
69
79
  it('handles Date instances by returning full ISO string', () => {
@@ -73,7 +83,8 @@ describe('mask', () => {
73
83
 
74
84
  it('respects custom fill and mask length in recursive calls', () => {
75
85
  const input = { val: 'abcdefgh' }
76
- const expected = { val: 'a*****h' }
77
- expect(mask(input, '*', 5)).toEqual(expected)
86
+ const expected = { val: 'ab*****gh' }
87
+ const value = mask(input, '*', 5)
88
+ expect(value).toEqual(expected)
78
89
  })
79
90
  })