@meshconnect/uwc-core 1.0.3 → 1.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/README.md +247 -76
- package/dist/events.d.ts +241 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +77 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/create-logger.d.ts +32 -0
- package/dist/logger/create-logger.d.ts.map +1 -0
- package/dist/logger/create-logger.js +79 -0
- package/dist/logger/create-logger.js.map +1 -0
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +5 -1
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/observability/connect-observer.d.ts +33 -0
- package/dist/observability/connect-observer.d.ts.map +1 -0
- package/dist/observability/connect-observer.js +115 -0
- package/dist/observability/connect-observer.js.map +1 -0
- package/dist/observability/focus-tracker.d.ts +42 -0
- package/dist/observability/focus-tracker.d.ts.map +1 -0
- package/dist/observability/focus-tracker.js +57 -0
- package/dist/observability/focus-tracker.js.map +1 -0
- package/dist/observability/instrument-handoff.d.ts +54 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -0
- package/dist/observability/instrument-handoff.js +173 -0
- package/dist/observability/instrument-handoff.js.map +1 -0
- package/dist/observability/lifecycle-bridge.d.ts +69 -0
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
- package/dist/observability/lifecycle-bridge.js +155 -0
- package/dist/observability/lifecycle-bridge.js.map +1 -0
- package/dist/observability/scrub.d.ts +31 -0
- package/dist/observability/scrub.d.ts.map +1 -0
- package/dist/observability/scrub.js +167 -0
- package/dist/observability/scrub.js.map +1 -0
- package/dist/observability/telemetry.d.ts +157 -0
- package/dist/observability/telemetry.d.ts.map +1 -0
- package/dist/observability/telemetry.js +373 -0
- package/dist/observability/telemetry.js.map +1 -0
- package/dist/services/connection-service.d.ts +23 -1
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +128 -19
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +2 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +45 -16
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +14 -7
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +56 -58
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +19 -5
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +55 -38
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +56 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +207 -110
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/resolve-session-provider.d.ts +10 -0
- package/dist/utils/resolve-session-provider.d.ts.map +1 -0
- package/dist/utils/resolve-session-provider.js +21 -0
- package/dist/utils/resolve-session-provider.js.map +1 -0
- package/dist/utils/to-wallet-error.d.ts +35 -0
- package/dist/utils/to-wallet-error.d.ts.map +1 -0
- package/dist/utils/to-wallet-error.js +130 -0
- package/dist/utils/to-wallet-error.js.map +1 -0
- package/dist/utils/validate-wallet-session.d.ts +15 -0
- package/dist/utils/validate-wallet-session.d.ts.map +1 -0
- package/dist/utils/validate-wallet-session.js +29 -0
- package/dist/utils/validate-wallet-session.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -6
- package/src/events.ts +326 -14
- package/src/index.ts +10 -0
- package/src/logger/create-logger.test.ts +126 -0
- package/src/logger/create-logger.ts +112 -0
- package/src/managers/event-manager.test.ts +55 -0
- package/src/managers/event-manager.ts +5 -1
- package/src/observability/connect-observer.test.ts +214 -0
- package/src/observability/connect-observer.ts +128 -0
- package/src/observability/focus-tracker.test.ts +191 -0
- package/src/observability/focus-tracker.ts +96 -0
- package/src/observability/instrument-handoff.test.ts +397 -0
- package/src/observability/instrument-handoff.ts +231 -0
- package/src/observability/lifecycle-bridge.test.ts +398 -0
- package/src/observability/lifecycle-bridge.ts +216 -0
- package/src/observability/scrub.test.ts +183 -0
- package/src/observability/scrub.ts +174 -0
- package/src/observability/telemetry.test.ts +549 -0
- package/src/observability/telemetry.ts +546 -0
- package/src/services/connection-service.test.ts +261 -1
- package/src/services/connection-service.ts +167 -38
- package/src/services/network-switch-service.test.ts +51 -1
- package/src/services/network-switch-service.ts +56 -24
- package/src/services/signature-service.test.ts +181 -76
- package/src/services/signature-service.ts +75 -85
- package/src/services/transaction-service.test.ts +270 -69
- package/src/services/transaction-service.ts +74 -51
- package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
- package/src/universal-wallet-connector.observability.test.ts +610 -0
- package/src/universal-wallet-connector.test.ts +27 -146
- package/src/universal-wallet-connector.ts +279 -142
- package/src/utils/id.test.ts +15 -0
- package/src/utils/id.ts +48 -0
- package/src/utils/resolve-session-provider.test.ts +54 -0
- package/src/utils/resolve-session-provider.ts +25 -0
- package/src/utils/to-wallet-error.test.ts +181 -0
- package/src/utils/to-wallet-error.ts +152 -0
- package/src/utils/validate-wallet-session.test.ts +77 -0
- package/src/utils/validate-wallet-session.ts +40 -0
- package/src/version.test.ts +11 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { scrubValue, scrubMessage, scrubArgs } from './scrub'
|
|
3
|
+
|
|
4
|
+
describe('scrub', () => {
|
|
5
|
+
it('redacts sensitive object keys (substring match, case-insensitive)', () => {
|
|
6
|
+
const out = scrubValue({
|
|
7
|
+
walletPrivateKey: '0xabc',
|
|
8
|
+
authToken: 'jwt',
|
|
9
|
+
'x-api-key': 'k',
|
|
10
|
+
mnemonic: 'a b c',
|
|
11
|
+
signature: '0xsig',
|
|
12
|
+
message: 'sign me',
|
|
13
|
+
chainId: 'eip155:1'
|
|
14
|
+
}) as Record<string, unknown>
|
|
15
|
+
expect(out.walletPrivateKey).toBe('[Redacted]')
|
|
16
|
+
expect(out.authToken).toBe('[Redacted]')
|
|
17
|
+
expect(out['x-api-key']).toBe('[Redacted]')
|
|
18
|
+
expect(out.mnemonic).toBe('[Redacted]')
|
|
19
|
+
expect(out.signature).toBe('[Redacted]')
|
|
20
|
+
expect(out.message).toBe('[Redacted]')
|
|
21
|
+
// Non-sensitive keys pass through.
|
|
22
|
+
expect(out.chainId).toBe('eip155:1')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('redacts secrets embedded in free-form strings (hex blobs, k/v params, bearer)', () => {
|
|
26
|
+
// EVM address (40 nibbles) and a longer signature/key blob.
|
|
27
|
+
expect(
|
|
28
|
+
scrubMessage(
|
|
29
|
+
'insufficient funds for 0x1234567890abcdef1234567890abcdef12345678'
|
|
30
|
+
)
|
|
31
|
+
).toBe('insufficient funds for 0x[redacted-hex]')
|
|
32
|
+
expect(
|
|
33
|
+
scrubMessage('relay https://r.io/?token=abc123secret&chain=1 failed')
|
|
34
|
+
).toContain('token=[redacted]')
|
|
35
|
+
expect(scrubMessage('Authorization: Bearer eyJabc.def-ghi')).toContain(
|
|
36
|
+
'Bearer [redacted]'
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('redacts a Solana base58 address embedded in a free-form string', () => {
|
|
41
|
+
const out = scrubMessage(
|
|
42
|
+
'transfer to EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v failed'
|
|
43
|
+
)
|
|
44
|
+
expect(out).toContain('[redacted-base58]')
|
|
45
|
+
expect(out).not.toContain('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('redacts base58 runs LONGER than a bare address (signatures, serialized blobs)', () => {
|
|
49
|
+
// A Solana signature is ~88 base58 chars. The old `{32,44}\b` rule could
|
|
50
|
+
// never match a contiguous run >44 chars (no word boundary lands inside a
|
|
51
|
+
// run), so signatures egressed verbatim — contradicting the documented
|
|
52
|
+
// over-redaction contract.
|
|
53
|
+
const sig = 'A2b'.repeat(30) // 90-char base58 run (no 0/O/I/l)
|
|
54
|
+
const out = scrubMessage(`Transaction ${sig} failed to confirm`)
|
|
55
|
+
expect(out).not.toContain(sig)
|
|
56
|
+
expect(out).toContain('[redacted-base58]')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('redacts a base58 address even when GLUED to a base58-excluded word char', () => {
|
|
60
|
+
// `0`, `O`, `I`, `l`, `_` are word chars OUTSIDE the base58 alphabet: they
|
|
61
|
+
// break the character class AND defeat a `\b` anchor, so an address glued
|
|
62
|
+
// to one slipped through. Anchor-free matching closes that shape.
|
|
63
|
+
const addr = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
|
|
64
|
+
expect(scrubMessage(`account_${addr}`)).not.toContain(addr)
|
|
65
|
+
expect(scrubMessage(`${addr}0suffix`)).not.toContain(addr)
|
|
66
|
+
expect(scrubMessage(`l${addr}`)).not.toContain(addr)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('redacts raw-form TON addresses (workchain:hex), both 0: and -1:', () => {
|
|
70
|
+
const raw = '0:' + 'ab12'.repeat(16)
|
|
71
|
+
const neg = '-1:' + 'ab12'.repeat(16)
|
|
72
|
+
const out = scrubMessage(`bounce from ${raw} and ${neg}`)
|
|
73
|
+
expect(out).not.toContain(raw)
|
|
74
|
+
expect(out).not.toContain(neg)
|
|
75
|
+
expect(out).toContain('[redacted-ton-address]')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('redacts a raw-form TON address GLUED to word chars (no \\b anchoring)', () => {
|
|
79
|
+
// Digits and `_` are word chars, so a `\b`-anchored pattern never matched
|
|
80
|
+
// `account_0:<hex>` — the same glue failure as the base58 case above.
|
|
81
|
+
const raw = '0:' + 'ab12'.repeat(16)
|
|
82
|
+
const glued = scrubMessage(`account_${raw}`)
|
|
83
|
+
expect(glued).not.toContain(raw)
|
|
84
|
+
expect(glued).toContain('[redacted-ton-address]')
|
|
85
|
+
expect(scrubMessage(`${raw}_suffix`)).not.toContain(raw)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('does not treat a LONGER hex run as a raw-form TON address', () => {
|
|
89
|
+
// 65+ hex chars after the colon is some other blob, not a raw address.
|
|
90
|
+
// (The broad base58 rule may still over-redact it — by design — so assert
|
|
91
|
+
// only that the TON-specific marker is not applied.)
|
|
92
|
+
const longer = '0:' + 'ab12'.repeat(16) + 'f'
|
|
93
|
+
expect(scrubMessage(`id ${longer}`)).not.toContain('[redacted-ton-address]')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('redacts a TON user-friendly address (EQ…/UQ…)', () => {
|
|
97
|
+
const ton = 'EQ' + 'D'.repeat(46)
|
|
98
|
+
expect(scrubMessage(`bounce from ${ton}`)).toContain(
|
|
99
|
+
'[redacted-ton-address]'
|
|
100
|
+
)
|
|
101
|
+
const uq = 'UQ' + 'a'.repeat(46)
|
|
102
|
+
expect(scrubMessage(`to ${uq}`)).toContain('[redacted-ton-address]')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('does not redact a short ordinary word', () => {
|
|
106
|
+
expect(scrubMessage('transfer failed: insufficient balance')).toBe(
|
|
107
|
+
'transfer failed: insufficient balance'
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('redacts embedded secrets inside object string values too', () => {
|
|
112
|
+
const out = scrubValue({
|
|
113
|
+
note: 'sent to 0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
|
|
114
|
+
}) as Record<string, unknown>
|
|
115
|
+
expect(out.note).toBe('sent to 0x[redacted-hex]')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('caps long strings', () => {
|
|
119
|
+
// '0' is outside the base58 alphabet, so this exercises the cap alone.
|
|
120
|
+
const long = '0'.repeat(600)
|
|
121
|
+
const out = scrubMessage(long)
|
|
122
|
+
expect(out.length).toBeLessThan(600)
|
|
123
|
+
expect(out.endsWith('…[truncated]')).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('redacts a hex blob before the length cap can slice it (no surviving tail)', () => {
|
|
127
|
+
// The secret must be redacted whole BEFORE truncation — never sliced so a
|
|
128
|
+
// partial address survives the cut. The address sits past the 500-char cap.
|
|
129
|
+
const addr = '0x' + 'a'.repeat(40)
|
|
130
|
+
const out = scrubMessage('y'.repeat(600) + addr)
|
|
131
|
+
expect(out).not.toContain(addr)
|
|
132
|
+
// And when the placeholder fits, it's present (redaction actually ran).
|
|
133
|
+
const out2 = scrubMessage('y'.repeat(100) + addr + 'z'.repeat(600))
|
|
134
|
+
expect(out2).not.toContain(addr)
|
|
135
|
+
expect(out2).toContain('0x[redacted-hex]')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('uses a null prototype so __proto__ keys cannot pollute Object.prototype', () => {
|
|
139
|
+
const malicious = JSON.parse('{"__proto__": {"polluted": true}}')
|
|
140
|
+
const out = scrubValue(malicious) as Record<string, unknown>
|
|
141
|
+
expect(Object.getPrototypeOf(out)).toBeNull()
|
|
142
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('renders bigint, function and symbol inertly', () => {
|
|
146
|
+
expect(scrubValue(10n)).toBe('10')
|
|
147
|
+
expect(scrubValue(() => 1)).toBe('[function]')
|
|
148
|
+
expect(scrubValue(Symbol('s'))).toBe('[symbol]')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('reduces Error instances to name + message (no stack)', () => {
|
|
152
|
+
const out = scrubValue(new Error('kaboom')) as Record<string, unknown>
|
|
153
|
+
expect(out).toEqual({ name: 'Error', message: 'kaboom' })
|
|
154
|
+
// Same null-prototype guarantee as the generic object branch.
|
|
155
|
+
expect(Object.getPrototypeOf(out)).toBe(null)
|
|
156
|
+
expect(out.stack).toBeUndefined()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('stops recursing past max depth', () => {
|
|
160
|
+
const deep = { a: { b: { c: { d: { e: { f: 1 } } } } } }
|
|
161
|
+
const out = scrubValue(deep) as Record<string, unknown>
|
|
162
|
+
// 5 levels down we hit the cap.
|
|
163
|
+
const a = out.a as Record<string, unknown>
|
|
164
|
+
const b = a.b as Record<string, unknown>
|
|
165
|
+
const c = b.c as Record<string, unknown>
|
|
166
|
+
const d = c.d as Record<string, unknown>
|
|
167
|
+
expect(d.e).toBe('[Truncated: max depth]')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('scrubArgs maps every arg', () => {
|
|
171
|
+
const out = scrubArgs(['ok', { secret: 's', id: 1 }])
|
|
172
|
+
expect(out[0]).toBe('ok')
|
|
173
|
+
expect((out[1] as Record<string, unknown>).secret).toBe('[Redacted]')
|
|
174
|
+
expect((out[1] as Record<string, unknown>).id).toBe(1)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('passes null/undefined/primitives through', () => {
|
|
178
|
+
expect(scrubValue(null)).toBeNull()
|
|
179
|
+
expect(scrubValue(undefined)).toBeUndefined()
|
|
180
|
+
expect(scrubValue(true)).toBe(true)
|
|
181
|
+
expect(scrubValue(5)).toBe(5)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII / secret scrubber for data leaving UWC through the observer seam.
|
|
3
|
+
*
|
|
4
|
+
* Security-first: the normalized `UWCTelemetryEvent` excludes signing payloads
|
|
5
|
+
* and addresses by construction — its STRUCTURED fields have no slot for them
|
|
6
|
+
* (§ telemetry.ts). This scrubber is the second line of defence for the two
|
|
7
|
+
* surfaces that CAN still carry free-form data on the observer path: the
|
|
8
|
+
* `WalletError.message` forwarded to `observer.onEvent`, and the `args` forwarded
|
|
9
|
+
* to `observer.onLog`. (The console / custom-logger sink receives raw data — it's
|
|
10
|
+
* local, not egress.)
|
|
11
|
+
*
|
|
12
|
+
* Two detection layers:
|
|
13
|
+
* - KEY-based redaction of sensitive object keys — substring match (not regex),
|
|
14
|
+
* avoiding the ReDoS class that Wiz flags on user-input regexes.
|
|
15
|
+
* - CONTENT-based redaction of secrets embedded in free-form strings (hex blobs,
|
|
16
|
+
* secret k/v params, bearer tokens) — via linear regexes with no nested or
|
|
17
|
+
* overlapping quantifiers (see SECRET_CONTENT_PATTERNS), so they carry no
|
|
18
|
+
* catastrophic-backtracking risk.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Separator-free, lower-cased fragments that mark a value as sensitive. Keys are
|
|
23
|
+
* normalized (separators stripped) before matching, so `walletPrivateKey`,
|
|
24
|
+
* `private_key`, `x-api-key`, and `authToken` all hit the same short list. This
|
|
25
|
+
* over-redacts by design (a `security`-first scrubber prefers a false redaction
|
|
26
|
+
* to a leak).
|
|
27
|
+
*/
|
|
28
|
+
const SENSITIVE_KEY_FRAGMENTS = [
|
|
29
|
+
'seed',
|
|
30
|
+
'mnemonic',
|
|
31
|
+
'privatekey',
|
|
32
|
+
'secret',
|
|
33
|
+
'password',
|
|
34
|
+
'passphrase',
|
|
35
|
+
'signature',
|
|
36
|
+
'message',
|
|
37
|
+
'payload',
|
|
38
|
+
'token',
|
|
39
|
+
'apikey',
|
|
40
|
+
'authorization',
|
|
41
|
+
'auth'
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const MAX_STRING_LENGTH = 500
|
|
45
|
+
const MAX_DEPTH = 4
|
|
46
|
+
const REDACTED = '[Redacted]'
|
|
47
|
+
|
|
48
|
+
function isSensitiveKey(key: string): boolean {
|
|
49
|
+
// Strip separators so `private_key` / `x-api-key` normalize to the bare form.
|
|
50
|
+
const normalized = key.toLowerCase().split('-').join('').split('_').join('')
|
|
51
|
+
return SENSITIVE_KEY_FRAGMENTS.some(fragment => normalized.includes(fragment))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Content patterns for secrets embedded in FREE-FORM strings (error messages,
|
|
55
|
+
// log args) where key-based redaction can't reach. All are linear expressions
|
|
56
|
+
// with no nested/overlapping quantifiers, so they carry no catastrophic-
|
|
57
|
+
// backtracking (ReDoS) risk on attacker-controlled input.
|
|
58
|
+
const SECRET_CONTENT_PATTERNS: ReadonlyArray<readonly [RegExp, string]> = [
|
|
59
|
+
// 0x-prefixed hex blobs: EVM addresses (40 nibbles) and longer
|
|
60
|
+
// signatures/keys/hashes/calldata (64+). Over-redacts public hashes by design.
|
|
61
|
+
[/0x[a-fA-F0-9]{40,}/g, '0x[redacted-hex]'],
|
|
62
|
+
// `secret=…` / `token=…` / `key=…` query-string or k/v fragments.
|
|
63
|
+
[
|
|
64
|
+
/\b(token|key|secret|auth|sig|signature|password|passphrase|mnemonic|seed|apikey)=[^\s&"']+/gi,
|
|
65
|
+
'$1=[redacted]'
|
|
66
|
+
],
|
|
67
|
+
// `Bearer <opaque>` / `Basic <opaque>` authorization values.
|
|
68
|
+
[/\b(bearer|basic)\s+[A-Za-z0-9._~+/-]+=*/gi, '$1 [redacted]'],
|
|
69
|
+
// TON user-friendly addresses: `EQ…` / `UQ…` + 46 base64url chars (48 total).
|
|
70
|
+
// Trailing edge is a negative lookahead, not `\b`: a base64url address can end
|
|
71
|
+
// in `-`/`_`, where `\b` fails to anchor and the address would slip through.
|
|
72
|
+
[/\b[EU]Q[A-Za-z0-9_-]{46}(?![A-Za-z0-9_-])/g, '[redacted-ton-address]'],
|
|
73
|
+
// TON raw-form addresses: `workchain:64-hex` (`0:…`, `-1:…`). Before base58 so
|
|
74
|
+
// the hex tail gets the specific TON marker, not a generic base58 hit.
|
|
75
|
+
// Anchor-free like base58 below: digits and `_` are word chars, so a `\b`
|
|
76
|
+
// never anchors when the address is glued to one (`account_0:…` leaked).
|
|
77
|
+
// The trailing edge is a hex negative lookahead — a LONGER contiguous hex
|
|
78
|
+
// run is a different blob, not a 64-hex address.
|
|
79
|
+
[/-?\d+:[0-9a-fA-F]{64}(?![0-9a-fA-F])/g, '[redacted-ton-address]'],
|
|
80
|
+
// Solana / generic base58, 32 chars or MORE — bare addresses AND longer blobs
|
|
81
|
+
// (a signature is ~88 chars, a serialized tx longer still). No upper bound: a
|
|
82
|
+
// capped quantifier can never match inside a longer contiguous run. No `\b`
|
|
83
|
+
// anchors either: `0`/`O`/`I`/`l`/`_` are word chars OUTSIDE the base58
|
|
84
|
+
// alphabet, so they break the class AND defeat a boundary anchor — an address
|
|
85
|
+
// glued to one (`account_<addr>`) would slip through anchored matching.
|
|
86
|
+
// Over-redaction inside longer tokens is accepted by design. Placed LAST: it
|
|
87
|
+
// is the broadest pattern and would otherwise swallow narrower matches above.
|
|
88
|
+
[/[1-9A-HJ-NP-Za-km-z]{32,}/g, '[redacted-base58]']
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Redact secrets that live INSIDE a free-form string. Best-effort and
|
|
93
|
+
* deliberately conservative toward over-redaction: a wallet error string can
|
|
94
|
+
* embed an address, a reverted-tx calldata blob, or a URL with a token, none of
|
|
95
|
+
* which key-based redaction sees.
|
|
96
|
+
*
|
|
97
|
+
* Base58 (Solana) and TON addresses (user-friendly `EQ…`/`UQ…` AND the raw
|
|
98
|
+
* `workchain:hex` form) are redacted as a security gate: a leaked
|
|
99
|
+
* recipient/wallet address on the egress path is treated as a defect, so we
|
|
100
|
+
* accept that any base58-looking token of 32+ chars (some ids, tx hashes,
|
|
101
|
+
* IPFS CIDs) is over-redacted by design. The narrower TON patterns run before
|
|
102
|
+
* the broad base58 one so TON addresses get the specific marker.
|
|
103
|
+
*
|
|
104
|
+
* One documented gap remains: BARE (non-`0x`) hex blobs — a raw 64-char hex
|
|
105
|
+
* string is far more often a legitimate id/hash than a secret, so only
|
|
106
|
+
* `0x`-prefixed hex is redacted. Structured telemetry fields never carry
|
|
107
|
+
* addresses regardless (see telemetry.ts); this pass only hardens the free-form
|
|
108
|
+
* `error.message` / log-arg surface.
|
|
109
|
+
*/
|
|
110
|
+
function redactSecretsInString(input: string): string {
|
|
111
|
+
let out = input
|
|
112
|
+
for (const [pattern, replacement] of SECRET_CONTENT_PATTERNS) {
|
|
113
|
+
out = out.replace(pattern, replacement)
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Recursively copy `value`, redacting sensitive object-keys, capping long
|
|
120
|
+
* strings, and rendering non-serializable values inert. The returned objects use
|
|
121
|
+
* a null prototype so a malicious `__proto__`/`constructor` key in attacker-shaped
|
|
122
|
+
* input can't pollute `Object.prototype` downstream.
|
|
123
|
+
*/
|
|
124
|
+
export function scrubValue(value: unknown, depth = 0): unknown {
|
|
125
|
+
if (depth > MAX_DEPTH) return '[Truncated: max depth]'
|
|
126
|
+
|
|
127
|
+
if (value === null || value === undefined) return value
|
|
128
|
+
|
|
129
|
+
const type = typeof value
|
|
130
|
+
if (type === 'string') {
|
|
131
|
+
// Redact embedded secrets BEFORE capping, so the cap can't slice a pattern
|
|
132
|
+
// in half and leak its tail.
|
|
133
|
+
const str = redactSecretsInString(value as string)
|
|
134
|
+
return str.length > MAX_STRING_LENGTH
|
|
135
|
+
? str.slice(0, MAX_STRING_LENGTH) + '…[truncated]'
|
|
136
|
+
: str
|
|
137
|
+
}
|
|
138
|
+
if (type === 'number' || type === 'boolean') return value
|
|
139
|
+
if (type === 'bigint') return (value as bigint).toString()
|
|
140
|
+
if (type === 'function' || type === 'symbol') return `[${type}]`
|
|
141
|
+
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
return value.map(item => scrubValue(item, depth + 1))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (type === 'object') {
|
|
147
|
+
// Error instances carry a useful message but also a stack we don't want.
|
|
148
|
+
// Null prototype here too — the doc above promises it for every returned
|
|
149
|
+
// object, and an Error-shaped attacker value lands in this branch.
|
|
150
|
+
if (value instanceof Error) {
|
|
151
|
+
const out: Record<string, unknown> = Object.create(null)
|
|
152
|
+
out['name'] = value.name
|
|
153
|
+
out['message'] = scrubValue(value.message, depth + 1)
|
|
154
|
+
return out
|
|
155
|
+
}
|
|
156
|
+
const out: Record<string, unknown> = Object.create(null)
|
|
157
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
158
|
+
out[key] = isSensitiveKey(key) ? REDACTED : scrubValue(val, depth + 1)
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return '[Unserializable]'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Scrub a free-form error message (string in → capped string out). */
|
|
167
|
+
export function scrubMessage(message: string): string {
|
|
168
|
+
return scrubValue(message) as string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Scrub a variadic `args` array before it reaches `observer.onLog`. */
|
|
172
|
+
export function scrubArgs(args: unknown[]): unknown[] {
|
|
173
|
+
return args.map(arg => scrubValue(arg))
|
|
174
|
+
}
|