@wdyy/skills 0.1.22 → 0.1.24

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.
@@ -1,179 +1,455 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
3
4
  import { tmpdir } from 'node:os';
4
5
  import { join } from 'node:path';
5
6
  import { afterEach, test } from 'node:test';
6
- import { spawnSync } from 'node:child_process';
7
7
  import {
8
+ BusinessFunctionCatalog,
8
9
  createLogFileName,
9
10
  createLogFilePath,
11
+ createTraceContext,
10
12
  LOG_DIRECTORY,
11
13
  MAX_LOG_FILE_SIZE_BYTES,
14
+ MIN_NETWORK_LOG_RETENTION_MONTHS,
15
+ requireSourceIp,
16
+ resolveActorContext,
17
+ sanitizeLogValue,
18
+ SecureLogWriter,
19
+ toLogEntry,
12
20
  } from '../templates/logger.template.ts';
21
+ import {
22
+ createTraceContext as createFrontendTraceContext,
23
+ sanitizeFrontendErrorReport,
24
+ } from '../templates/frontend-error-report.template.ts';
25
+
26
+ process.env.TZ = 'Asia/Shanghai';
13
27
 
14
28
  const validator = new URL('./validate-log-entry.mjs', import.meta.url);
29
+ const loggerTemplate = new URL('../templates/logger.template.ts', import.meta.url);
30
+ const skillRoot = new URL('..', import.meta.url);
15
31
  const temporaryDirectories = [];
32
+ const fixedDate = new Date('2026-07-09T08:00:00+08:00');
16
33
 
17
34
  afterEach(async () => {
18
35
  await Promise.all(
19
- temporaryDirectories.splice(0).map((directory) =>
20
- rm(directory, { force: true, recursive: true }),
21
- ),
36
+ temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })),
22
37
  );
23
38
  });
24
39
 
