@huaqiu/dsh-plugin-log 0.3.11
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/LICENSE +21 -0
- package/lib/client.d.ts +26 -0
- package/lib/client.js +186 -0
- package/lib/index.d.mts +107 -0
- package/lib/index.mjs +456 -0
- package/package.json +36 -0
- package/src/client.ts +94 -0
- package/src/index.ts +445 -0
- package/src/levels.ts +39 -0
- package/src/redact.ts +95 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 深圳华秋智联股份有限公司
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/levels.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Log levels for `@huaqiu/dsh-plugin-log`.
|
|
4
|
+
*
|
|
5
|
+
* Four levels only — enough to separate "noise while debugging" from "someone
|
|
6
|
+
* must look at this", and few enough that a reader of the log file can filter
|
|
7
|
+
* with a single grep.
|
|
8
|
+
*/
|
|
9
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/client.d.ts
|
|
12
|
+
type LogFields = Record<string, unknown>;
|
|
13
|
+
interface PluginLogger {
|
|
14
|
+
readonly component: string;
|
|
15
|
+
debug(message: string, fields?: LogFields): void;
|
|
16
|
+
info(message: string, fields?: LogFields): void;
|
|
17
|
+
warn(message: string, fields?: LogFields): void;
|
|
18
|
+
error(message: string, fields?: LogFields): void;
|
|
19
|
+
child(fields: LogFields): PluginLogger;
|
|
20
|
+
}
|
|
21
|
+
/** Get (or create) the browser logger for `component`. */
|
|
22
|
+
declare function getLogger(component: string, defaults?: LogFields): PluginLogger;
|
|
23
|
+
/** Snapshot of the in-memory ring — handy from the devtools console. */
|
|
24
|
+
declare function dumpPluginLogs(): Array<Record<string, unknown>>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { LogFields, type LogLevel, PluginLogger, dumpPluginLogs, getLogger };
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//#region src/levels.ts
|
|
2
|
+
/** Levels ordered from most to least verbose. */
|
|
3
|
+
const LOG_LEVELS = [
|
|
4
|
+
"debug",
|
|
5
|
+
"info",
|
|
6
|
+
"warn",
|
|
7
|
+
"error"
|
|
8
|
+
];
|
|
9
|
+
const RANK = {
|
|
10
|
+
debug: 10,
|
|
11
|
+
info: 20,
|
|
12
|
+
warn: 30,
|
|
13
|
+
error: 40
|
|
14
|
+
};
|
|
15
|
+
/** Numeric rank of a level — higher means more severe. */
|
|
16
|
+
function levelRank(level) {
|
|
17
|
+
return RANK[level];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse a level from an arbitrary (env-supplied) value.
|
|
21
|
+
*
|
|
22
|
+
* Unparseable or empty input falls back instead of throwing: a bad
|
|
23
|
+
* `DSH_PLUGIN_LOG_LEVEL` in a customer environment must degrade to "log
|
|
24
|
+
* normally", never to "crash the plugin host".
|
|
25
|
+
*/
|
|
26
|
+
function parseLevel(value, fallback) {
|
|
27
|
+
if (typeof value !== "string") return fallback;
|
|
28
|
+
const normalized = value.trim().toLowerCase();
|
|
29
|
+
return LOG_LEVELS.includes(normalized) ? normalized : fallback;
|
|
30
|
+
}
|
|
31
|
+
/** True when `level` is at least as severe as `threshold`. */
|
|
32
|
+
function isEnabled(level, threshold) {
|
|
33
|
+
return levelRank(level) >= levelRank(threshold);
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/redact.ts
|
|
37
|
+
/**
|
|
38
|
+
* Credential redaction for `@huaqiu/dsh-plugin-log`.
|
|
39
|
+
*
|
|
40
|
+
* The whole point of a shared plugin log is that it is safe to hand to someone
|
|
41
|
+
* else when debugging — which means it must never become a second, unmanaged
|
|
42
|
+
* copy of the user's credential. Everything written through this logger goes
|
|
43
|
+
* through `redact()` first, so a caller cannot leak a token by accident.
|
|
44
|
+
*
|
|
45
|
+
* Two rules:
|
|
46
|
+
*
|
|
47
|
+
* 1. **Key-based** — any field whose name looks credential-ish
|
|
48
|
+
* (`token`, `authorization`, `password`, `cookie`, `apiKey`, …) has its
|
|
49
|
+
* value replaced, at any nesting depth.
|
|
50
|
+
* 2. **Shape-based** — string values that look like a bearer header or a
|
|
51
|
+
* long opaque secret are replaced even when the key is innocent
|
|
52
|
+
* (`headers: ['Authorization: Bearer ey…']`).
|
|
53
|
+
*
|
|
54
|
+
* Redaction is deliberately key-name based rather than "redact every long
|
|
55
|
+
* string": log readability matters, and most long strings (project paths,
|
|
56
|
+
* URLs, artifact ids) carry no secret.
|
|
57
|
+
*/
|
|
58
|
+
/** Field names whose values are always replaced. Matches at any depth. */
|
|
59
|
+
const SENSITIVE_KEY = /(token|secret|password|passwd|pwd|authorization|cookie|api[-_]?key|access[-_]?key|credential)/i;
|
|
60
|
+
/** `Bearer <opaque>` / `Basic <opaque>` inside a free-form string. */
|
|
61
|
+
const BEARER_IN_STRING = /\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/i;
|
|
62
|
+
/** Strings at least this long that look like a single opaque credential blob. */
|
|
63
|
+
const OPAQUE_SECRET = /^[A-Za-z0-9_-]{32,}$/;
|
|
64
|
+
const REDACTED = "[redacted]";
|
|
65
|
+
/** Maximum object depth walked before the value is collapsed. Cycle-safe. */
|
|
66
|
+
const MAX_DEPTH = 6;
|
|
67
|
+
/**
|
|
68
|
+
* Return a copy of `value` with credential-ish fields replaced.
|
|
69
|
+
*
|
|
70
|
+
* Never throws and never mutates the caller's object — a logging call must not
|
|
71
|
+
* be able to change plugin state or crash the host.
|
|
72
|
+
*/
|
|
73
|
+
function redact(value, depth = 0) {
|
|
74
|
+
try {
|
|
75
|
+
return redactInner(value, depth, /* @__PURE__ */ new WeakSet());
|
|
76
|
+
} catch {
|
|
77
|
+
return REDACTED;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function redactInner(value, depth, seen) {
|
|
81
|
+
if (value === null || value === void 0) return value;
|
|
82
|
+
if (typeof value === "string") return redactString(value);
|
|
83
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
|
|
84
|
+
if (typeof value === "function") return "[function]";
|
|
85
|
+
if (typeof value === "symbol") return value.toString();
|
|
86
|
+
if (value instanceof Error) return {
|
|
87
|
+
name: value.name,
|
|
88
|
+
message: redactString(value.message),
|
|
89
|
+
...value.stack ? { stack: value.stack } : {}
|
|
90
|
+
};
|
|
91
|
+
if (value instanceof Date) return value.toISOString();
|
|
92
|
+
if (depth >= MAX_DEPTH) return "[deep]";
|
|
93
|
+
if (seen.has(value)) return "[circular]";
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
seen.add(value);
|
|
96
|
+
const out = value.map((item) => redactInner(item, depth + 1, seen));
|
|
97
|
+
seen.delete(value);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === "object") {
|
|
101
|
+
seen.add(value);
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const [key, raw] of Object.entries(value)) out[key] = SENSITIVE_KEY.test(key) ? REDACTED : redactInner(raw, depth + 1, seen);
|
|
104
|
+
seen.delete(value);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
return String(value);
|
|
108
|
+
}
|
|
109
|
+
/** Redact credentials embedded in an otherwise ordinary string. */
|
|
110
|
+
function redactString(value) {
|
|
111
|
+
if (BEARER_IN_STRING.test(value)) return value.replace(BEARER_IN_STRING, "$1 " + REDACTED);
|
|
112
|
+
if (OPAQUE_SECRET.test(value)) return REDACTED;
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/client.ts
|
|
117
|
+
/**
|
|
118
|
+
* `@huaqiu/dsh-plugin-log/client` — browser half of the shared plugin log.
|
|
119
|
+
*
|
|
120
|
+
* The browser has no filesystem, so there is nothing to unify: this module
|
|
121
|
+
* keeps the *same* `PluginLogger` surface as the node half (so a plugin can log
|
|
122
|
+
* identically from either half) but writes to the console with a stable
|
|
123
|
+
* `[component] message` prefix and a structured payload as the second
|
|
124
|
+
* argument — which is what the existing client-side debugging already relies on.
|
|
125
|
+
*
|
|
126
|
+
* It also keeps a small in-memory ring of the most recent records. That costs
|
|
127
|
+
* nothing and gives a user something to copy out of the devtools console
|
|
128
|
+
* (`dumpPluginLogs()`) when a file is not available.
|
|
129
|
+
*
|
|
130
|
+
* Redaction is shared with the node half, so a credential can never reach the
|
|
131
|
+
* browser console through this logger either.
|
|
132
|
+
*/
|
|
133
|
+
/** Most recent records kept in memory for `dumpPluginLogs()`. */
|
|
134
|
+
const RING_SIZE = 200;
|
|
135
|
+
const ring = [];
|
|
136
|
+
const DEFAULT_LEVEL = "debug";
|
|
137
|
+
function currentLevel() {
|
|
138
|
+
return parseLevel(globalThis.DSH_PLUGIN_LOG_LEVEL, DEFAULT_LEVEL);
|
|
139
|
+
}
|
|
140
|
+
function consoleMethod(level) {
|
|
141
|
+
if (level === "error") return "error";
|
|
142
|
+
if (level === "warn") return "warn";
|
|
143
|
+
if (level === "info") return "info";
|
|
144
|
+
return "log";
|
|
145
|
+
}
|
|
146
|
+
function createLogger(component, defaults) {
|
|
147
|
+
const emit = (level, message, fields) => {
|
|
148
|
+
try {
|
|
149
|
+
if (!isEnabled(level, currentLevel())) return;
|
|
150
|
+
const record = {
|
|
151
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
152
|
+
level,
|
|
153
|
+
component,
|
|
154
|
+
msg: message
|
|
155
|
+
};
|
|
156
|
+
if (Object.keys(defaults).length > 0) Object.assign(record, defaults);
|
|
157
|
+
if (fields && Object.keys(fields).length > 0) Object.assign(record, fields);
|
|
158
|
+
const safe = redact(record);
|
|
159
|
+
ring.push(safe);
|
|
160
|
+
if (ring.length > RING_SIZE) ring.shift();
|
|
161
|
+
const { msg, ...rest } = safe;
|
|
162
|
+
console[consoleMethod(level)](`[${component}] ${String(msg)}`, rest);
|
|
163
|
+
} catch {}
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
component,
|
|
167
|
+
debug: (message, fields) => emit("debug", message, fields),
|
|
168
|
+
info: (message, fields) => emit("info", message, fields),
|
|
169
|
+
warn: (message, fields) => emit("warn", message, fields),
|
|
170
|
+
error: (message, fields) => emit("error", message, fields),
|
|
171
|
+
child: (fields) => createLogger(component, {
|
|
172
|
+
...defaults,
|
|
173
|
+
...fields
|
|
174
|
+
})
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Get (or create) the browser logger for `component`. */
|
|
178
|
+
function getLogger(component, defaults = {}) {
|
|
179
|
+
return createLogger(component, defaults);
|
|
180
|
+
}
|
|
181
|
+
/** Snapshot of the in-memory ring — handy from the devtools console. */
|
|
182
|
+
function dumpPluginLogs() {
|
|
183
|
+
return ring.slice();
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export { dumpPluginLogs, getLogger };
|
package/lib/index.d.mts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//#region src/levels.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Log levels for `@huaqiu/dsh-plugin-log`.
|
|
4
|
+
*
|
|
5
|
+
* Four levels only — enough to separate "noise while debugging" from "someone
|
|
6
|
+
* must look at this", and few enough that a reader of the log file can filter
|
|
7
|
+
* with a single grep.
|
|
8
|
+
*/
|
|
9
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
10
|
+
/** Levels ordered from most to least verbose. */
|
|
11
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
12
|
+
/** Numeric rank of a level — higher means more severe. */
|
|
13
|
+
declare function levelRank(level: LogLevel): number;
|
|
14
|
+
/**
|
|
15
|
+
* Parse a level from an arbitrary (env-supplied) value.
|
|
16
|
+
*
|
|
17
|
+
* Unparseable or empty input falls back instead of throwing: a bad
|
|
18
|
+
* `DSH_PLUGIN_LOG_LEVEL` in a customer environment must degrade to "log
|
|
19
|
+
* normally", never to "crash the plugin host".
|
|
20
|
+
*/
|
|
21
|
+
declare function parseLevel(value: unknown, fallback: LogLevel): LogLevel;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/redact.d.ts
|
|
24
|
+
/**
|
|
25
|
+
* Credential redaction for `@huaqiu/dsh-plugin-log`.
|
|
26
|
+
*
|
|
27
|
+
* The whole point of a shared plugin log is that it is safe to hand to someone
|
|
28
|
+
* else when debugging — which means it must never become a second, unmanaged
|
|
29
|
+
* copy of the user's credential. Everything written through this logger goes
|
|
30
|
+
* through `redact()` first, so a caller cannot leak a token by accident.
|
|
31
|
+
*
|
|
32
|
+
* Two rules:
|
|
33
|
+
*
|
|
34
|
+
* 1. **Key-based** — any field whose name looks credential-ish
|
|
35
|
+
* (`token`, `authorization`, `password`, `cookie`, `apiKey`, …) has its
|
|
36
|
+
* value replaced, at any nesting depth.
|
|
37
|
+
* 2. **Shape-based** — string values that look like a bearer header or a
|
|
38
|
+
* long opaque secret are replaced even when the key is innocent
|
|
39
|
+
* (`headers: ['Authorization: Bearer ey…']`).
|
|
40
|
+
*
|
|
41
|
+
* Redaction is deliberately key-name based rather than "redact every long
|
|
42
|
+
* string": log readability matters, and most long strings (project paths,
|
|
43
|
+
* URLs, artifact ids) carry no secret.
|
|
44
|
+
*/
|
|
45
|
+
declare const REDACTED = "[redacted]";
|
|
46
|
+
/**
|
|
47
|
+
* Return a copy of `value` with credential-ish fields replaced.
|
|
48
|
+
*
|
|
49
|
+
* Never throws and never mutates the caller's object — a logging call must not
|
|
50
|
+
* be able to change plugin state or crash the host.
|
|
51
|
+
*/
|
|
52
|
+
declare function redact(value: unknown, depth?: number): unknown;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/index.d.ts
|
|
55
|
+
/** Arbitrary structured context attached to a log record. */
|
|
56
|
+
type LogFields = Record<string, unknown>;
|
|
57
|
+
/** The logger surface every plugin codes against. */
|
|
58
|
+
interface PluginLogger {
|
|
59
|
+
readonly component: string;
|
|
60
|
+
debug(message: string, fields?: LogFields): void;
|
|
61
|
+
info(message: string, fields?: LogFields): void;
|
|
62
|
+
warn(message: string, fields?: LogFields): void;
|
|
63
|
+
error(message: string, fields?: LogFields): void;
|
|
64
|
+
/** Derive a logger that always carries `fields`. */
|
|
65
|
+
child(fields: LogFields): PluginLogger;
|
|
66
|
+
}
|
|
67
|
+
interface LoggingOptions {
|
|
68
|
+
/** Override the log directory (highest precedence, beats every env var). */
|
|
69
|
+
dir?: string;
|
|
70
|
+
/** Base file name inside the directory. Default `dsh-plugins.log`. */
|
|
71
|
+
fileName?: string;
|
|
72
|
+
/** Minimum file level. Default from `$DSH_PLUGIN_LOG_LEVEL`, else `info`. */
|
|
73
|
+
level?: LogLevel;
|
|
74
|
+
/**
|
|
75
|
+
* Minimum level mirrored to the console. HQ Edge captures DSH's stdout, so
|
|
76
|
+
* this is how a plugin surfaces a problem without anyone opening a file.
|
|
77
|
+
* Default from `$DSH_PLUGIN_LOG_CONSOLE`, else `warn`.
|
|
78
|
+
*/
|
|
79
|
+
consoleLevel?: LogLevel | 'off';
|
|
80
|
+
/** Rotate once the current file exceeds this many bytes. Default 5 MiB. */
|
|
81
|
+
maxBytes?: number;
|
|
82
|
+
/** Number of files kept (current + rotated). Default 4. */
|
|
83
|
+
maxFiles?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Programmatically configure logging. Call before the first `getLogger()`;
|
|
87
|
+
* later calls take effect on the next `resetLogging()` (tests, host re-init).
|
|
88
|
+
*/
|
|
89
|
+
declare function configureLogging(options: LoggingOptions): void;
|
|
90
|
+
/** Forget all configuration and cached loggers. Test/teardown helper. */
|
|
91
|
+
declare function resetLogging(): void;
|
|
92
|
+
/** Absolute path of the current log file, or `null` before first use. */
|
|
93
|
+
declare function logFilePath(): string | null;
|
|
94
|
+
/** Absolute directory plugin logs are written to. */
|
|
95
|
+
declare function logDir(): string;
|
|
96
|
+
/** Wait for all pending writes to land (shutdown hooks, tests). */
|
|
97
|
+
declare function flushLogs(): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Get (or create) the logger for `component`.
|
|
100
|
+
*
|
|
101
|
+
* `component` should be the short plugin name used everywhere else in its
|
|
102
|
+
* output — `dsh-auth`, `dsh-artifacts`, `dsh-schematic-gen` — so the unified
|
|
103
|
+
* file can be filtered with a single grep.
|
|
104
|
+
*/
|
|
105
|
+
declare function getLogger(component: string, defaults?: LogFields): PluginLogger;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { LOG_LEVELS, LogFields, type LogLevel, LoggingOptions, PluginLogger, REDACTED, configureLogging, flushLogs, getLogger, levelRank, logDir, logFilePath, parseLevel, redact, resetLogging };
|