@outputai/core 0.10.1-next.b7b2fbe.0 → 0.10.1-next.bf95a6f.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/package.json +13 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +7 -3
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +23 -14
- package/src/helpers/errors.spec.js +31 -75
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/object.js +48 -35
- package/src/helpers/object.spec.js +123 -75
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +76 -78
- package/src/hooks/index.js +57 -49
- package/src/hooks/index.spec.js +104 -105
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/index.js +2 -1
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +64 -85
- package/src/interface/workflow.spec.js +220 -226
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/sdk/helpers/objects.d.ts +24 -19
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +24 -11
- package/src/worker/index.spec.js +36 -16
- package/src/worker/interceptors/activity.js +34 -21
- package/src/worker/interceptors/activity.spec.js +77 -172
- package/src/worker/interceptors/workflow.js +58 -16
- package/src/worker/interceptors/workflow.spec.js +183 -56
- package/src/worker/loader/activities.js +56 -40
- package/src/worker/loader/activities.spec.js +21 -22
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +36 -5
- package/src/worker/log_hooks.js +22 -19
- package/src/worker/log_hooks.spec.js +14 -6
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/worker/webpack_loaders/consts.js +6 -0
- package/src/worker/webpack_loaders/tools.js +1 -146
- package/src/worker/webpack_loaders/tools.spec.js +0 -23
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
|
@@ -1,89 +1,45 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
import { buildApplicationFailureWithDetails } from './errors.js';
|
|
2
|
+
import { inheritsFromAnyNamedType } from './errors.js';
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
describe( 'inheritsFromAnyNamedType', () => {
|
|
5
|
+
class BaseFailure extends Error {}
|
|
6
|
+
class DomainFailure extends BaseFailure {}
|
|
7
|
+
class RequestFailure extends DomainFailure {}
|
|
8
|
+
class UnrelatedFailure extends Error {}
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const error = new Error( 'step failed' );
|
|
10
|
-
const info = { aggregations: { cost: { total: 1 } } };
|
|
10
|
+
it( 'matches exact, parent, and grandparent constructor names', () => {
|
|
11
|
+
const error = new RequestFailure( 'request failed' );
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
expect(
|
|
15
|
-
expect(
|
|
16
|
-
message: 'step failed',
|
|
17
|
-
type: 'Error',
|
|
18
|
-
nonRetryable: false,
|
|
19
|
-
details: [ info ],
|
|
20
|
-
cause: error
|
|
21
|
-
} );
|
|
22
|
-
} );
|
|
23
|
-
|
|
24
|
-
it( 'uses the original constructor name for custom errors', () => {
|
|
25
|
-
const error = new CustomFailure( 'custom failed' );
|
|
26
|
-
const info = { trace: { destinations: { local: '/tmp/trace' } } };
|
|
27
|
-
|
|
28
|
-
const failure = buildApplicationFailureWithDetails( error, info );
|
|
29
|
-
|
|
30
|
-
expect( failure ).toMatchObject( {
|
|
31
|
-
message: 'custom failed',
|
|
32
|
-
type: 'CustomFailure',
|
|
33
|
-
details: [ info ],
|
|
34
|
-
cause: error
|
|
35
|
-
} );
|
|
13
|
+
expect( inheritsFromAnyNamedType( error, [ 'RequestFailure' ] ) ).toBe( true );
|
|
14
|
+
expect( inheritsFromAnyNamedType( error, [ 'DomainFailure' ] ) ).toBe( true );
|
|
15
|
+
expect( inheritsFromAnyNamedType( error, [ 'BaseFailure' ] ) ).toBe( true );
|
|
16
|
+
expect( inheritsFromAnyNamedType( error, [ 'OtherFailure', 'BaseFailure' ] ) ).toBe( true );
|
|
36
17
|
} );
|
|
37
18
|
|
|
38
|
-
it( '
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const info = { aggregations: { httpRequests: { total: 1 } } };
|
|
43
|
-
|
|
44
|
-
const failure = buildApplicationFailureWithDetails( error, info );
|
|
45
|
-
|
|
46
|
-
expect( failure.details ).toEqual( [
|
|
47
|
-
{ domain: { reason: 'bad-input' } },
|
|
48
|
-
info
|
|
49
|
-
] );
|
|
50
|
-
expect( error.details ).toBe( existingDetails );
|
|
51
|
-
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
19
|
+
it( 'does not match unrelated or descendant constructor names', () => {
|
|
20
|
+
expect( inheritsFromAnyNamedType( new RequestFailure(), [ 'UnrelatedFailure' ] ) ).toBe( false );
|
|
21
|
+
expect( inheritsFromAnyNamedType( new BaseFailure(), [ 'RequestFailure' ] ) ).toBe( false );
|
|
22
|
+
expect( inheritsFromAnyNamedType( new UnrelatedFailure(), [ 'BaseFailure' ] ) ).toBe( false );
|
|
52
23
|
} );
|
|
53
24
|
|
|
54
|
-
it( 'ignores
|
|
55
|
-
const error = new
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const failure = buildApplicationFailureWithDetails( error, info );
|
|
25
|
+
it( 'ignores a spoofed own constructor property', () => {
|
|
26
|
+
const error = new RequestFailure();
|
|
27
|
+
Object.defineProperty( error, 'constructor', {
|
|
28
|
+
value: { name: 'SpoofedFailure' }
|
|
29
|
+
} );
|
|
60
30
|
|
|
61
|
-
expect(
|
|
31
|
+
expect( inheritsFromAnyNamedType( error, [ 'SpoofedFailure' ] ) ).toBe( false );
|
|
32
|
+
expect( inheritsFromAnyNamedType( error, [ 'RequestFailure' ] ) ).toBe( true );
|
|
62
33
|
} );
|
|
63
34
|
|
|
64
|
-
it( '
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
nonRetryable: true,
|
|
69
|
-
details: [ { domain: { reason: 'bad-input' } } ]
|
|
70
|
-
} );
|
|
71
|
-
const info = { aggregations: { cost: { total: 2 } } };
|
|
72
|
-
|
|
73
|
-
const failure = buildApplicationFailureWithDetails( original, info );
|
|
35
|
+
it( 'rejects empty names and invalid values without false positives', () => {
|
|
36
|
+
const unnamedConstructor = function NamedConstructor() {};
|
|
37
|
+
Object.defineProperty( unnamedConstructor, 'name', { value: '' } );
|
|
38
|
+
const value = Object.create( { constructor: unnamedConstructor } );
|
|
74
39
|
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
77
|
-
expect(
|
|
78
|
-
expect( failure
|
|
79
|
-
expect( failure ).toMatchObject( {
|
|
80
|
-
message: 'application failed',
|
|
81
|
-
type: 'DomainFailure',
|
|
82
|
-
nonRetryable: true,
|
|
83
|
-
details: [
|
|
84
|
-
{ domain: { reason: 'bad-input' } },
|
|
85
|
-
info
|
|
86
|
-
]
|
|
87
|
-
} );
|
|
40
|
+
expect( inheritsFromAnyNamedType( value, [ '', undefined ] ) ).toBe( false );
|
|
41
|
+
expect( inheritsFromAnyNamedType( value, [] ) ).toBe( false );
|
|
42
|
+
expect( inheritsFromAnyNamedType( null, [ 'BaseFailure' ] ) ).toBe( false );
|
|
43
|
+
expect( inheritsFromAnyNamedType( 'failure', [ 'String' ] ) ).toBe( false );
|
|
88
44
|
} );
|
|
89
45
|
} );
|
package/src/helpers/fetch.js
CHANGED
|
@@ -1,42 +1,5 @@
|
|
|
1
1
|
import { FatalError } from '#errors';
|
|
2
|
-
|
|
3
|
-
/** Matches red int "hot-red-pie", but not int "redact" */
|
|
4
|
-
const wordMatcher = term => new RegExp( `(?<![a-z\\d])${term}(?![a-z\\d])`, 'i' );
|
|
5
|
-
|
|
6
|
-
/** Matches red in "acquired", but not in "redact" */
|
|
7
|
-
const wordEndMatcher = term => new RegExp( `${term}(?![a-z\\d])`, 'i' );
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Redacts sensitive headers
|
|
11
|
-
* @param {object} headers
|
|
12
|
-
* @returns {object} The redacted headers
|
|
13
|
-
*/
|
|
14
|
-
export const redactHeaders = headers => {
|
|
15
|
-
/** Header names that look sensitive by substring rules but are not secret material. */
|
|
16
|
-
const ignoreHeaders = new Set( [
|
|
17
|
-
'x-csrf-token',
|
|
18
|
-
'public-key-pins'
|
|
19
|
-
] );
|
|
20
|
-
|
|
21
|
-
/** * Sensitive header patterns for redaction (case-insensitive). */
|
|
22
|
-
const sensitiveHeadersPatterns = [
|
|
23
|
-
// matches headers that contain these exact words
|
|
24
|
-
wordMatcher( 'authorization' ),
|
|
25
|
-
wordMatcher( 'token' ),
|
|
26
|
-
wordMatcher( 'secret' ),
|
|
27
|
-
wordMatcher( 'password' ),
|
|
28
|
-
wordMatcher( 'pwd' ),
|
|
29
|
-
wordMatcher( 'cookie' ),
|
|
30
|
-
// matches header that contain words ending with these sequences
|
|
31
|
-
wordEndMatcher( 'key' )
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
return Object.entries( headers ).reduce( ( redacted, [ key, value ] ) => {
|
|
35
|
-
const lowKey = key.toLowerCase();
|
|
36
|
-
const isSensitive = !ignoreHeaders.has( lowKey ) && sensitiveHeadersPatterns.some( rx => rx.test( lowKey ) );
|
|
37
|
-
return Object.assign( redacted, { [key]: isSensitive ? '[REDACTED]' : value } );
|
|
38
|
-
}, {} );
|
|
39
|
-
};
|
|
2
|
+
import { redactHeaders } from './redact.js';
|
|
40
3
|
|
|
41
4
|
/**
|
|
42
5
|
* Consume the body of a Response according it its content-type and returns it
|
|
@@ -1,44 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, it, expect } from 'vitest';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
|
-
import { hydrateHeaders,
|
|
4
|
-
|
|
5
|
-
describe( 'redactHeaders', () => {
|
|
6
|
-
it( 'redacts sensitive header names', () => {
|
|
7
|
-
const result = redactHeaders( {
|
|
8
|
-
Authorization: 'Bearer token',
|
|
9
|
-
'X-Api-Key': 'api-key',
|
|
10
|
-
Cookie: 'session=id',
|
|
11
|
-
'x-client-secret': 'secret'
|
|
12
|
-
} );
|
|
13
|
-
|
|
14
|
-
expect( result ).toEqual( {
|
|
15
|
-
Authorization: '[REDACTED]',
|
|
16
|
-
'X-Api-Key': '[REDACTED]',
|
|
17
|
-
Cookie: '[REDACTED]',
|
|
18
|
-
'x-client-secret': '[REDACTED]'
|
|
19
|
-
} );
|
|
20
|
-
} );
|
|
21
|
-
|
|
22
|
-
it( 'preserves non-sensitive header names and ignored false positives', () => {
|
|
23
|
-
const result = redactHeaders( {
|
|
24
|
-
Accept: 'application/json',
|
|
25
|
-
'Content-Type': 'application/json',
|
|
26
|
-
'x-csrf-token': 'csrf-token',
|
|
27
|
-
'public-key-pins': 'pin'
|
|
28
|
-
} );
|
|
29
|
-
|
|
30
|
-
expect( result ).toEqual( {
|
|
31
|
-
Accept: 'application/json',
|
|
32
|
-
'Content-Type': 'application/json',
|
|
33
|
-
'x-csrf-token': 'csrf-token',
|
|
34
|
-
'public-key-pins': 'pin'
|
|
35
|
-
} );
|
|
36
|
-
} );
|
|
37
|
-
|
|
38
|
-
it( 'handles empty headers', () => {
|
|
39
|
-
expect( redactHeaders( {} ) ).toEqual( {} );
|
|
40
|
-
} );
|
|
41
|
-
} );
|
|
3
|
+
import { hydrateHeaders, serializeBodyAndInferContentType, serializeResponse } from './fetch.js';
|
|
42
4
|
|
|
43
5
|
describe( 'hydrateHeaders', () => {
|
|
44
6
|
afterEach( () => {
|
package/src/helpers/function.js
CHANGED
|
@@ -13,3 +13,16 @@ export const runOnce = fn => {
|
|
|
13
13
|
return state.result;
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calls a given function and in case it throws errors, returns `undefined`
|
|
19
|
+
* @param {Function} fn
|
|
20
|
+
* @returns {unknown|undefined}
|
|
21
|
+
*/
|
|
22
|
+
export const tryOrUndefined = fn => {
|
|
23
|
+
try {
|
|
24
|
+
return fn();
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined; // eslint-disable-line consistent-return
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { runOnce } from './function.js';
|
|
2
|
+
import { runOnce, tryOrUndefined } from './function.js';
|
|
3
3
|
|
|
4
4
|
describe( 'runOnce', () => {
|
|
5
5
|
it( 'calls the wrapped function only once', () => {
|
|
@@ -46,3 +46,25 @@ describe( 'runOnce', () => {
|
|
|
46
46
|
expect( fn ).toHaveBeenCalledOnce();
|
|
47
47
|
} );
|
|
48
48
|
} );
|
|
49
|
+
|
|
50
|
+
describe( 'tryOrUndefined', () => {
|
|
51
|
+
it( 'returns the invoked function result', () => {
|
|
52
|
+
const fn = vi.fn( () => 'result' );
|
|
53
|
+
|
|
54
|
+
expect( tryOrUndefined( fn ) ).toBe( 'result' );
|
|
55
|
+
expect( fn ).toHaveBeenCalledOnce();
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'returns undefined when the invoked function throws', () => {
|
|
59
|
+
const fn = vi.fn( () => {
|
|
60
|
+
throw new Error( 'unavailable' );
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
expect( tryOrUndefined( fn ) ).toBeUndefined();
|
|
64
|
+
expect( fn ).toHaveBeenCalledOnce();
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
it( 'preserves an explicit undefined result', () => {
|
|
68
|
+
expect( tryOrUndefined( () => undefined ) ).toBeUndefined();
|
|
69
|
+
} );
|
|
70
|
+
} );
|
package/src/helpers/object.js
CHANGED
|
@@ -24,58 +24,71 @@ export const clone = v => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Creates a new object merging object
|
|
28
|
-
*
|
|
29
|
-
* - Object "
|
|
30
|
-
* - Object "
|
|
27
|
+
* Creates a new object recursively merging the rightmost object over the previous one using a resolver function.
|
|
28
|
+
* Give two objects, (L)eft and (R)right
|
|
29
|
+
* - Object "R" will overwrite fields on object "L" based on the result of the resolver function;
|
|
30
|
+
* - Object "R" fields that don't exist on object "L" will be added;
|
|
31
|
+
* - Object "L" fields that don't exist on object "R" will be preserved;
|
|
31
32
|
*
|
|
32
|
-
* If "
|
|
33
|
+
* If "R" isn't an object, a new object equal to "L" is returned.
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* The resolver function will define the final value of the merge, it receives two args value "L" and "R".
|
|
36
|
+
*
|
|
37
|
+
*
|
|
38
|
+
* @param {object} base - The base object
|
|
39
|
+
* @param {...(object|function)} args - Target objects followed by the resolver function
|
|
37
40
|
* @returns {object} A new object
|
|
38
41
|
*/
|
|
39
|
-
export const deepMergeWithResolver = (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
export const deepMergeWithResolver = ( base, ...args ) => {
|
|
43
|
+
const objects = args.slice( 0, -1 );
|
|
44
|
+
const resolver = args.at( -1 );
|
|
45
|
+
|
|
46
|
+
if ( !isPlainObject( base ) ) {
|
|
47
|
+
throw new Error( 'First argument (base object) is not an object.' );
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
if ( typeof resolver !== 'function' ) {
|
|
51
|
+
throw new Error( 'Last argument (resolver) is not a function.' );
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
|
|
54
|
+
return objects.reduce( ( merged, object ) => {
|
|
55
|
+
if ( !isPlainObject( object ) ) {
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for ( const [ k, v ] of Object.entries( object ) ) {
|
|
60
|
+
if ( isPlainObject( v ) && isPlainObject( merged[k] ) ) {
|
|
61
|
+
merged[k] = deepMergeWithResolver( merged[k], v, resolver );
|
|
62
|
+
} else if ( Object.hasOwn( merged, k ) ) {
|
|
63
|
+
merged[k] = resolver( merged[k], v );
|
|
64
|
+
} else {
|
|
65
|
+
merged[k] = v;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return merged;
|
|
69
|
+
}, clone( base ) );
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
/**
|
|
62
|
-
* Creates a new object merging
|
|
63
|
-
*
|
|
64
|
-
* - Object "
|
|
65
|
-
* - Object "
|
|
73
|
+
* Creates a new object recursively merging the rightmost object over the previous one.
|
|
74
|
+
* Give two objects, (L)eft and (R)right
|
|
75
|
+
* - Object "R" will overwrite fields on object "L";
|
|
76
|
+
* - Object "R" fields that don't exist on object "L" will be added;
|
|
77
|
+
* - Object "L" fields that don't exist on object "R" will be preserved;
|
|
66
78
|
*
|
|
67
|
-
* If "
|
|
79
|
+
* If "R" isn't an object, a new object equal to "L" is returned.
|
|
68
80
|
*
|
|
69
|
-
* @param {object}
|
|
70
|
-
* @param {object}
|
|
81
|
+
* @param {object} base - The base object
|
|
82
|
+
* @param {...object} rest - The target objects
|
|
71
83
|
* @returns {object} A new object
|
|
72
84
|
*/
|
|
73
|
-
export const deepMerge = (
|
|
85
|
+
export const deepMerge = ( base, ...rest ) =>
|
|
86
|
+
deepMergeWithResolver( base, ...rest, ( _, b ) => b );
|
|
74
87
|
|
|
75
88
|
/**
|
|
76
89
|
* Adds an non-writable, non-configurable and non-enumerable property to an object
|
|
77
90
|
* @param {object} obj
|
|
78
|
-
* @param {string} key
|
|
91
|
+
* @param {string|Symbol} key
|
|
79
92
|
* @param {any} value
|
|
80
93
|
* @returns
|
|
81
94
|
*/
|
|
@@ -95,98 +95,86 @@ describe( 'clone', () => {
|
|
|
95
95
|
} );
|
|
96
96
|
|
|
97
97
|
describe( 'deepMerge', () => {
|
|
98
|
-
it( '
|
|
98
|
+
it( 'returns a clone when only the base object is provided', () => {
|
|
99
99
|
const a = {
|
|
100
|
-
|
|
101
|
-
b: {
|
|
102
|
-
c: 2
|
|
103
|
-
}
|
|
100
|
+
nested: { value: 1 }
|
|
104
101
|
};
|
|
105
|
-
const
|
|
106
|
-
a: false,
|
|
107
|
-
b: {
|
|
108
|
-
c: true
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
expect( deepMerge( a, b ) ).toEqual( {
|
|
112
|
-
a: false,
|
|
113
|
-
b: {
|
|
114
|
-
c: true
|
|
115
|
-
}
|
|
116
|
-
} );
|
|
117
|
-
} );
|
|
102
|
+
const result = deepMerge( a );
|
|
118
103
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const b = {
|
|
124
|
-
a: false,
|
|
125
|
-
b: true
|
|
126
|
-
};
|
|
127
|
-
expect( deepMerge( a, b ) ).toEqual( {
|
|
128
|
-
a: false,
|
|
129
|
-
b: true
|
|
104
|
+
a.nested.value = 2;
|
|
105
|
+
|
|
106
|
+
expect( result ).toEqual( {
|
|
107
|
+
nested: { value: 1 }
|
|
130
108
|
} );
|
|
131
109
|
} );
|
|
132
110
|
|
|
133
|
-
it( '
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
111
|
+
it( 'Throws when first argument is not a plain object', () => {
|
|
112
|
+
expect( () => deepMerge( null ) ).toThrow( Error );
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
it( 'merges multiple objects from left to right using rightmost values', () => {
|
|
116
|
+
expect( deepMerge(
|
|
117
|
+
{
|
|
118
|
+
a: 1,
|
|
119
|
+
nested: {
|
|
120
|
+
a: 'base',
|
|
121
|
+
kept: true
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
b: 2,
|
|
126
|
+
nested: {
|
|
127
|
+
a: 'first',
|
|
128
|
+
b: 'first'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
a: 3,
|
|
133
|
+
nested: {
|
|
134
|
+
b: 'second',
|
|
135
|
+
c: 'second'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
) ).toEqual( {
|
|
139
|
+
a: 3,
|
|
140
|
+
b: 2,
|
|
141
|
+
nested: {
|
|
142
|
+
a: 'first',
|
|
143
|
+
b: 'second',
|
|
144
|
+
c: 'second',
|
|
145
|
+
kept: true
|
|
146
|
+
}
|
|
143
147
|
} );
|
|
144
148
|
} );
|
|
145
149
|
|
|
146
|
-
it( '
|
|
147
|
-
|
|
148
|
-
a: 1
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
it( 'ignores non-object values among multiple overlays', () => {
|
|
151
|
+
expect( deepMerge(
|
|
152
|
+
{ a: 1 },
|
|
153
|
+
null,
|
|
154
|
+
{ b: 2 },
|
|
155
|
+
undefined,
|
|
156
|
+
{ a: 3 }
|
|
157
|
+
) ).toEqual( {
|
|
158
|
+
a: 3,
|
|
159
|
+
b: 2
|
|
160
|
+
} );
|
|
157
161
|
} );
|
|
162
|
+
} );
|
|
158
163
|
|
|
159
|
-
|
|
164
|
+
describe( 'deepMergeWithResolver', () => {
|
|
165
|
+
it( 'returns a clone when only the base object and resolver are provided', () => {
|
|
160
166
|
const a = {
|
|
161
|
-
|
|
167
|
+
nested: { value: 1 }
|
|
162
168
|
};
|
|
163
|
-
|
|
164
|
-
expect( deepMerge( a, undefined ) ).toEqual( { a: 1 } );
|
|
165
|
-
} );
|
|
169
|
+
const result = deepMergeWithResolver( a, ( x, y ) => x + y );
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
const a = {
|
|
169
|
-
a: 1
|
|
170
|
-
};
|
|
171
|
-
const result = deepMerge( a, null );
|
|
172
|
-
a.a = 2;
|
|
173
|
-
expect( result.a ).toEqual( 1 );
|
|
174
|
-
} );
|
|
171
|
+
a.nested.value = 2;
|
|
175
172
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
expect( () => deepMerge( 'a' ) ).toThrow( Error );
|
|
180
|
-
expect( () => deepMerge( true ) ).toThrow( Error );
|
|
181
|
-
expect( () => deepMerge( /a/ ) ).toThrow( Error );
|
|
182
|
-
expect( () => deepMerge( [] ) ).toThrow( Error );
|
|
183
|
-
expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
|
|
184
|
-
expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
|
|
185
|
-
expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
|
|
173
|
+
expect( result ).toEqual( {
|
|
174
|
+
nested: { value: 1 }
|
|
175
|
+
} );
|
|
186
176
|
} );
|
|
187
|
-
} );
|
|
188
177
|
|
|
189
|
-
describe( 'deepMergeWithResolver', () => {
|
|
190
178
|
it( 'uses resolver for existing leaf values, including nested leaves', () => {
|
|
191
179
|
const a = {
|
|
192
180
|
cost: { total: 1 },
|
|
@@ -235,6 +223,66 @@ describe( 'deepMergeWithResolver', () => {
|
|
|
235
223
|
expect( () => deepMergeWithResolver( [], {}, ( x, y ) => x + y ) ).toThrow( Error );
|
|
236
224
|
expect( () => deepMergeWithResolver( 'a', {}, ( x, y ) => x + y ) ).toThrow( Error );
|
|
237
225
|
} );
|
|
226
|
+
|
|
227
|
+
it( 'throws when last argument is not a resolver function', () => {
|
|
228
|
+
expect( () => deepMergeWithResolver( { a: 1 }, { a: 2 } ) )
|
|
229
|
+
.toThrow( 'Last argument (resolver) is not a function.' );
|
|
230
|
+
} );
|
|
231
|
+
|
|
232
|
+
it( 'merges multiple objects using the resolver from left to right', () => {
|
|
233
|
+
const resolver = vi.fn( ( x, y ) => `${ x }:${ y }` );
|
|
234
|
+
|
|
235
|
+
expect( deepMergeWithResolver(
|
|
236
|
+
{
|
|
237
|
+
a: 'base',
|
|
238
|
+
nested: {
|
|
239
|
+
count: 1,
|
|
240
|
+
kept: 'yes'
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
a: 'first',
|
|
245
|
+
nested: {
|
|
246
|
+
count: 2,
|
|
247
|
+
firstOnly: true
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
a: 'second',
|
|
252
|
+
nested: {
|
|
253
|
+
count: 3,
|
|
254
|
+
secondOnly: true
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
resolver
|
|
258
|
+
) ).toEqual( {
|
|
259
|
+
a: 'base:first:second',
|
|
260
|
+
nested: {
|
|
261
|
+
count: '1:2:3',
|
|
262
|
+
firstOnly: true,
|
|
263
|
+
kept: 'yes',
|
|
264
|
+
secondOnly: true
|
|
265
|
+
}
|
|
266
|
+
} );
|
|
267
|
+
expect( resolver ).toHaveBeenCalledTimes( 4 );
|
|
268
|
+
} );
|
|
269
|
+
|
|
270
|
+
it( 'ignores non-object values among multiple resolver overlays', () => {
|
|
271
|
+
const resolver = vi.fn( ( x, y ) => x + y );
|
|
272
|
+
|
|
273
|
+
expect( deepMergeWithResolver(
|
|
274
|
+
{ a: 1 },
|
|
275
|
+
null,
|
|
276
|
+
{ a: 2 },
|
|
277
|
+
undefined,
|
|
278
|
+
{ b: 3 },
|
|
279
|
+
resolver
|
|
280
|
+
) ).toEqual( {
|
|
281
|
+
a: 3,
|
|
282
|
+
b: 3
|
|
283
|
+
} );
|
|
284
|
+
expect( resolver ).toHaveBeenCalledOnce();
|
|
285
|
+
} );
|
|
238
286
|
} );
|
|
239
287
|
|
|
240
288
|
describe( 'isPlainObject', () => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redacts a given URL object by setting each search param, hash, username and password to '***'
|
|
3
|
+
* @param {URL|string} target
|
|
4
|
+
* @returns {string} Redacted URL as a string
|
|
5
|
+
*/
|
|
6
|
+
export const redactUrl = target => {
|
|
7
|
+
const url = new URL( target );
|
|
8
|
+
url.search = new URLSearchParams( url.searchParams.entries().map( ( [ p ] ) => [ p, '***' ] ) ).toString();
|
|
9
|
+
url.username = url.username.length > 0 ? '***' : '';
|
|
10
|
+
url.password = url.password.length > 0 ? '***' : '';
|
|
11
|
+
url.hash = url.hash ? '#***' : '';
|
|
12
|
+
return url.toString();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Matches red in "hot-red-pie", but not in "redact" */
|
|
16
|
+
const wordMatcher = term => new RegExp( `(?<![a-z\\d])${term}(?![a-z\\d])`, 'i' );
|
|
17
|
+
|
|
18
|
+
/** Matches red in "acquired", but not in "redact" */
|
|
19
|
+
const wordEndMatcher = term => new RegExp( `${term}(?![a-z\\d])`, 'i' );
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Redacts sensitive headers
|
|
23
|
+
* @param {object} headers
|
|
24
|
+
* @returns {object} The redacted headers
|
|
25
|
+
*/
|
|
26
|
+
export const redactHeaders = headers => {
|
|
27
|
+
/** Header names that look sensitive by substring rules but are not secret material. */
|
|
28
|
+
const ignoreHeaders = new Set( [
|
|
29
|
+
'x-csrf-token',
|
|
30
|
+
'public-key-pins'
|
|
31
|
+
] );
|
|
32
|
+
|
|
33
|
+
/** * Sensitive header patterns for redaction (case-insensitive). */
|
|
34
|
+
const sensitiveHeadersPatterns = [
|
|
35
|
+
// matches headers that contain these exact words
|
|
36
|
+
wordMatcher( 'authorization' ),
|
|
37
|
+
wordMatcher( 'token' ),
|
|
38
|
+
wordMatcher( 'secret' ),
|
|
39
|
+
wordMatcher( 'password' ),
|
|
40
|
+
wordMatcher( 'pwd' ),
|
|
41
|
+
wordMatcher( 'cookie' ),
|
|
42
|
+
// matches header that contain words ending with these sequences
|
|
43
|
+
wordEndMatcher( 'key' )
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
return Object.entries( headers ).reduce( ( redacted, [ key, value ] ) => {
|
|
47
|
+
const lowKey = key.toLowerCase();
|
|
48
|
+
const isSensitive = !ignoreHeaders.has( lowKey ) && sensitiveHeadersPatterns.some( rx => rx.test( lowKey ) );
|
|
49
|
+
return Object.assign( redacted, { [key]: isSensitive ? '[REDACTED]' : value } );
|
|
50
|
+
}, {} );
|
|
51
|
+
};
|