25
- async function validate(entry) {
26
- const directory = await mkdtemp(join(tmpdir(), 'log-entry-'));
40
+ async function createTemporaryDirectory() {
41
+ const directory = await mkdtemp(join(tmpdir(), 'secure-log-'));
27
42
  temporaryDirectories.push(directory);
28
- const input = join(directory, 'entry.json');
29
- await writeFile(input, JSON.stringify(entry));
30
- return spawnSync(process.execPath, [validator.pathname, input], { encoding: 'utf8' });
43
+ return directory;
31
44
  }
32
45
 
33
- test('同一秒日志文件使用递增后缀', () => {
34
- const date = new Date(2026, 6, 28, 9, 5, 3);
35
-
36
- assert.deepEqual(
37
- [createLogFileName(date), createLogFileName(date, 1), createLogFileName(date, 2)],
38
- ['2026-07-28_09-05-03.log', '2026-07-28_09-05-03_1.log', '2026-07-28_09-05-03_2.log'],
46
+ async function validate(entry, stored = false) {
47
+ const directory = await createTemporaryDirectory();
48
+ const input = join(directory, 'entry.log');
49
+ await writeFile(input, stored ? entry : JSON.stringify(entry));
50
+ return spawnSync(
51
+ process.execPath,
52
+ [...(stored ? ['--experimental-strip-types'] : []), validator.pathname, ...(stored ? ['--stored'] : []), input],
53
+ { encoding: 'utf8', env: { ...process.env, TZ: 'Asia/Shanghai' } },
39
54
  );
40
- });
55
+ }
41
56
 
42
- test('日志路径固定在项目根目录的扁平 logs 目录', () => {
43
- const date = new Date(2026, 6, 28, 9, 5, 3);
57
+ function validAccessEntry(overrides = {}) {
58
+ return {
59
+ timestamp: '2026-07-09 08:00:00',
60
+ timestampEpochMs: fixedDate.getTime(),
61
+ timezone: '+08:00',
62
+ level: 'info',
63
+ service: 'backend',
64
+ instanceId: 'blue',
65
+ env: 'production',
66
+ logType: 'access',
67
+ message: 'request completed',
68
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
69
+ method: 'GET',
70
+ route: '/patients/:patientId',
71
+ sourceIp: '10.0.0.8',
72
+ actorId: 'user-1',
73
+ actorType: 'employee',
74
+ functionCode: 'patient.query',
75
+ functionName: '患者信息查询',
76
+ action: 'read',
77
+ statusCode: 200,
78
+ result: 'success',
79
+ durationMs: 12.5,
80
+ ...overrides,
81
+ };
82
+ }
83
+
84
+ function validSecurityEntry(overrides = {}) {
85
+ return {
86
+ timestamp: '2026-07-09 08:00:00',
87
+ timestampEpochMs: fixedDate.getTime(),
88
+ timezone: '+08:00',
89
+ level: 'warn',
90
+ service: 'backend',
91
+ instanceId: 'blue',
92
+ env: 'production',
93
+ logType: 'security',
94
+ message: 'access denied',
95
+ eventId: 'event-1',
96
+ eventType: 'access.denied',
97
+ actorId: 'user-1',
98
+ actorType: 'employee',
99
+ action: 'patient.read',
100
+ targetType: 'patient',
101
+ targetId: 'patient-1',
102
+ result: 'failure',
103
+ reason: 'permission denied',
104
+ ...overrides,
105
+ };
106
+ }
107
+
108
+ function runWriter(directory, body) {
109
+ const source = `
110
+ import { SecureLogWriter, toLogEntry } from ${JSON.stringify(loggerTemplate.href)};
111
+ process.env.TZ = 'Asia/Shanghai';
112
+ const fixedDate = new Date('2026-07-09T08:00:00+08:00');
113
+ const writer = new SecureLogWriter(() => fixedDate);
114
+ ${body}
115
+ await writer.close();
116
+ `;
117
+ return spawnSync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', source], {
118
+ cwd: directory,
119
+ encoding: 'utf8',
120
+ env: { ...process.env, TZ: 'Asia/Shanghai' },
121
+ });
122
+ }
44
123
 
124
+ test('固定根日志路径、2MB 阈值和六个月留存合同', () => {
45
125
  assert.equal(LOG_DIRECTORY, './logs');
126
+ assert.equal(MAX_LOG_FILE_SIZE_BYTES, 2 * 1024 * 1024);
127
+ assert.equal(MIN_NETWORK_LOG_RETENTION_MONTHS, 6);
128
+ assert.equal(createLogFileName(fixedDate), '2026-07-09_08-00-00.log');
129
+ assert.equal(createLogFilePath(fixedDate, 2), './logs/2026-07-09_08-00-00_2.log');
130
+ });
131
+
132
+ test('完整业务结构保留但秘密和敏感个人信息被递归删除', () => {
133
+ const sanitized = sanitizeLogValue({
134
+ orderId: 'order-1',
135
+ nested: {
136
+ password: 'not-allowed',
137
+ Authorization: 'Bearer not-allowed',
138
+ idCard: '110101199001011234',
139
+ medicalRecord: { diagnosis: 'not-allowed' },
140
+ items: [{ sku: 'A', token: 'not-allowed' }],
141
+ },
142
+ });
143
+
144
+ assert.deepEqual(sanitized, {
145
+ orderId: 'order-1',
146
+ nested: { items: [{ sku: 'A' }] },
147
+ });
148
+ });
149
+
150
+ test('项目确认的附加敏感字段也会被删除', () => {
46
151
  assert.deepEqual(
47
- [createLogFilePath(date), createLogFilePath(date, 1), createLogFilePath(date, 2)],
48
- [
49
- './logs/2026-07-28_09-05-03.log',
50
- './logs/2026-07-28_09-05-03_1.log',
51
- './logs/2026-07-28_09-05-03_2.log',
52
- ],
152
+ sanitizeLogValue({ orderId: '1', hospitalSecretField: 'remove-me' }, ['hospitalSecretField']),
153
+ { orderId: '1' },
53
154
  );
54
- assert.ok(
55
- [createLogFilePath(date), createLogFilePath(date, 1), createLogFilePath(date, 2)]
56
- .every((path) => path.split('/').length === 3),
155
+ });
156
+
157
+ test('访问日志强制关键字段并映射 HTTP 结果', () => {
158
+ const trace = createTraceContext();
159
+ const entry = toLogEntry('info', 'request completed', {
160
+ logType: 'access',
161
+ traceId: trace.traceId,
162
+ spanId: trace.spanId,
163
+ method: 'POST',
164
+ route: '/inventory/:inventoryId',
165
+ sourceIp: '10.0.0.8',
166
+ actorId: 'user-1',
167
+ actorType: 'employee',
168
+ functionCode: 'inventory.update',
169
+ functionName: '库存更新',
170
+ action: 'update',
171
+ statusCode: 422,
172
+ durationMs: 7,
173
+ }, fixedDate);
174
+
175
+ assert.equal(entry.result, 'failure');
176
+ assert.equal(entry.timezone, '+08:00');
177
+ assert.throws(
178
+ () => toLogEntry('info', 'incomplete', { logType: 'access' }, fixedDate),
179
+ /Missing access log fields/,
57
180
  );
58
181
  });
59
182
 
60
- test('旧日志目录环境变量不能改变固定路径', () => {
61
- const previousLogDir = process.env.LOG_DIR;
62
- const previousHostLogDir = process.env.HOST_LOG_DIR;
63
- process.env.LOG_DIR = '/tmp/legacy-logs';
64
- process.env.HOST_LOG_DIR = '/srv/legacy-logs';
183
+ test('显式功能目录按 method 与路由解析且拒绝缺失或重复映射', () => {
184
+ const catalog = new BusinessFunctionCatalog([{
185
+ method: 'GET',
186
+ route: '/patients/:patientId',
187
+ functionCode: 'patient.query',
188
+ functionName: '患者信息查询',
189
+ action: 'read',
190
+ }]);
191
+ assert.deepEqual(catalog.resolve('get', '/patients/:patientId'), {
192
+ functionCode: 'patient.query',
193
+ functionName: '患者信息查询',
194
+ action: 'read',
195
+ });
196
+ assert.throws(() => catalog.resolve('POST', '/patients/:patientId'), /Missing business function mapping/);
197
+ assert.throws(() => new BusinessFunctionCatalog([
198
+ {
199
+ method: 'GET', route: '/patients', functionCode: 'patient.list', functionName: '患者列表', action: 'read',
200
+ },
201
+ {
202
+ method: 'get', route: '/patients', functionCode: 'patient.query', functionName: '患者查询', action: 'read',
203
+ },
204
+ ]), /Duplicate business function mapping/);
205
+ });
65
206
 
66
- try {
67
- assert.equal(
68
- createLogFilePath(new Date(2026, 6, 28, 9, 5, 3)),
69
- './logs/2026-07-28_09-05-03.log',
70
- );
71
- } finally {
72
- if (previousLogDir === undefined) delete process.env.LOG_DIR;
73
- else process.env.LOG_DIR = previousLogDir;
74
- if (previousHostLogDir === undefined) delete process.env.HOST_LOG_DIR;
75
- else process.env.HOST_LOG_DIR = previousHostLogDir;
207
+ test('认证和匿名主体语义明确且不接受不完整身份', () => {
208
+ assert.deepEqual(resolveActorContext(), { actorId: 'anonymous', actorType: 'anonymous' });
209
+ assert.deepEqual(resolveActorContext({ id: 'user-1', type: 'employee' }), {
210
+ actorId: 'user-1', actorType: 'employee',
211
+ });
212
+ assert.throws(() => resolveActorContext({ id: 'user-1' }), /both id and type/);
213
+ });
214
+
215
+ test('访问日志拒绝无效 IP、未知功能和缺少主体', async () => {
216
+ for (const [overrides, expected] of [
217
+ [{ sourceIp: 'unknown' }, /valid IPv4 or IPv6/],
218
+ [{ functionCode: undefined }, /Missing access log fields.*functionCode/],
219
+ [{ functionCode: 'Patient Query' }, /stable lowercase business identifier/],
220
+ [{ functionCode: 'unknown' }, /stable lowercase business identifier/],
221
+ [{ functionName: '待确认' }, /resolved non-empty business name/],
222
+ [{ actorId: undefined }, /Missing access log fields.*actorId/],
223
+ ]) {
224
+ const result = await validate(validAccessEntry(overrides));
225
+ assert.notEqual(result.status, 0);
226
+ assert.match(result.stderr, expected);
76
227
  }
228
+ assert.equal(requireSourceIp('2001:db8::1'), '2001:db8::1');
229
+ assert.throws(() => requireSourceIp('unknown'), /valid IPv4 or IPv6/);
77
230
  });
78
231
 
79
- test('2MB 轮转阈值使用同一 logs 目录的下一个候选路径', () => {
80
- const date = new Date(2026, 6, 28, 9, 5, 3);
232
+ test('访问日志拒绝原始查询 URL 和无效 traceId', async () => {
233
+ const rawRoute = await validate(validAccessEntry({ route: '/patients/1?token=secret' }));
234
+ assert.notEqual(rawRoute.status, 0);
235
+ assert.match(rawRoute.stderr, /route template/);
81
236
 
82
- assert.equal(MAX_LOG_FILE_SIZE_BYTES, 2 * 1024 * 1024);
83
- assert.equal(createLogFilePath(date, 3), './logs/2026-07-28_09-05-03_3.log');
237
+ const invalidTrace = await validate(validAccessEntry({ traceId: '00000000000000000000000000000000' }));
238
+ assert.notEqual(invalidTrace.status, 0);
239
+ assert.match(invalidTrace.stderr, /traceId/);
84
240
  });
85
241
 
86
- test('多进程日志必须包含 instanceId', async () => {
87
- const result = await validate({
88
- timestamp: '2026-07-09 08:00:00',
89
- level: 'info',
90
- service: 'backend',
91
- instanceId: 'blue',
92
- env: 'production',
93
- message: 'request completed',
94
- });
242
+ test('有效 traceparent 延续 trace,无效值生成新上下文', () => {
243
+ const incoming = '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01';
244
+ const continued = createTraceContext(incoming);
245
+ assert.equal(continued.traceId, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
246
+ assert.equal(continued.parentSpanId, 'bbbbbbbbbbbbbbbb');
247
+ assert.match(continued.traceparent, /^00-a{32}-[0-9a-f]{16}-01$/);
95
248
 
96
- assert.equal(result.status, 0, result.stderr);
249
+ const replaced = createTraceContext('00-00000000000000000000000000000000-0000000000000000-01');
250
+ assert.match(replaced.traceparent, /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/);
251
+ assert.notEqual(replaced.traceId, '00000000000000000000000000000000');
97
252
  });
98
253
 
99
- test('缺少 instanceId 时明确失败', async () => {
100
- const result = await validate({
101
- timestamp: '2026-07-09 08:00:00',
102
- level: 'info',
103
- service: 'backend',
104
- env: 'production',
105
- message: 'request completed',
106
- });
254
+ test('安全与审计失败事件必须包含原因', () => {
255
+ assert.doesNotThrow(() => toLogEntry('warn', 'denied', {
256
+ logType: 'security',
257
+ eventId: 'event-1',
258
+ eventType: 'access.denied',
259
+ actorId: 'user-1',
260
+ actorType: 'employee',
261
+ action: 'patient.read',
262
+ targetType: 'patient',
263
+ targetId: 'patient-1',
264
+ result: 'failure',
265
+ reason: 'permission denied',
266
+ }, fixedDate));
267
+ assert.throws(
268
+ () => toLogEntry('warn', 'denied', {
269
+ logType: 'security',
270
+ eventId: 'event-1',
271
+ eventType: 'access.denied',
272
+ actorId: 'user-1',
273
+ actorType: 'employee',
274
+ action: 'patient.read',
275
+ targetType: 'patient',
276
+ targetId: 'patient-1',
277
+ result: 'failure',
278
+ }, fixedDate),
279
+ /require reason/,
280
+ );
281
+ const auditEntry = toLogEntry('info', 'permission changed', {
282
+ logType: 'audit',
283
+ eventId: 'event-2',
284
+ eventType: 'permission.changed',
285
+ actorId: 'admin-1',
286
+ actorType: 'employee',
287
+ action: 'permission.update',
288
+ targetType: 'role',
289
+ targetId: 'role-1',
290
+ result: 'success',
291
+ before: { permission: 'read' },
292
+ after: { permission: 'write', token: 'not-allowed' },
293
+ }, fixedDate);
294
+ assert.deepEqual(auditEntry.after, { permission: 'write' });
295
+ });
296
+
297
+ test('请求触发的安全审计日志要求完整功能和来源上下文', () => {
298
+ assert.doesNotThrow(() => toLogEntry('warn', 'denied', {
299
+ ...validSecurityEntry(),
300
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
301
+ sourceIp: '10.0.0.8',
302
+ route: '/patients/:patientId',
303
+ functionCode: 'patient.query',
304
+ functionName: '患者信息查询',
305
+ }, fixedDate));
306
+ assert.throws(() => toLogEntry('warn', 'denied', {
307
+ ...validSecurityEntry(),
308
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
309
+ sourceIp: '10.0.0.8',
310
+ }, fixedDate), /require traceId, sourceIp, route, functionCode and functionName/);
311
+ });
107
312
 
313
+ test('校验器拒绝绕过净化的秘密字段', async () => {
314
+ const result = await validate(validAccessEntry({ body: { password: 'not-allowed' } }));
108
315
  assert.notEqual(result.status, 0);
109
- assert.match(result.stderr, /instanceId/);
316
+ assert.match(result.stderr, /Forbidden log field.*password/);
110
317
  });
111
318
 
112
- test('HTTP 状态码与 result 不一致时明确失败', async () => {
113
- const result = await validate({
114
- timestamp: '2026-07-09 08:00:00',
115
- level: 'info',
116
- service: 'backend',
117
- instanceId: 'blue',
118
- env: 'production',
119
- message: 'request completed',
120
- statusCode: 201,
121
- result: 'failure',
122
- });
319
+ test('日志消息换行被编码而不是注入新日志行', () => {
320
+ const entry = toLogEntry('info', 'first\nsecond\rthird token=secret-value', {
321
+ logType: 'application',
322
+ params: { note: 'Bearer abc.def', orderId: 'order-1' },
323
+ }, fixedDate);
324
+ assert.equal(entry.message, 'first\\nsecond\\rthird token=[REMOVED]');
325
+ assert.deepEqual(entry.params, { note: 'Bearer [REMOVED]', orderId: 'order-1' });
326
+ assert.equal(JSON.stringify(entry).split('\n').length, 1);
327
+ });
123
328
 
124
- assert.notEqual(result.status, 0);
125
- assert.match(result.stderr, /result/);
329
+ test('前端 trace 在缺少 Web Crypto 时仍有效且不会阻断请求', () => {
330
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'crypto');
331
+ Object.defineProperty(globalThis, 'crypto', { configurable: true, value: undefined });
332
+ try {
333
+ const trace = createFrontendTraceContext();
334
+ assert.match(trace.traceparent, /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/);
335
+ } finally {
336
+ if (descriptor) Object.defineProperty(globalThis, 'crypto', descriptor);
337
+ else delete globalThis.crypto;
338
+ }
126
339
  });
127
340
 
128
- test('完整原始入参和成功响应可通过验证', async () => {
129
- const result = await validate({
130
- timestamp: '2026-07-09 08:00:00',
131
- level: 'info',
132
- service: 'backend',
133
- instanceId: 'blue',
134
- env: 'production',
135
- message: 'request completed',
136
- query: { page: '1' },
137
- body: {
138
- password: 'unredacted',
139
- idCard: '110101199001011234',
140
- permissions: Array.from({ length: 501 }, (_, index) => index),
141
- },
142
- response: { code: 'OK', message: '处理成功' },
143
- statusCode: 201,
144
- result: 'success',
341
+ test('前端异常上报移除查询串和常见凭据', () => {
342
+ const report = sanitizeFrontendErrorReport({
343
+ errorName: 'FetchError',
344
+ message: 'authorization: Bearer abc.def password=hunter2',
345
+ stack: 'token=secret-value',
346
+ path: '/login?token=secret#fragment',
347
+ occurredAt: '2026-07-09T00:00:00.000Z',
145
348
  });
349
+ assert.equal(report.path, '/login');
350
+ assert.doesNotMatch(JSON.stringify(report), /hunter2|secret-value|abc\.def/);
351
+ });
146
352
 
353
+ test('真实写入使用最小权限、单行 JSON 和可验证哈希链', async () => {
354
+ const directory = await createTemporaryDirectory();
355
+ const result = runWriter(directory, `
356
+ await writer.write(toLogEntry('info', 'one', { logType: 'application' }, fixedDate));
357
+ await writer.write(toLogEntry('info', 'two', { logType: 'application' }, fixedDate));
358
+ `);
147
359
  assert.equal(result.status, 0, result.stderr);
360
+
361
+ const logsDirectory = join(directory, 'logs');
362
+ const files = await readdir(logsDirectory);
363
+ assert.deepEqual(files, ['2026-07-09_08-00-00.log']);
364
+ assert.equal((await stat(logsDirectory)).mode & 0o777, 0o700);
365
+ const logPath = join(logsDirectory, files[0]);
366
+ assert.equal((await stat(logPath)).mode & 0o777, 0o600);
367
+ const source = await readFile(logPath, 'utf8');
368
+ assert.equal(source.trim().split('\n').length, 2);
369
+ const validation = await validate(source, true);
370
+ assert.equal(validation.status, 0, validation.stderr);
148
371
  });
149
372
 
150
- test('失败响应内容和 failure 结果可通过验证', async () => {
151
- const result = await validate({
152
- timestamp: '2026-07-09 08:00:00',
153
- level: 'info',
154
- service: 'backend',
155
- instanceId: 'blue',
156
- env: 'production',
157
- message: 'request failed',
158
- body: { data: 'original-request' },
159
- response: { code: 'INVALID_DATA', message: '参数不合法' },
160
- statusCode: 422,
161
- result: 'failure',
162
- });
373
+ test('多进程同秒排他创建且不覆盖既有文件', async () => {
374
+ const directory = await createTemporaryDirectory();
375
+ const body = "await writer.write(toLogEntry('info', 'entry', { logType: 'application' }, fixedDate));";
376
+ const first = runWriter(directory, body);
377
+ const second = runWriter(directory, body);
378
+ assert.equal(first.status, 0, first.stderr);
379
+ assert.equal(second.status, 0, second.stderr);
380
+ assert.deepEqual((await readdir(join(directory, 'logs'))).sort(), [
381
+ '2026-07-09_08-00-00.log',
382
+ '2026-07-09_08-00-00_1.log',
383
+ ]);
384
+ });
163
385
 
386
+ test('超过 2MB 时创建新文件并保留既有文件', async () => {
387
+ const directory = await createTemporaryDirectory();
388
+ const result = runWriter(directory, `
389
+ const payload = 'x'.repeat(1_200_000);
390
+ await writer.write(toLogEntry('info', 'first', { logType: 'application', body: { payload } }, fixedDate));
391
+ await writer.write(toLogEntry('info', 'second', { logType: 'application', body: { payload } }, fixedDate));
392
+ `);
164
393
  assert.equal(result.status, 0, result.stderr);
394
+ assert.deepEqual((await readdir(join(directory, 'logs'))).sort(), [
395
+ '2026-07-09_08-00-00.log',
396
+ '2026-07-09_08-00-00_1.log',
397
+ ]);
165
398
  });
166
399
 
167
- test('UTC ISO timestamp 时明确失败', async () => {
168
- const result = await validate({
169
- timestamp: '2026-07-09T00:00:00.000Z',
170
- level: 'info',
171
- service: 'backend',
172
- instanceId: 'blue',
173
- env: 'production',
174
- message: 'request completed',
400
+ test('跨秒轮转使用新的基础文件名而非沿用冲突后缀', async () => {
401
+ const directory = await createTemporaryDirectory();
402
+ const source = `
403
+ import { SecureLogWriter, toLogEntry } from ${JSON.stringify(loggerTemplate.href)};
404
+ process.env.TZ = 'Asia/Shanghai';
405
+ const firstDate = new Date('2026-07-09T08:00:00+08:00');
406
+ const secondDate = new Date('2026-07-09T08:00:01+08:00');
407
+ const dates = [firstDate, secondDate];
408
+ const writer = new SecureLogWriter(() => dates.shift() ?? secondDate);
409
+ const payload = 'x'.repeat(1_200_000);
410
+ await writer.write(toLogEntry('info', 'first', { logType: 'application', body: { payload } }, firstDate));
411
+ await writer.write(toLogEntry('info', 'second', { logType: 'application', body: { payload } }, secondDate));
412
+ await writer.close();
413
+ `;
414
+ const result = spawnSync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', source], {
415
+ cwd: directory,
416
+ encoding: 'utf8',
417
+ env: { ...process.env, TZ: 'Asia/Shanghai' },
175
418
  });
419
+ assert.equal(result.status, 0, result.stderr);
420
+ assert.deepEqual((await readdir(join(directory, 'logs'))).sort(), [
421
+ '2026-07-09_08-00-00.log',
422
+ '2026-07-09_08-00-01.log',
423
+ ]);
424
+ });
176
425
 
177
- assert.notEqual(result.status, 0);
178
- assert.match(result.stderr, /timestamp/);
426
+ test('篡改落盘记录后完整性验证失败', async () => {
427
+ const directory = await createTemporaryDirectory();
428
+ const result = runWriter(directory, `
429
+ await writer.write(toLogEntry('info', 'original', { logType: 'application' }, fixedDate));
430
+ `);
431
+ assert.equal(result.status, 0, result.stderr);
432
+ const logPath = join(directory, 'logs', '2026-07-09_08-00-00.log');
433
+ const entry = JSON.parse((await readFile(logPath, 'utf8')).trim());
434
+ entry.message = 'tampered';
435
+ const validation = await validate(`${JSON.stringify(entry)}\n`, true);
436
+ assert.notEqual(validation.status, 0);
437
+ assert.match(validation.stderr, /integrity verification/);
438
+ });
439
+
440
+ test('HTTP 与安全审计模板包含真实事件入口且不记录原始 URL', async () => {
441
+ const middleware = await readFile(new URL('templates/nestjs-http-logging.middleware.template.ts', skillRoot), 'utf8');
442
+ const securityLogger = await readFile(new URL('templates/security-audit-logger.template.ts', skillRoot), 'utf8');
443
+ assert.match(middleware, /response\.once\('finish'/);
444
+ assert.match(middleware, /traceparent/);
445
+ assert.match(middleware, /functionCatalog\.resolve/);
446
+ assert.match(middleware, /resolveActorContext/);
447
+ assert.match(middleware, /requireSourceIp/);
448
+ assert.doesNotMatch(middleware, /originalUrl/);
449
+ assert.doesNotMatch(middleware, /['"]unknown['"]/);
450
+ assert.match(securityLogger, /functionCode/);
451
+ assert.match(securityLogger, /functionName/);
452
+ for (const eventName of ['auth.login.failure', 'access.denied', 'permission.changed', 'sensitive-data.export']) {
453
+ assert.match(securityLogger, new RegExp(eventName.replaceAll('.', '\\.')));
454
+ }
179
455
  });
@@ -1,4 +1,11 @@
1
- type FrontendErrorReport = {
1
+ export type FrontendTraceContext = {
2
+ traceId: string;
3
+ spanId: string;
4
+ traceparent: string;
5
+ };
6
+
7
+ export type FrontendErrorReport = {
8
+ errorName: string;
2
9
  message: string;
3
10
  stack?: string;
4
11
  path: string;
@@ -6,22 +13,45 @@ type FrontendErrorReport = {
6
13
  occurredAt: string;
7
14
  };
8
15
 
9
- /**
10
- * HTTP 请求生成追踪标识。HTTP 非安全上下文可能不提供 crypto.randomUUID,
11
- * 因此追踪标识必须有非加密用途的回退实现,不能阻断请求。
12
- */
13
- export function createTraceId(): string {
14
- const cryptoApi = globalThis.crypto;
15
- if (typeof cryptoApi?.randomUUID === 'function') {
16
- return cryptoApi.randomUUID();
16
+ const randomHex = (byteLength: number): string => {
17
+ const bytes = new Uint8Array(byteLength);
18
+ if (typeof globalThis.crypto?.getRandomValues === 'function') {
19
+ globalThis.crypto.getRandomValues(bytes);
20
+ } else {
21
+ for (let index = 0; index < bytes.length; index += 1) {
22
+ bytes[index] = Math.floor(Math.random() * 256);
23
+ }
17
24
  }
25
+ if (bytes.every((value) => value === 0)) bytes[bytes.length - 1] = 1;
26
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('');
27
+ };
28
+
29
+ export function createTraceContext(): FrontendTraceContext {
30
+ const traceId = randomHex(16);
31
+ const spanId = randomHex(8);
32
+ return { traceId, spanId, traceparent: `00-${traceId}-${spanId}-01` };
33
+ }
34
+
35
+ const sanitizeErrorText = (value: string): string => value
36
+ .replaceAll(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REMOVED]')
37
+ .replaceAll(/\b(password|passwd|pwd|token|secret|api[_-]?key)\s*[:=]\s*[^\s,;]+/gi, '$1=[REMOVED]')
38
+ .replaceAll(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[REMOVED_JWT]');
18
39
 
19
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
40
+ export function sanitizeFrontendErrorReport(report: FrontendErrorReport): FrontendErrorReport {
41
+ return {
42
+ ...report,
43
+ errorName: sanitizeErrorText(report.errorName),
44
+ message: sanitizeErrorText(report.message),
45
+ stack: report.stack ? sanitizeErrorText(report.stack) : undefined,
46
+ path: report.path.split(/[?#]/, 1)[0],
47
+ };
20
48
  }
21
49
 
22
50
  // Axios 请求拦截器示例:
23
51
  // api.interceptors.request.use((config) => {
24
- // config.headers.set('x-trace-id', createTraceId());
52
+ // const trace = createTraceContext();
53
+ // config.headers.set('traceparent', trace.traceparent);
54
+ // config.headers.set('x-trace-id', trace.traceId);
25
55
  // return config;
26
56
  // });
27
57
 
@@ -30,7 +60,7 @@ export async function reportFrontendError(report: FrontendErrorReport): Promise<
30
60
  method: 'POST',
31
61
  headers: { 'Content-Type': 'application/json' },
32
62
  credentials: 'same-origin',
33
- body: JSON.stringify(report),
63
+ body: JSON.stringify(sanitizeFrontendErrorReport(report)),
34
64
  });
35
65
  if (!response.ok) throw new Error(`Frontend error report failed: ${response.status}`);
36
66
  }