@logtrace/shared 0.1.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 +30 -0
- package/src/constants.js +32 -0
- package/src/index.js +3 -0
- package/src/masker.js +114 -0
- package/src/utils.js +55 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@logtrace/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Utilidades compartidas del ecosistema LogTrace (constantes, máscara de datos sensibles, helpers).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./constants": "./src/constants.js",
|
|
10
|
+
"./utils": "./src/utils.js",
|
|
11
|
+
"./masker": "./src/masker.js"
|
|
12
|
+
},
|
|
13
|
+
"files": ["src"],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.0.0"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/logtrace/tracker-cli.git",
|
|
20
|
+
"directory": "packages/shared"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://logtrace.cloud",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test test/**/*.test.js"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @logtrace/shared — constants.js
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for paths, versions, and config shared
|
|
5
|
+
* between daemon and CLI.
|
|
6
|
+
*
|
|
7
|
+
* ENV overrides let dev run without root or /opt access.
|
|
8
|
+
* Dev: LOGTRACE_DIR=./tmp/logtrace SOCKET_PATH=./tmp/logtrace/daemon.sock
|
|
9
|
+
* Prod: defaults below (/opt/logtrace, /run/logtrace/daemon.sock)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const LOGTRACE_DIR = process.env.LOGTRACE_DIR || '/opt/logtrace'
|
|
13
|
+
export const SOCKET_PATH = process.env.SOCKET_PATH || '/run/logtrace/daemon.sock'
|
|
14
|
+
|
|
15
|
+
export const VERSION = '0.1.0'
|
|
16
|
+
export const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'critical']
|
|
17
|
+
|
|
18
|
+
/** True when running in development mode — relaxes license checks, verbose logs */
|
|
19
|
+
export const IS_DEV = process.env.LOGTRACE_ENV === 'development'
|
|
20
|
+
|
|
21
|
+
/** Directories created by daemon on startup */
|
|
22
|
+
export const DAEMON_DIRS = [
|
|
23
|
+
`${LOGTRACE_DIR}/data`,
|
|
24
|
+
`${LOGTRACE_DIR}/backups`,
|
|
25
|
+
`${LOGTRACE_DIR}/logs`,
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** SQLite database path */
|
|
29
|
+
export const DB_PATH = `${LOGTRACE_DIR}/data/logtrace.db`
|
|
30
|
+
|
|
31
|
+
/** License cache path */
|
|
32
|
+
export const LICENSE_CACHE_PATH = `${LOGTRACE_DIR}/.license-cache.json`
|
package/src/index.js
ADDED
package/src/masker.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @logtrace/shared — masker.js
|
|
3
|
+
*
|
|
4
|
+
* maskLog(obj) strips sensitive data from a log object BEFORE it is stored
|
|
5
|
+
* in SQLite or forwarded to the central server.
|
|
6
|
+
*
|
|
7
|
+
* Patterns applied (in order):
|
|
8
|
+
* EMAIL — user@example.com → [EMAIL]
|
|
9
|
+
* CARD — 16-digit Luhn numbers → [CARD]
|
|
10
|
+
* JWT — eyJh...eyJh...sig → [JWT]
|
|
11
|
+
* AWS_KEY — AKIA[A-Z0-9]{16} → [AWS_KEY]
|
|
12
|
+
* PASSWORD — JSON field "password":"…" → "password":"[REDACTED]"
|
|
13
|
+
* SECRET — JSON field "secret":"…" → "secret":"[REDACTED]"
|
|
14
|
+
* TOKEN — JSON field "token":"…" → "token":"[REDACTED]"
|
|
15
|
+
*
|
|
16
|
+
* Only string values inside the object are modified.
|
|
17
|
+
* Non-string values (numbers, booleans, null) are left as-is.
|
|
18
|
+
* Nested objects are processed recursively.
|
|
19
|
+
*
|
|
20
|
+
* Custom patterns can be added at runtime via addPattern().
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @type {Array<{regex: RegExp, replace: string}>} */
|
|
24
|
+
const PATTERNS = [
|
|
25
|
+
// Email addresses
|
|
26
|
+
{
|
|
27
|
+
regex: /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g,
|
|
28
|
+
replace: '[EMAIL]',
|
|
29
|
+
},
|
|
30
|
+
// Credit/debit card numbers (Luhn-structured 16-digit, with optional separators)
|
|
31
|
+
{
|
|
32
|
+
regex: /\b(?:\d[ \-]?){13,15}\d\b/g,
|
|
33
|
+
replace: '[CARD]',
|
|
34
|
+
},
|
|
35
|
+
// JSON Web Tokens (3-part base64url)
|
|
36
|
+
{
|
|
37
|
+
regex: /eyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/g,
|
|
38
|
+
replace: '[JWT]',
|
|
39
|
+
},
|
|
40
|
+
// AWS access key IDs
|
|
41
|
+
{
|
|
42
|
+
regex: /AKIA[0-9A-Z]{16}/g,
|
|
43
|
+
replace: '[AWS_KEY]',
|
|
44
|
+
},
|
|
45
|
+
// JSON field: "password":"<value>" (double or single quotes, any depth)
|
|
46
|
+
{
|
|
47
|
+
regex: /"password"\s*:\s*"[^"]*"/gi,
|
|
48
|
+
replace: '"password":"[REDACTED]"',
|
|
49
|
+
},
|
|
50
|
+
// JSON field: "secret":"<value>"
|
|
51
|
+
{
|
|
52
|
+
regex: /"secret"\s*:\s*"[^"]*"/gi,
|
|
53
|
+
replace: '"secret":"[REDACTED]"',
|
|
54
|
+
},
|
|
55
|
+
// JSON field: "token":"<value>" (catches api_token, access_token, etc.)
|
|
56
|
+
{
|
|
57
|
+
regex: /"[a-z_]*token[a-z_]*"\s*:\s*"[^"]*"/gi,
|
|
58
|
+
replace: (match) => match.replace(/"[^"]*"$/, '"[REDACTED]"'),
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add a custom masking pattern at runtime.
|
|
64
|
+
* @param {RegExp} regex
|
|
65
|
+
* @param {string|Function} replace
|
|
66
|
+
*/
|
|
67
|
+
export function addPattern(regex, replace) {
|
|
68
|
+
PATTERNS.push({ regex, replace })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Apply all masking patterns to a single string value.
|
|
73
|
+
* @param {string} str
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function maskString(str) {
|
|
77
|
+
let result = str
|
|
78
|
+
for (const { regex, replace } of PATTERNS) {
|
|
79
|
+
// Reset lastIndex for global regexes to avoid stale state
|
|
80
|
+
if (regex.global) regex.lastIndex = 0
|
|
81
|
+
result = result.replace(regex, replace)
|
|
82
|
+
}
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Recursively mask all string values in an object or array.
|
|
88
|
+
* Returns a new object — the original is never mutated.
|
|
89
|
+
*
|
|
90
|
+
* @param {any} obj
|
|
91
|
+
* @returns {any}
|
|
92
|
+
*/
|
|
93
|
+
export function maskLog(obj) {
|
|
94
|
+
if (obj === null || obj === undefined) return obj
|
|
95
|
+
|
|
96
|
+
if (typeof obj === 'string') {
|
|
97
|
+
return maskString(obj)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(obj)) {
|
|
101
|
+
return obj.map(maskLog)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof obj === 'object') {
|
|
105
|
+
const result = {}
|
|
106
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
107
|
+
result[key] = maskLog(value)
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// number, boolean, bigint, symbol — return as-is
|
|
113
|
+
return obj
|
|
114
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @logtrace/shared — utils.js
|
|
3
|
+
*
|
|
4
|
+
* Utility functions shared between daemon and CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* fingerprint(log) — deterministic hash for deduplication.
|
|
11
|
+
*
|
|
12
|
+
* Two log entries with the same level + service + message produce the same
|
|
13
|
+
* fingerprint, regardless of timestamp or meta. This lets the alert system
|
|
14
|
+
* deduplicate bursts without a DB query per log.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} log - { level, service, message }
|
|
17
|
+
* @returns {string} 16-char hex fingerprint
|
|
18
|
+
*/
|
|
19
|
+
export function fingerprint(log) {
|
|
20
|
+
const key = [
|
|
21
|
+
(log.level || '').toLowerCase(),
|
|
22
|
+
(log.service || '').toLowerCase(),
|
|
23
|
+
(log.message || '').slice(0, 200), // cap at 200 chars — ignore trailing noise
|
|
24
|
+
].join('|')
|
|
25
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 16)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* formatDate(ts) — ISO timestamp → human-readable local string.
|
|
30
|
+
*
|
|
31
|
+
* @param {number|string|Date} ts
|
|
32
|
+
* @returns {string} e.g. "2026-06-09 14:32:01"
|
|
33
|
+
*/
|
|
34
|
+
export function formatDate(ts) {
|
|
35
|
+
const d = ts instanceof Date ? ts : new Date(ts)
|
|
36
|
+
if (isNaN(d.getTime())) return 'invalid date'
|
|
37
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
38
|
+
return (
|
|
39
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
40
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* truncate(str, n) — truncate a string to n chars, appending "…" if cut.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} str
|
|
48
|
+
* @param {number} n - maximum length (including the ellipsis character)
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function truncate(str, n) {
|
|
52
|
+
if (typeof str !== 'string') return String(str)
|
|
53
|
+
if (str.length <= n) return str
|
|
54
|
+
return str.slice(0, n - 1) + '…'
|
|
55
|
+
}
|