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
|
@@ -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=
|
|
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 =
|
|
14
|
-
left =
|
|
15
|
-
right =
|
|
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
|
-
|
|
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(
|
|
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=
|
|
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 = (
|
|
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
|
-
|
|
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('
|
|
12
|
+
expect(maskSingle(12345678)).toBe('12....78')
|
|
12
13
|
})
|
|
13
14
|
|
|
14
15
|
it('masks booleans', () => {
|
|
15
|
-
|
|
16
|
-
expect(
|
|
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
|
-
|
|
21
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
42
|
-
expect(mask(12345678)).toBe('
|
|
43
|
-
|
|
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
|
-
|
|
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: '
|
|
59
|
-
b: '
|
|
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: ['
|
|
66
|
-
|
|
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: '
|
|
77
|
-
|
|
86
|
+
const expected = { val: 'ab*****gh' }
|
|
87
|
+
const value = mask(input, '*', 5)
|
|
88
|
+
expect(value).toEqual(expected)
|
|
78
89
|
})
|
|
79
90
|
})
|