unnbound-logger-sdk 3.1.2 → 3.2.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/CHANGELOG.md +21 -0
- package/README.md +20 -0
- package/dist/axios.d.ts +3 -0
- package/dist/axios.js +25 -19
- package/dist/encode.d.ts +5 -0
- package/dist/encode.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/logger.d.ts +3 -2
- package/dist/logger.js +78 -27
- package/dist/redaction.d.ts +3 -0
- package/dist/redaction.js +135 -0
- package/dist/span.js +1 -2
- package/dist/storage.d.ts +16 -0
- package/dist/storage.js +91 -3
- package/dist/trace.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/utils.d.ts +6 -2
- package/dist/utils.js +3 -3
- package/package.json +12 -13
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## [3.0.34](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-logger-sdk-v3.0.33...unnbound-logger-sdk-v3.0.34) (2025-11-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* setup ci/cd ([dd2640f](https://github.com/unnbounddev/unnbound-sdks/commit/dd2640f01e39f1342f3e08dc882164b50331c7bb))
|
|
7
|
+
|
|
8
|
+
## [3.0.33](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-logger-sdk-v3.0.32...unnbound-logger-sdk-v3.0.33) (2025-11-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* setup ci/cd ([520c03a](https://github.com/unnbounddev/unnbound-sdks/commit/520c03aacd978d7923b3bcf45c4f74a870be019d))
|
|
14
|
+
|
|
15
|
+
## [3.0.32](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-logger-sdk-v3.0.31...unnbound-logger-sdk-v3.0.32) (2025-11-25)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* setup ci/cd ([d3759c1](https://github.com/unnbounddev/unnbound-sdks/commit/d3759c14f7863e4c8d2f5e80bdbbf2446df79f1a))
|
|
21
|
+
* setup ci/cd ([7e06fc8](https://github.com/unnbounddev/unnbound-sdks/commit/7e06fc84ae7bafba3e9c2c0225f9f9d614fa9207))
|
package/README.md
CHANGED
|
@@ -148,6 +148,26 @@ logger.error({ err: new Error('Connection refused') }, 'Failed to sync order.');
|
|
|
148
148
|
Errors are serialized by Pino's `stdSerializers.err`, which emits `type`, `message`, and `stack`.
|
|
149
149
|
Unlike the other levels, `error` has no single-argument overload, so the message is required.
|
|
150
150
|
|
|
151
|
+
### Redaction Masks
|
|
152
|
+
|
|
153
|
+
`registerRedactionMasks` replaces each registered string with `[Redacted]` wherever it appears in a log line: messages, fields, errors, and child-logger bindings. `@ontemper/resources` registers the secrets it resolves; register other secret values yourself:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { logger, registerRedactionMasks } from 'unnbound-logger-sdk';
|
|
157
|
+
|
|
158
|
+
registerRedactionMasks([apiKey]);
|
|
159
|
+
const partnerLog = logger.child({ partner: 'acme', auth: `Bearer ${apiKey}` });
|
|
160
|
+
partnerLog.info('Calling partner.');
|
|
161
|
+
// Result: { ..., "partner": "acme", "auth": "Bearer [Redacted]", "message": "Calling partner." }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- Masks replace text in string values only. A property name that contains a secret is written unchanged.
|
|
165
|
+
- Masks apply to the finished line when it is written. A mask registered after `child()` or `setBindings()` still redacts those bindings, and the output of custom serializers and formatters is masked too.
|
|
166
|
+
- A mask registered inside a request scope applies to the whole scope, and stays active for the rest of the process after the scope ends. The process keeps at most 4096 of these retained masks and drops the oldest first; masks held by an active scope are not capped.
|
|
167
|
+
- A value that code transforms before logging (for example, Base64-encodes) no longer matches its mask.
|
|
168
|
+
|
|
169
|
+
Treat masking as a safeguard, not a guarantee: do not log secrets.
|
|
170
|
+
|
|
151
171
|
## HTTP Request/Response Logging
|
|
152
172
|
|
|
153
173
|
```typescript
|
package/dist/axios.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { startSpan } from './span';
|
|
|
3
3
|
import { type HttpOptions } from './types';
|
|
4
4
|
type GetPayload = (config: AxiosRequestConfig, res?: AxiosResponse) => object;
|
|
5
5
|
type RedactOptions = {
|
|
6
|
+
request?: boolean;
|
|
7
|
+
requestHeaders?: boolean;
|
|
6
8
|
response?: boolean;
|
|
9
|
+
responseHeaders?: boolean;
|
|
7
10
|
};
|
|
8
11
|
export type OnRequest = (config: AxiosRequestConfig, callback: (config: AxiosRequestConfig) => Promise<AxiosResponse<any>>) => Promise<AxiosResponse<any>>;
|
|
9
12
|
interface HttpClientOptions extends HttpOptions<GetPayload> {
|
package/dist/axios.js
CHANGED
|
@@ -16,25 +16,29 @@ const extractQueryParams = (url, config) => {
|
|
|
16
16
|
keys.forEach((key) => object.searchParams.delete(key));
|
|
17
17
|
return { url: object.toString(), query: keys.length > 0 ? query : undefined };
|
|
18
18
|
};
|
|
19
|
-
const buildOutgoingHttpPayload = (config, res, redact) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
const buildOutgoingHttpPayload = (config, res, redact) => {
|
|
20
|
+
const { url, query } = extractQueryParams(buildUrl(config.baseURL, config.url), config);
|
|
21
|
+
return {
|
|
22
|
+
type: 'http',
|
|
23
|
+
http: {
|
|
24
|
+
url,
|
|
25
|
+
query: redact?.request && query ? '[Redacted]' : query,
|
|
26
|
+
method: config.method?.toLowerCase() ?? 'get',
|
|
27
|
+
incoming: false,
|
|
28
|
+
request: {
|
|
29
|
+
headers: redact?.request || redact?.requestHeaders ? '[Redacted]' : config.headers,
|
|
30
|
+
body: redact?.request ? '[Redacted]' : (0, utils_1.safeJsonParse)(config.data),
|
|
31
|
+
},
|
|
32
|
+
response: res
|
|
33
|
+
? {
|
|
34
|
+
headers: redact?.response || redact?.responseHeaders ? '[Redacted]' : res.headers,
|
|
35
|
+
status: res.status,
|
|
36
|
+
body: redact?.response ? '[Redacted]' : (0, utils_1.safeJsonParse)(res.data),
|
|
37
|
+
}
|
|
38
|
+
: undefined,
|
|
28
39
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
headers: res?.headers,
|
|
32
|
-
status: res.status,
|
|
33
|
-
body: redact?.response ? '[Redacted]' : (0, utils_1.safeJsonParse)(res.data),
|
|
34
|
-
}
|
|
35
|
-
: undefined,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
40
|
+
};
|
|
41
|
+
};
|
|
38
42
|
const getNoopPayload = () => ({});
|
|
39
43
|
const onNoopRequest = (config, callback) => callback(config);
|
|
40
44
|
const tracedMarker = Symbol.for('unnbound-logger-sdk.traceAxios.traced');
|
|
@@ -54,7 +58,9 @@ const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRout
|
|
|
54
58
|
}) => {
|
|
55
59
|
if (tracedMarker in client)
|
|
56
60
|
return client;
|
|
57
|
-
const redact = redactOptions === true || redactOptions === false
|
|
61
|
+
const redact = redactOptions === true || redactOptions === false
|
|
62
|
+
? { request: redactOptions, response: redactOptions }
|
|
63
|
+
: redactOptions;
|
|
58
64
|
const createSpanWrappedRequest = (originalMethod, method) => {
|
|
59
65
|
const { headers: defaultHeaders, ...partialDefaultConfig } = client.defaults;
|
|
60
66
|
const headers = { ...defaultHeaders.common, ...(method && defaultHeaders[method]) };
|
package/dist/encode.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log records are read one line at a time — container stdout becomes one CloudWatch event per
|
|
3
|
+
* line and the log forwarder JSON-parses each event — so LF becomes CR to keep a value from
|
|
4
|
+
* splitting its own record. `whitespace-pre-wrap` still renders it as a break; `decode` restores LF.
|
|
5
|
+
*/
|
|
1
6
|
export declare const encode: (value: string) => string;
|
|
2
7
|
export declare const decode: (value: string) => string;
|
package/dist/encode.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.decode = exports.encode = void 0;
|
|
4
|
-
const
|
|
4
|
+
const redaction_1 = require("./redaction");
|
|
5
|
+
/**
|
|
6
|
+
* Log records are read one line at a time — container stdout becomes one CloudWatch event per
|
|
7
|
+
* line and the log forwarder JSON-parses each event — so LF becomes CR to keep a value from
|
|
8
|
+
* splitting its own record. `whitespace-pre-wrap` still renders it as a break; `decode` restores LF.
|
|
9
|
+
*/
|
|
10
|
+
const encode = (value) => (0, redaction_1.applyRedactionMasks)(value).replaceAll('\n', '\r');
|
|
5
11
|
exports.encode = encode;
|
|
6
12
|
const decode = (value) => value.replaceAll('\r', '\n');
|
|
7
13
|
exports.decode = decode;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { decode, encode } from './encode';
|
|
|
5
5
|
export type { ILogger, UnnboundLogger } from './logger';
|
|
6
6
|
export { logger } from './logger';
|
|
7
7
|
export { traceMiddleware } from './middleware';
|
|
8
|
+
export { applyRedactionMasks, registerRedactionMasks } from './redaction';
|
|
8
9
|
export { startSpan } from './span';
|
|
9
10
|
export type { UnnboundContext } from './storage';
|
|
10
11
|
export { storage, withStorage } from './storage';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.defaultTraceHeaderKey = exports.defaultMessageHeaderKey = exports.withTrace = exports.getTraceId = exports.withStorage = exports.storage = exports.startSpan = exports.traceMiddleware = exports.logger = exports.encode = exports.decode = exports.emitMetrics = exports.emitMetric = exports.traceAxios = void 0;
|
|
3
|
+
exports.defaultTraceHeaderKey = exports.defaultMessageHeaderKey = exports.withTrace = exports.getTraceId = exports.withStorage = exports.storage = exports.startSpan = exports.registerRedactionMasks = exports.applyRedactionMasks = exports.traceMiddleware = exports.logger = exports.encode = exports.decode = exports.emitMetrics = exports.emitMetric = exports.traceAxios = void 0;
|
|
4
4
|
var axios_1 = require("./axios");
|
|
5
5
|
Object.defineProperty(exports, "traceAxios", { enumerable: true, get: function () { return axios_1.traceAxios; } });
|
|
6
6
|
var emf_1 = require("./emf");
|
|
@@ -13,6 +13,9 @@ var logger_1 = require("./logger");
|
|
|
13
13
|
Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logger_1.logger; } });
|
|
14
14
|
var middleware_1 = require("./middleware");
|
|
15
15
|
Object.defineProperty(exports, "traceMiddleware", { enumerable: true, get: function () { return middleware_1.traceMiddleware; } });
|
|
16
|
+
var redaction_1 = require("./redaction");
|
|
17
|
+
Object.defineProperty(exports, "applyRedactionMasks", { enumerable: true, get: function () { return redaction_1.applyRedactionMasks; } });
|
|
18
|
+
Object.defineProperty(exports, "registerRedactionMasks", { enumerable: true, get: function () { return redaction_1.registerRedactionMasks; } });
|
|
16
19
|
var span_1 = require("./span");
|
|
17
20
|
Object.defineProperty(exports, "startSpan", { enumerable: true, get: function () { return span_1.startSpan; } });
|
|
18
21
|
var storage_1 = require("./storage");
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pino from 'pino';
|
|
2
|
-
import type { Bindings, DestinationStream } from 'pino';
|
|
2
|
+
import type { Bindings, ChildLoggerOptions, DestinationStream } from 'pino';
|
|
3
3
|
export interface ILogger {
|
|
4
4
|
trace(object: Bindings, message: string): void;
|
|
5
5
|
trace(message: string): void;
|
|
@@ -13,7 +13,8 @@ export interface ILogger {
|
|
|
13
13
|
err: unknown;
|
|
14
14
|
}>(object: O, message: string): void;
|
|
15
15
|
}
|
|
16
|
-
export interface UnnboundLogger extends ILogger, Omit<pino.Logger, keyof ILogger> {
|
|
16
|
+
export interface UnnboundLogger extends ILogger, Omit<pino.Logger, keyof ILogger | 'child'> {
|
|
17
|
+
child(bindings: Bindings, options?: ChildLoggerOptions): UnnboundLogger;
|
|
17
18
|
}
|
|
18
19
|
export declare const createLogger: (destination?: DestinationStream) => UnnboundLogger;
|
|
19
20
|
export declare const logger: UnnboundLogger;
|
package/dist/logger.js
CHANGED
|
@@ -8,6 +8,7 @@ const pino_1 = __importDefault(require("pino"));
|
|
|
8
8
|
const uuid_1 = require("uuid");
|
|
9
9
|
const encode_1 = require("./encode");
|
|
10
10
|
const internal_1 = require("./internal");
|
|
11
|
+
const redaction_1 = require("./redaction");
|
|
11
12
|
const storage_1 = require("./storage");
|
|
12
13
|
// The Temper name wins while legacy fallbacks keep existing workflow images compatible.
|
|
13
14
|
const workflowEnvironment = process.env.TEMPER_ENVIRONMENT ??
|
|
@@ -25,15 +26,7 @@ const isStringAnnotation = (value) => {
|
|
|
25
26
|
return false;
|
|
26
27
|
}
|
|
27
28
|
};
|
|
28
|
-
const
|
|
29
|
-
// Prevent infinite loops from circular references
|
|
30
|
-
if (visited.has(log))
|
|
31
|
-
return log;
|
|
32
|
-
visited.add(log);
|
|
33
|
-
// We can't encode frozen objects.
|
|
34
|
-
if (Object.isFrozen(log))
|
|
35
|
-
return log;
|
|
36
|
-
// We don't use Object.entries() for performance reasons
|
|
29
|
+
const copyLogFields = (log, copy, copies, preserveErrorSerializer) => {
|
|
37
30
|
for (const key in log) {
|
|
38
31
|
if (!Object.hasOwn(log, key))
|
|
39
32
|
continue;
|
|
@@ -42,23 +35,65 @@ const formatLog = (log, visited = new WeakSet()) => {
|
|
|
42
35
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
43
36
|
continue;
|
|
44
37
|
const value = log[key];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// We can't encode immutable properties.
|
|
49
|
-
if (!Object.getOwnPropertyDescriptor(log, key)?.writable)
|
|
50
|
-
continue;
|
|
51
|
-
if (isStringAnnotation(value)) {
|
|
52
|
-
log[key] = (0, encode_1.encode)(value);
|
|
53
|
-
}
|
|
54
|
-
else if (Array.isArray(value)) {
|
|
55
|
-
log[key] = value.map((item) => isStringAnnotation(item) ? (0, encode_1.encode)(item) : isBindings(item) ? formatLog(item, visited) : item);
|
|
56
|
-
}
|
|
57
|
-
else if (isBindings(value)) {
|
|
58
|
-
log[key] = formatLog(value, visited);
|
|
38
|
+
if (value !== undefined) {
|
|
39
|
+
copy[key] =
|
|
40
|
+
preserveErrorSerializer && key === 'err' && value instanceof Error ? value : copyLogValue(value, copies);
|
|
59
41
|
}
|
|
60
42
|
}
|
|
61
|
-
return
|
|
43
|
+
return copy;
|
|
44
|
+
};
|
|
45
|
+
const copyLogValue = (value, copies) => {
|
|
46
|
+
if (isStringAnnotation(value))
|
|
47
|
+
return (0, encode_1.encode)(value);
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
const existing = copies.get(value);
|
|
50
|
+
if (Array.isArray(existing))
|
|
51
|
+
return existing;
|
|
52
|
+
const copy = [];
|
|
53
|
+
copies.set(value, copy);
|
|
54
|
+
for (const item of value)
|
|
55
|
+
copy.push(copyLogValue(item, copies));
|
|
56
|
+
return copy;
|
|
57
|
+
}
|
|
58
|
+
if (value instanceof Error) {
|
|
59
|
+
const serialized = pino_1.default.stdSerializers.err(value);
|
|
60
|
+
const copy = {};
|
|
61
|
+
copies.set(value, copy);
|
|
62
|
+
copies.set(serialized, copy);
|
|
63
|
+
return copyLogFields(serialized, copy, copies, false);
|
|
64
|
+
}
|
|
65
|
+
if (isBindings(value) && Object.prototype.toString.call(value) === '[object Object]') {
|
|
66
|
+
return copyBindings(value, copies, false);
|
|
67
|
+
}
|
|
68
|
+
// Class instances such as AxiosHeaders serialize through toJSON, so the copy walks that output.
|
|
69
|
+
if (isBindings(value) && value.toJSON instanceof Function) {
|
|
70
|
+
const existing = copies.get(value);
|
|
71
|
+
if (existing !== undefined)
|
|
72
|
+
return existing;
|
|
73
|
+
const copy = copyLogValue(value.toJSON(), copies);
|
|
74
|
+
copies.set(value, copy);
|
|
75
|
+
return copy;
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
};
|
|
79
|
+
const copyBindings = (log, copies, preserveErrorSerializer) => {
|
|
80
|
+
// Pino hands serializers whatever the caller passed; a primitive is not a WeakMap key.
|
|
81
|
+
if (!isBindings(log))
|
|
82
|
+
return log;
|
|
83
|
+
const existing = copies.get(log);
|
|
84
|
+
if (isBindings(existing))
|
|
85
|
+
return existing;
|
|
86
|
+
const copy = {};
|
|
87
|
+
copies.set(log, copy);
|
|
88
|
+
return copyLogFields(log, copy, copies, preserveErrorSerializer);
|
|
89
|
+
};
|
|
90
|
+
const formatLog = (log) => copyBindings(log, new WeakMap(), true);
|
|
91
|
+
// `err` may be anything thrown; pino returns non-Error values unchanged, so only objects are copied.
|
|
92
|
+
const formatError = (error) => {
|
|
93
|
+
const serialized = pino_1.default.stdSerializers.err(error);
|
|
94
|
+
if (isStringAnnotation(serialized))
|
|
95
|
+
return (0, encode_1.encode)(serialized);
|
|
96
|
+
return isBindings(serialized) ? formatLog(serialized) : serialized;
|
|
62
97
|
};
|
|
63
98
|
const loggerOptions = {
|
|
64
99
|
level: process.env.LOG_LEVEL ?? 'debug',
|
|
@@ -73,14 +108,14 @@ const loggerOptions = {
|
|
|
73
108
|
serializers: {
|
|
74
109
|
req: pino_1.default.stdSerializers.req,
|
|
75
110
|
res: pino_1.default.stdSerializers.res,
|
|
76
|
-
err:
|
|
111
|
+
err: formatError,
|
|
77
112
|
message: encode_1.encode,
|
|
78
113
|
},
|
|
79
114
|
// Let CloudWatch handle timestamps
|
|
80
115
|
timestamp: false,
|
|
81
116
|
// Change message field from 'msg' to 'message'
|
|
82
117
|
messageKey: 'message',
|
|
83
|
-
formatters: { level: formatLevel, log: formatLog },
|
|
118
|
+
formatters: { bindings: formatLog, level: formatLevel, log: formatLog },
|
|
84
119
|
hooks: {
|
|
85
120
|
logMethod(args, method) {
|
|
86
121
|
const firstArg = args[0];
|
|
@@ -98,6 +133,8 @@ const loggerOptions = {
|
|
|
98
133
|
}
|
|
99
134
|
activeMethod.apply(this, args);
|
|
100
135
|
},
|
|
136
|
+
// Masks registered after a value was serialized (child bindings, formatter output) still reach the line.
|
|
137
|
+
streamWrite: redaction_1.redactLogLine,
|
|
101
138
|
},
|
|
102
139
|
redact: {
|
|
103
140
|
paths: [
|
|
@@ -105,12 +142,26 @@ const loggerOptions = {
|
|
|
105
142
|
'http.response.headers.authorization',
|
|
106
143
|
'http.request.headers.Authorization',
|
|
107
144
|
'http.response.headers.Authorization',
|
|
145
|
+
// The EDI client presents its gateway credential in this header, so it joins
|
|
146
|
+
// the Authorization paths: request evidence keeps the URL/status, never the key.
|
|
147
|
+
'http.request.headers.ocp-apim-subscription-key',
|
|
148
|
+
'http.request.headers.Ocp-Apim-Subscription-Key',
|
|
108
149
|
'err.config',
|
|
109
150
|
],
|
|
110
151
|
remove: true,
|
|
111
152
|
},
|
|
112
153
|
};
|
|
113
|
-
|
|
154
|
+
// Pino children inherit the parent's own methods through Object.create, so every logger gets its own
|
|
155
|
+
// wrappers bound to itself; otherwise a child's `child`/`setBindings` would act on its parent.
|
|
156
|
+
const withRedaction = (instance, child, setBindings) => {
|
|
157
|
+
instance.child = (bindings, options) => withRedaction(child.call(instance, formatLog(bindings), options), child, setBindings);
|
|
158
|
+
instance.setBindings = (bindings) => setBindings.call(instance, formatLog(bindings));
|
|
159
|
+
return instance;
|
|
160
|
+
};
|
|
161
|
+
const createLogger = (destination) => {
|
|
162
|
+
const instance = destination ? (0, pino_1.default)(loggerOptions, destination) : (0, pino_1.default)(loggerOptions);
|
|
163
|
+
return withRedaction(instance, instance.child, instance.setBindings);
|
|
164
|
+
};
|
|
114
165
|
exports.createLogger = createLogger;
|
|
115
166
|
exports.logger = (0, exports.createLogger)();
|
|
116
167
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRedactionMasks = registerRedactionMasks;
|
|
4
|
+
exports.applyRedactionMasks = applyRedactionMasks;
|
|
5
|
+
exports.redactLogLine = redactLogLine;
|
|
6
|
+
const storage_1 = require("./storage");
|
|
7
|
+
const REDACTED = '[Redacted]';
|
|
8
|
+
function registerRedactionMasks(values) {
|
|
9
|
+
const masks = (0, storage_1.getRedactionMasks)() ?? storage_1.processRedactionMasks;
|
|
10
|
+
for (const value of values) {
|
|
11
|
+
if (value.length > 0)
|
|
12
|
+
masks.add(value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const indexMasks = (masks) => {
|
|
16
|
+
const root = { edge: '', end: false, children: new Map() };
|
|
17
|
+
for (const mask of masks) {
|
|
18
|
+
let node = root;
|
|
19
|
+
let rest = mask;
|
|
20
|
+
while (rest.length > 0) {
|
|
21
|
+
const child = node.children.get(rest.charCodeAt(0));
|
|
22
|
+
if (!child) {
|
|
23
|
+
node.children.set(rest.charCodeAt(0), { edge: rest, end: true, children: new Map() });
|
|
24
|
+
rest = '';
|
|
25
|
+
node = root;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
let common = 1;
|
|
29
|
+
while (common < child.edge.length && child.edge.charCodeAt(common) === rest.charCodeAt(common))
|
|
30
|
+
common += 1;
|
|
31
|
+
if (common < child.edge.length) {
|
|
32
|
+
const tail = { edge: child.edge.slice(common), end: child.end, children: child.children };
|
|
33
|
+
child.edge = child.edge.slice(0, common);
|
|
34
|
+
child.end = false;
|
|
35
|
+
child.children = new Map([[tail.edge.charCodeAt(0), tail]]);
|
|
36
|
+
}
|
|
37
|
+
node = child;
|
|
38
|
+
rest = rest.slice(common);
|
|
39
|
+
}
|
|
40
|
+
if (node !== root)
|
|
41
|
+
node.end = true;
|
|
42
|
+
}
|
|
43
|
+
return root;
|
|
44
|
+
};
|
|
45
|
+
const jsonEscaped = (value) => JSON.stringify(value).slice(1, -1);
|
|
46
|
+
// A written line holds each secret JSON-escaped, and `encode` may have turned LF into CR before serialization.
|
|
47
|
+
const lineForms = (mask) => [jsonEscaped(mask), jsonEscaped(mask.replaceAll('\n', '\r'))];
|
|
48
|
+
const indexes = new WeakMap();
|
|
49
|
+
const indexOf = (masks) => {
|
|
50
|
+
const cached = indexes.get(masks);
|
|
51
|
+
if (cached?.version === masks.version)
|
|
52
|
+
return cached;
|
|
53
|
+
const line = indexMasks(new Set([...masks].flatMap(lineForms)));
|
|
54
|
+
const built = { version: masks.version, plain: indexMasks(masks), line };
|
|
55
|
+
indexes.set(masks, built);
|
|
56
|
+
return built;
|
|
57
|
+
};
|
|
58
|
+
const indexesInReach = (mode) => {
|
|
59
|
+
const scope = (0, storage_1.getRedactionMasks)();
|
|
60
|
+
const all = scope ? [storage_1.processRedactionMasks, scope] : [storage_1.processRedactionMasks];
|
|
61
|
+
return all.filter((masks) => masks.size > 0).map((masks) => indexOf(masks)[mode]);
|
|
62
|
+
};
|
|
63
|
+
const matchLength = (reach, value, cursor) => {
|
|
64
|
+
let longest = 0;
|
|
65
|
+
for (const root of reach) {
|
|
66
|
+
let position = cursor;
|
|
67
|
+
let node = root.children.get(value.charCodeAt(position));
|
|
68
|
+
while (node && value.startsWith(node.edge, position)) {
|
|
69
|
+
position += node.edge.length;
|
|
70
|
+
if (node.end && position - cursor > longest)
|
|
71
|
+
longest = position - cursor;
|
|
72
|
+
node = node.children.get(value.charCodeAt(position));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return longest;
|
|
76
|
+
};
|
|
77
|
+
// In a JSON line only string values are replaced, so property names, numbers, and delimiters keep the line
|
|
78
|
+
// parseable, and the scan steps over whole escape sequences so a replacement never starts inside one.
|
|
79
|
+
const replaceMasks = (value, reach, jsonLine) => {
|
|
80
|
+
if (reach.length === 0)
|
|
81
|
+
return value;
|
|
82
|
+
let redacted = '';
|
|
83
|
+
let copied = 0;
|
|
84
|
+
let cursor = 0;
|
|
85
|
+
let inString = !jsonLine;
|
|
86
|
+
let masking = !jsonLine;
|
|
87
|
+
const containers = [];
|
|
88
|
+
let expectKey = false;
|
|
89
|
+
while (cursor < value.length) {
|
|
90
|
+
const unit = value[cursor];
|
|
91
|
+
if (jsonLine && !inString) {
|
|
92
|
+
if (unit === '"') {
|
|
93
|
+
inString = true;
|
|
94
|
+
masking = !expectKey;
|
|
95
|
+
}
|
|
96
|
+
else if (unit === '{' || unit === '[') {
|
|
97
|
+
containers.push(unit);
|
|
98
|
+
expectKey = unit === '{';
|
|
99
|
+
}
|
|
100
|
+
else if (unit === '}' || unit === ']') {
|
|
101
|
+
containers.pop();
|
|
102
|
+
}
|
|
103
|
+
else if (unit === ',') {
|
|
104
|
+
expectKey = containers.at(-1) === '{';
|
|
105
|
+
}
|
|
106
|
+
else if (unit === ':') {
|
|
107
|
+
expectKey = false;
|
|
108
|
+
}
|
|
109
|
+
cursor += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (jsonLine && unit === '"') {
|
|
113
|
+
inString = false;
|
|
114
|
+
cursor += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const length = masking ? matchLength(reach, value, cursor) : 0;
|
|
118
|
+
if (length > 0) {
|
|
119
|
+
redacted += value.slice(copied, cursor) + REDACTED;
|
|
120
|
+
cursor += length;
|
|
121
|
+
copied = cursor;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
cursor += jsonLine && unit === '\\' ? (value[cursor + 1] === 'u' ? 6 : 2) : 1;
|
|
125
|
+
}
|
|
126
|
+
return copied === 0 ? value : redacted + value.slice(copied);
|
|
127
|
+
};
|
|
128
|
+
function applyRedactionMasks(value) {
|
|
129
|
+
return replaceMasks(value, indexesInReach('plain'), false);
|
|
130
|
+
}
|
|
131
|
+
// Masks the finished JSON line, so a mask registered after a child froze its bindings, or a value a serializer or
|
|
132
|
+
// formatter produced, is still caught.
|
|
133
|
+
function redactLogLine(line) {
|
|
134
|
+
return replaceMasks(line, indexesInReach('line'), true);
|
|
135
|
+
}
|
package/dist/span.js
CHANGED
|
@@ -13,8 +13,7 @@ const startSpan = async (spanName, callback, getter) => {
|
|
|
13
13
|
const spanId = (0, uuid_1.v4)();
|
|
14
14
|
const start = performance.now();
|
|
15
15
|
const previous = storage_1.storage.getStore();
|
|
16
|
-
return storage_1.
|
|
17
|
-
...previous,
|
|
16
|
+
return (0, storage_1.runWithStorage)({
|
|
18
17
|
traceId: previous?.traceId ?? (0, trace_1.getTraceId)(),
|
|
19
18
|
spanId: previous?.spanId ? `${spanId} ${previous?.spanId}` : spanId,
|
|
20
19
|
}, async () => {
|
package/dist/storage.d.ts
CHANGED
|
@@ -4,9 +4,25 @@ export interface UnnboundContext {
|
|
|
4
4
|
spanId?: string;
|
|
5
5
|
messageId?: string;
|
|
6
6
|
}
|
|
7
|
+
/** Process-wide masks are bounded by count: the least recently registered secret is evicted first. */
|
|
8
|
+
export declare const PROCESS_REDACTION_MASK_LIMIT = 4096;
|
|
9
|
+
export declare class MaskSet extends Set<string> {
|
|
10
|
+
version: number;
|
|
11
|
+
add(mask: string): this;
|
|
12
|
+
delete(mask: string): boolean;
|
|
13
|
+
clear(): void;
|
|
14
|
+
}
|
|
15
|
+
declare class RecentMaskSet extends MaskSet {
|
|
16
|
+
private readonly limit;
|
|
17
|
+
constructor(limit: number);
|
|
18
|
+
add(mask: string): this;
|
|
19
|
+
}
|
|
20
|
+
export declare const processRedactionMasks: RecentMaskSet;
|
|
7
21
|
interface UnnboundStorage extends AsyncLocalStorage<object> {
|
|
8
22
|
getStore<T>(): T | undefined;
|
|
9
23
|
}
|
|
10
24
|
export declare const storage: UnnboundStorage;
|
|
25
|
+
export declare const getRedactionMasks: () => MaskSet | undefined;
|
|
26
|
+
export declare const runWithStorage: <T, Data extends object>(data: Data, callback: () => T) => T;
|
|
11
27
|
export declare const withStorage: <T, Data extends object>(data: Data, callback: () => Promise<T>) => Promise<T>;
|
|
12
28
|
export {};
|
package/dist/storage.js
CHANGED
|
@@ -1,12 +1,100 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.withStorage = exports.storage = void 0;
|
|
3
|
+
exports.withStorage = exports.runWithStorage = exports.getRedactionMasks = exports.storage = exports.processRedactionMasks = exports.MaskSet = exports.PROCESS_REDACTION_MASK_LIMIT = void 0;
|
|
4
4
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
+
const redactionMasksKey = Symbol('redactionMasks');
|
|
6
|
+
/** Process-wide masks are bounded by count: the least recently registered secret is evicted first. */
|
|
7
|
+
exports.PROCESS_REDACTION_MASK_LIMIT = 4096;
|
|
8
|
+
// `version` changes only when membership does, so redaction rebuilds its mask index for a new or removed mask,
|
|
9
|
+
// not when a re-registration moves an existing one to the newest position.
|
|
10
|
+
class MaskSet extends Set {
|
|
11
|
+
version = 0;
|
|
12
|
+
add(mask) {
|
|
13
|
+
const present = Set.prototype.delete.call(this, mask);
|
|
14
|
+
Set.prototype.add.call(this, mask);
|
|
15
|
+
if (!present)
|
|
16
|
+
this.version += 1;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
delete(mask) {
|
|
20
|
+
const removed = super.delete(mask);
|
|
21
|
+
if (removed)
|
|
22
|
+
this.version += 1;
|
|
23
|
+
return removed;
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
this.version += 1;
|
|
27
|
+
super.clear();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.MaskSet = MaskSet;
|
|
31
|
+
// Every resolver read re-registers its secrets, so one still being resolved stays resident while cold ones age out.
|
|
32
|
+
class RecentMaskSet extends MaskSet {
|
|
33
|
+
limit;
|
|
34
|
+
constructor(limit) {
|
|
35
|
+
super();
|
|
36
|
+
this.limit = limit;
|
|
37
|
+
}
|
|
38
|
+
add(mask) {
|
|
39
|
+
super.add(mask);
|
|
40
|
+
if (this.size > this.limit) {
|
|
41
|
+
const oldest = this.values().next();
|
|
42
|
+
if (!oldest.done)
|
|
43
|
+
this.delete(oldest.value);
|
|
44
|
+
}
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.processRedactionMasks = new RecentMaskSet(exports.PROCESS_REDACTION_MASK_LIMIT);
|
|
49
|
+
// A root scope retains its masks once; detached work (a timer, an unawaited promise) that registers after that
|
|
50
|
+
// point forwards to the process set for logs outside the scope, and keeps its own copy, which the cap cannot evict.
|
|
51
|
+
class ScopeMasks extends MaskSet {
|
|
52
|
+
retained = false;
|
|
53
|
+
// Re-registration moves the mask to the end, so retention hands the process set the scope's masks by recency.
|
|
54
|
+
// A mask the process set already holds is refreshed there too: another trace may log it while this scope is open.
|
|
55
|
+
add(mask) {
|
|
56
|
+
if (this.retained || exports.processRedactionMasks.has(mask))
|
|
57
|
+
exports.processRedactionMasks.add(mask);
|
|
58
|
+
super.add(mask);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
5
62
|
exports.storage =
|
|
6
63
|
// SAFETY: UnnboundStorage only adds a typed getStore helper over AsyncLocalStorage
|
|
7
64
|
new node_async_hooks_1.AsyncLocalStorage();
|
|
8
|
-
const
|
|
65
|
+
const getRedactionMasks = () => exports.storage.getStore()?.[redactionMasksKey];
|
|
66
|
+
exports.getRedactionMasks = getRedactionMasks;
|
|
67
|
+
const retainMasks = (masks) => {
|
|
68
|
+
masks.retained = true;
|
|
69
|
+
for (const mask of masks)
|
|
70
|
+
exports.processRedactionMasks.add(mask);
|
|
71
|
+
};
|
|
72
|
+
// Nested scopes share the root's mask set; a root scope's secrets are returned and logged after it
|
|
73
|
+
// exits, so its masks move to the process set on completion.
|
|
74
|
+
const runWithStorage = (data, callback) => {
|
|
9
75
|
const previous = exports.storage.getStore();
|
|
10
|
-
|
|
76
|
+
const inherited = previous?.[redactionMasksKey];
|
|
77
|
+
const masks = inherited ?? new ScopeMasks();
|
|
78
|
+
const context = { ...previous, ...data, [redactionMasksKey]: masks };
|
|
79
|
+
if (inherited)
|
|
80
|
+
return exports.storage.run(context, callback);
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = exports.storage.run(context, callback);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
retainMasks(masks);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
if (result instanceof Promise) {
|
|
90
|
+
result.finally(() => retainMasks(masks)).catch(() => undefined);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
retainMasks(masks);
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
exports.runWithStorage = runWithStorage;
|
|
97
|
+
const withStorage = async (data, callback) => {
|
|
98
|
+
return (0, exports.runWithStorage)(data, callback);
|
|
11
99
|
};
|
|
12
100
|
exports.withStorage = withStorage;
|
package/dist/trace.js
CHANGED
|
@@ -13,6 +13,6 @@ const withTrace = (callback, extra) => {
|
|
|
13
13
|
const previous = storage_1.storage.getStore();
|
|
14
14
|
const traceId = extra?.traceId ?? previous?.traceId ?? (0, exports.getTraceId)();
|
|
15
15
|
const messageId = extra?.messageId ?? previous?.messageId ?? (0, exports.getMessageId)();
|
|
16
|
-
return storage_1.
|
|
16
|
+
return (0, storage_1.runWithStorage)({ ...extra, traceId, messageId }, callback);
|
|
17
17
|
};
|
|
18
18
|
exports.withTrace = withTrace;
|
package/dist/types.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface SftpPayload {
|
|
|
51
51
|
files?: string[];
|
|
52
52
|
content?: string;
|
|
53
53
|
exists?: string | false;
|
|
54
|
+
mode?: number | string;
|
|
54
55
|
}
|
|
55
56
|
export type EdiX12Operation = 'fromX12' | 'toX12' | 'validateX12' | 'acknowledgeX12';
|
|
56
57
|
export type EdiOperation = EdiX12Operation;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
/** patterns are globs (`*`, `?`) matched against the whole path. */
|
|
1
|
+
/** patterns are globs (`*`, `?`) matched against the whole path; any other metacharacter is literal. */
|
|
2
2
|
export declare const shouldIgnorePath: (path: string, patterns: string[]) => boolean;
|
|
3
|
+
type ParsedJson = string | number | boolean | null | ParsedJson[] | {
|
|
4
|
+
[key: string]: ParsedJson;
|
|
5
|
+
};
|
|
3
6
|
/** Non-string or invalid JSON → input returned unchanged. */
|
|
4
|
-
export declare function safeJsonParse(data:
|
|
7
|
+
export declare function safeJsonParse<Body>(data: Body): Body | ParsedJson;
|
|
5
8
|
/** Strips the IPv4-mapped IPv6 prefix (::ffff:). */
|
|
6
9
|
export declare function normalizeIp(ip: string | undefined): string | undefined;
|
|
10
|
+
export {};
|
package/dist/utils.js
CHANGED
|
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.shouldIgnorePath = void 0;
|
|
4
4
|
exports.safeJsonParse = safeJsonParse;
|
|
5
5
|
exports.normalizeIp = normalizeIp;
|
|
6
|
-
/** patterns are globs (`*`, `?`) matched against the whole path. */
|
|
6
|
+
/** patterns are globs (`*`, `?`) matched against the whole path; any other metacharacter is literal. */
|
|
7
7
|
const shouldIgnorePath = (path, patterns) => {
|
|
8
8
|
return patterns.some((pattern) => {
|
|
9
|
-
const regexPattern = pattern.replace(
|
|
9
|
+
const regexPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, (character) => character === '*' ? '.*' : character === '?' ? '.' : `\\${character}`);
|
|
10
10
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
11
11
|
return regex.test(path);
|
|
12
12
|
});
|
|
@@ -14,7 +14,7 @@ const shouldIgnorePath = (path, patterns) => {
|
|
|
14
14
|
exports.shouldIgnorePath = shouldIgnorePath;
|
|
15
15
|
/** Non-string or invalid JSON → input returned unchanged. */
|
|
16
16
|
function safeJsonParse(data) {
|
|
17
|
-
if (
|
|
17
|
+
if (typeof data === 'string') {
|
|
18
18
|
try {
|
|
19
19
|
return JSON.parse(data);
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unnbound-logger-sdk",
|
|
3
3
|
"description": "A structured logging library with TypeScript support using Pino. Provides consistent, well-typed logging with automatic logId, workflowId, traceId, and deploymentId tracking across operational contexts.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"test": "vitest run src",
|
|
10
|
-
"typecheck": "tsc --noEmit",
|
|
11
|
-
"format": "oxfmt --write .",
|
|
12
|
-
"format:check": "oxfmt --check .",
|
|
13
|
-
"prepublishOnly": "npm run build",
|
|
14
|
-
"start:example": "tsx watch examples/node-express.ts",
|
|
15
|
-
"version:bump": "npm version patch"
|
|
16
|
-
},
|
|
17
7
|
"keywords": [
|
|
18
8
|
"logging",
|
|
19
9
|
"structured-logging",
|
|
@@ -66,5 +56,14 @@
|
|
|
66
56
|
"engines": {
|
|
67
57
|
"node": ">=22"
|
|
68
58
|
},
|
|
69
|
-
"sideEffects": false
|
|
70
|
-
|
|
59
|
+
"sideEffects": false,
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsc",
|
|
62
|
+
"test": "vitest run src",
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"format": "oxfmt --write .",
|
|
65
|
+
"format:check": "oxfmt --check .",
|
|
66
|
+
"start:example": "tsx watch examples/node-express.ts",
|
|
67
|
+
"version:bump": "npm version patch"
|
|
68
|
+
}
|
|
69
|
+
}
|