@tillstack/electron 0.2.1 → 0.4.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 +8 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +216 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +217 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -37,6 +37,13 @@ The main-process SDK captures:
|
|
|
37
37
|
|
|
38
38
|
Events are **PII-scrubbed on-device** and buffered in a **file-backed queue** in
|
|
39
39
|
`userData` — so the report explaining a crash survives the restart after it.
|
|
40
|
+
The file is encrypted with the OS keychain through Electron's `safeStorage`
|
|
41
|
+
(Keychain on macOS, DPAPI on Windows, the secret service on Linux) and readable
|
|
42
|
+
only by your user. A file that can't be decrypted is discarded, and with
|
|
43
|
+
`enableThreatDetection` on, a `storage_tamper` security event is sent. Events
|
|
44
|
+
older than 24 hours are dropped. On Linux without a secret store, the queue stays
|
|
45
|
+
in memory unless you set `allowPlaintextQueue: true`. Call `TillPulse.clearQueue()`
|
|
46
|
+
to wipe it, for example when a user signs out.
|
|
40
47
|
Native minidump upload is opt-in (`uploadMinidumps: true`); by default dumps are
|
|
41
48
|
kept locally and a metadata event is sent.
|
|
42
49
|
|
|
@@ -45,4 +52,5 @@ kept locally and a metadata event is sent.
|
|
|
45
52
|
`dsn`, `release` required. Toggles (default on): `enableNativeCrashReporter`,
|
|
46
53
|
`enableProcessMonitoring`, `enableHangDetection`, `enableSessionTracking`. Also
|
|
47
54
|
`uploadMinidumps`, `environment`, `dist`, `disableDefaultPiiScrubbing`,
|
|
55
|
+
`allowPlaintextQueue`,
|
|
48
56
|
`beforeSend`, `initialTags`, `flushIntervalMs`, `debug`.
|
package/dist/index.d.mts
CHANGED
|
@@ -19,6 +19,8 @@ interface TillPulseElectronOptions {
|
|
|
19
19
|
/** Opt in to TillShield desktop threat detection (DevTools-open on renderers). Off by default. */
|
|
20
20
|
enableThreatDetection?: boolean;
|
|
21
21
|
disableDefaultPiiScrubbing?: boolean;
|
|
22
|
+
/** Keep the offline queue on disk unencrypted where the OS offers no keychain (Linux without a secret store). Default false: memory only. */
|
|
23
|
+
allowPlaintextQueue?: boolean;
|
|
22
24
|
beforeSend?: (e: TillPulseEvent) => TillPulseEvent | null | Promise<TillPulseEvent | null>;
|
|
23
25
|
initialTags?: Record<string, string>;
|
|
24
26
|
flushIntervalMs?: number;
|
|
@@ -61,6 +63,7 @@ declare class TillPulseElectronSDK {
|
|
|
61
63
|
getQueueStats(): {
|
|
62
64
|
size: number;
|
|
63
65
|
};
|
|
66
|
+
clearQueue(): void;
|
|
64
67
|
flush(useKeepalive?: boolean): Promise<boolean>;
|
|
65
68
|
close(): void;
|
|
66
69
|
private _device;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ interface TillPulseElectronOptions {
|
|
|
19
19
|
/** Opt in to TillShield desktop threat detection (DevTools-open on renderers). Off by default. */
|
|
20
20
|
enableThreatDetection?: boolean;
|
|
21
21
|
disableDefaultPiiScrubbing?: boolean;
|
|
22
|
+
/** Keep the offline queue on disk unencrypted where the OS offers no keychain (Linux without a secret store). Default false: memory only. */
|
|
23
|
+
allowPlaintextQueue?: boolean;
|
|
22
24
|
beforeSend?: (e: TillPulseEvent) => TillPulseEvent | null | Promise<TillPulseEvent | null>;
|
|
23
25
|
initialTags?: Record<string, string>;
|
|
24
26
|
flushIntervalMs?: number;
|
|
@@ -61,6 +63,7 @@ declare class TillPulseElectronSDK {
|
|
|
61
63
|
getQueueStats(): {
|
|
62
64
|
size: number;
|
|
63
65
|
};
|
|
66
|
+
clearQueue(): void;
|
|
64
67
|
flush(useKeepalive?: boolean): Promise<boolean>;
|
|
65
68
|
close(): void;
|
|
66
69
|
private _device;
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var types = require('@tillstack/types');
|
|
5
6
|
var electron = require('electron');
|
|
6
7
|
var os = require('os');
|
|
8
|
+
var crypto = require('crypto');
|
|
7
9
|
var fs = require('fs');
|
|
8
10
|
var path = require('path');
|
|
9
11
|
|
|
@@ -31,31 +33,65 @@ var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
|
31
33
|
|
|
32
34
|
// src/index.ts
|
|
33
35
|
|
|
34
|
-
//
|
|
36
|
+
// ../scrub-core/dist/rules.js
|
|
35
37
|
var SCRUB_RULES = [
|
|
36
|
-
{ pattern:
|
|
37
|
-
{ pattern:
|
|
38
|
-
{ pattern: /\
|
|
39
|
-
{ pattern:
|
|
40
|
-
{ pattern: /\
|
|
41
|
-
{ pattern: /\
|
|
42
|
-
{ pattern: /\
|
|
43
|
-
{ pattern: /\
|
|
44
|
-
{ pattern:
|
|
45
|
-
{ pattern: /\
|
|
46
|
-
{ pattern: /\
|
|
47
|
-
{ pattern:
|
|
48
|
-
{ pattern:
|
|
49
|
-
{ pattern: /\
|
|
50
|
-
{ pattern:
|
|
51
|
-
{ pattern:
|
|
52
|
-
{ pattern: /\
|
|
38
|
+
{ label: "private_key", pattern: /-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY(?: BLOCK)?-----(?:[^-"\\]|\\[^"]|-Type|-Info)*(?:-----END [A-Z0-9 ]{0,40}PRIVATE KEY(?: BLOCK)?-----)?/g, replacement: "[private_key]" },
|
|
39
|
+
{ label: "private_key", pattern: /\bAGE-SECRET-KEY-1[0-9A-Z]{58}/g, replacement: "[private_key]" },
|
|
40
|
+
{ label: "aws_key", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g, replacement: "[aws_key]" },
|
|
41
|
+
{ label: "aws_secret", pattern: /(?:aws_secret|secret_access_key|AWS_SECRET)[^A-Za-z0-9+/]*([A-Za-z0-9+/]{40})\b/gi, replacement: "[aws_secret]" },
|
|
42
|
+
{ label: "gcp_key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: "[gcp_key]" },
|
|
43
|
+
{ label: "gcp_key", pattern: /\bGOCSPX-[0-9A-Za-z_-]{28}/g, replacement: "[gcp_key]" },
|
|
44
|
+
{ label: "github_token", pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g, replacement: "[github_token]" },
|
|
45
|
+
{ label: "github_token", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g, replacement: "[github_token]" },
|
|
46
|
+
{ label: "gitlab_token", pattern: /\bgl(?:pat|rt|ptt|ft|dt|soat|imt|cbt|agent)-[0-9A-Za-z_-]{20,}/g, replacement: "[gitlab_token]" },
|
|
47
|
+
{ label: "slack_token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
|
|
48
|
+
{ label: "slack_token", pattern: /\bxapp-\d-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
|
|
49
|
+
{ label: "webhook_url", pattern: /https:\/\/hooks\.slack\.com\/services\/[0-9A-Za-z_/]+/g, replacement: "[webhook_url]" },
|
|
50
|
+
{ label: "webhook_url", pattern: /https:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[0-9A-Za-z_-]+/g, replacement: "[webhook_url]" },
|
|
51
|
+
{ label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, replacement: "[jwt]" },
|
|
52
|
+
{ label: "bearer", pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: "Bearer [token]" },
|
|
53
|
+
{ label: "apikey", pattern: /\b(?:sk|pk|ak|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, replacement: "[apikey]" },
|
|
54
|
+
{ label: "apikey", pattern: /\bwhsec_[0-9A-Za-z]{24,}/g, replacement: "[apikey]" },
|
|
55
|
+
{ label: "package_token", pattern: /\b(?:npm_[0-9A-Za-z]{36}|pypi-[0-9A-Za-z_-]{50,}|rubygems_[0-9a-f]{48}|dckr_pat_[0-9A-Za-z_-]{20,})/g, replacement: "[package_token]" },
|
|
56
|
+
{ label: "ai_key", pattern: /\b(?:sk-(?:or-v1-[0-9a-f]{64}|ant-[0-9A-Za-z_-]{32,}|proj-[0-9A-Za-z_-]{32,})|hf_[0-9A-Za-z]{34,})/g, replacement: "[ai_key]" },
|
|
57
|
+
{ label: "ai_key", pattern: /\bsk-[A-Za-z0-9]{48}\b/g, replacement: "[ai_key]" },
|
|
58
|
+
{ label: "service_token", pattern: /\b(?:SG\.[0-9A-Za-z_-]{22}\.[0-9A-Za-z_-]{43}|do[oprt]_v1_[0-9a-f]{64}|hv[sb]\.[A-Za-z0-9_-]{24,}|lin_api_[0-9A-Za-z]{40}|shp(?:at|ss|pa|ca)_[0-9a-fA-F]{32}|sntry[su]_[0-9A-Za-z_-]{40,})/g, replacement: "[service_token]" },
|
|
59
|
+
{ label: "tilldev_token", pattern: /\b(?:(?:tp|tc|ts|ta|tsc|tse|tark|tnot|tfp|tfd|tfr|tfb)_[0-9a-f]{32}\b|tfn_[0-9a-f]{64}\b|tg_secret_[0-9a-f]{40}\b|(?:tausec|tau_rt|tau_sso|tau_ssg|scim)_[A-Za-z0-9_-]{43}|tau_inv_[A-Za-z0-9_-]{38}|tdr_[A-Za-z0-9_-]{32})/g, replacement: "[tilldev_token]" },
|
|
60
|
+
{ label: "db_url", pattern: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|rediss?|amqps?):\/\/[^:@/\s"']*:[^@/\s"']+@/g, replacement: "[db_credentials]@" },
|
|
61
|
+
{ label: "email", pattern: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g, replacement: "[email]" },
|
|
62
|
+
{ label: "mpesa_tx", pattern: /\b[A-Z0-9]{10}\b(?=\s*confirmed|\s*transaction|\s*M-?Pesa)/gi, replacement: "[mpesa_tx]" },
|
|
63
|
+
{ label: "card", pattern: /\b(?:\d[ -]*?){13,19}\b/g, replacement: "[card]", outsideIds: true },
|
|
64
|
+
{ label: "id_number", pattern: /\b\d{13}\b/g, replacement: "[id_number]", outsideIds: true },
|
|
65
|
+
{ label: "id_number", pattern: /\b\d{11}\b/g, replacement: "[id_number]", outsideIds: true },
|
|
66
|
+
{ label: "phone", pattern: /\+?\d{1,3}[\s.-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}\b/g, replacement: "[phone]", outsideIds: true },
|
|
67
|
+
{ label: "ip", pattern: /\b(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\b|\b(?:[0-9A-Fa-f]{1,4}:){2,6}:[0-9A-Fa-f]{1,4}\b|\b[0-9A-Fa-f]{1,4}::(?:[0-9A-Fa-f]{1,4}:){1,5}[0-9A-Fa-f]{1,4}\b/g, replacement: "[ip]", outsideIds: true },
|
|
68
|
+
{ label: "ip", pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[ip]", outsideIds: true }
|
|
53
69
|
];
|
|
54
70
|
var SENSITIVE_KEY = /^(?:password|passwd|pwd|secret|client[_-]?secret|token|access[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|apikey|authorization|auth|session|cookie|set[_-]?cookie|private[_-]?key|credit[_-]?card|card[_-]?number|cvv|cvc|ssn|pin|otp|mfa|totp)$/i;
|
|
71
|
+
var ID_TOKEN = /\b[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\b|\b[0-9A-Fa-f]{16,}\b/g;
|
|
72
|
+
var HEX_LETTER = /[A-Fa-f]/;
|
|
73
|
+
function applyRule(rule, text, onHit) {
|
|
74
|
+
const rep = () => rule.replacement;
|
|
75
|
+
if (!rule.outsideIds)
|
|
76
|
+
return text.replace(rule.pattern, rep);
|
|
77
|
+
let out = "";
|
|
78
|
+
let last = 0;
|
|
79
|
+
for (const m of text.matchAll(ID_TOKEN)) {
|
|
80
|
+
if (!HEX_LETTER.test(m[0]))
|
|
81
|
+
continue;
|
|
82
|
+
out += text.slice(last, m.index).replace(rule.pattern, rep) + m[0];
|
|
83
|
+
last = m.index + m[0].length;
|
|
84
|
+
}
|
|
85
|
+
return out + text.slice(last).replace(rule.pattern, rep);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../scrub-core/dist/scrub.js
|
|
55
89
|
var MAX_REDACT_DEPTH = 24;
|
|
56
90
|
function redactByKey(value, depth = 0) {
|
|
57
|
-
if (depth > MAX_REDACT_DEPTH || value === null || typeof value !== "object")
|
|
58
|
-
|
|
91
|
+
if (depth > MAX_REDACT_DEPTH || value === null || typeof value !== "object")
|
|
92
|
+
return value;
|
|
93
|
+
if (Array.isArray(value))
|
|
94
|
+
return value.map((v) => redactByKey(v, depth + 1));
|
|
59
95
|
const out = {};
|
|
60
96
|
for (const [k, v] of Object.entries(value)) {
|
|
61
97
|
out[k] = SENSITIVE_KEY.test(k) ? "[redacted]" : redactByKey(v, depth + 1);
|
|
@@ -65,46 +101,115 @@ function redactByKey(value, depth = 0) {
|
|
|
65
101
|
function scrubEvent(event) {
|
|
66
102
|
try {
|
|
67
103
|
let json = JSON.stringify(redactByKey(event));
|
|
68
|
-
for (const
|
|
104
|
+
for (const rule of SCRUB_RULES)
|
|
105
|
+
json = applyRule(rule, json);
|
|
69
106
|
return JSON.parse(json);
|
|
70
107
|
} catch {
|
|
71
108
|
return event;
|
|
72
109
|
}
|
|
73
110
|
}
|
|
74
111
|
|
|
75
|
-
// src/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
function parseDsn(dsn) {
|
|
79
|
-
const m = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);
|
|
80
|
-
if (!m) throw new Error(`TillPulse: invalid DSN: ${dsn}`);
|
|
81
|
-
const scheme = m[1];
|
|
82
|
-
const host = m[3];
|
|
83
|
-
const local = /^(localhost|127\.0\.0\.1|::1)(:\d+)?$/i.test(host);
|
|
84
|
-
if (scheme !== "https" && !local) {
|
|
85
|
-
throw new Error("TillPulse: DSN must use https:// (refusing to send telemetry over cleartext http)");
|
|
86
|
-
}
|
|
87
|
-
return { scheme, host };
|
|
112
|
+
// src/scrub.ts
|
|
113
|
+
function scrubEvent2(event) {
|
|
114
|
+
return scrubEvent(event);
|
|
88
115
|
}
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
var KEY_FILE = "tillpulse-queue.key";
|
|
117
|
+
var AAD = Buffer.from("tillpulse.queue.v1");
|
|
118
|
+
var VERSION = 1;
|
|
119
|
+
function gcmCodec(dir, kw) {
|
|
120
|
+
let key = null;
|
|
121
|
+
const keyFile = path__namespace.join(dir, KEY_FILE);
|
|
122
|
+
const load = () => {
|
|
123
|
+
if (key) return key;
|
|
124
|
+
try {
|
|
125
|
+
const k = Buffer.from(kw.unwrap(fs__namespace.readFileSync(keyFile)), "base64");
|
|
126
|
+
if (k.length === 32) return key = k;
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
const fresh = crypto.randomBytes(32);
|
|
130
|
+
const tmp = `${keyFile}.${process.pid}.tmp`;
|
|
131
|
+
fs__namespace.writeFileSync(tmp, kw.wrap(fresh.toString("base64")), { mode: 384 });
|
|
132
|
+
fs__namespace.renameSync(tmp, keyFile);
|
|
133
|
+
return key = fresh;
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
encode(plain) {
|
|
137
|
+
const iv = crypto.randomBytes(12);
|
|
138
|
+
const c = crypto.createCipheriv("aes-256-gcm", load(), iv);
|
|
139
|
+
c.setAAD(AAD);
|
|
140
|
+
const ct = Buffer.concat([c.update(plain, "utf8"), c.final()]);
|
|
141
|
+
return Buffer.concat([Buffer.from([VERSION]), iv, ct, c.getAuthTag()]);
|
|
142
|
+
},
|
|
143
|
+
decode(data) {
|
|
144
|
+
if (data.length < 29 || data[0] !== VERSION) throw new Error("bad blob");
|
|
145
|
+
const d = crypto.createDecipheriv("aes-256-gcm", load(), data.subarray(1, 13));
|
|
146
|
+
d.setAAD(AAD);
|
|
147
|
+
d.setAuthTag(data.subarray(data.length - 16));
|
|
148
|
+
return Buffer.concat([d.update(data.subarray(13, data.length - 16)), d.final()]).toString("utf8");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
91
151
|
}
|
|
152
|
+
var FILE = "tillpulse-queue.bin";
|
|
153
|
+
var LEGACY_FILE = "tillpulse-queue.json";
|
|
154
|
+
var MAX_ITEMS = 500;
|
|
155
|
+
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
156
|
+
var LEASE_MS = 2e4;
|
|
92
157
|
var FileQueue = class {
|
|
93
|
-
constructor(
|
|
158
|
+
constructor() {
|
|
94
159
|
this.items = [];
|
|
95
|
-
this.
|
|
96
|
-
this.
|
|
160
|
+
this.codec = null;
|
|
161
|
+
this.dir = null;
|
|
162
|
+
this.onTamper = null;
|
|
163
|
+
}
|
|
164
|
+
attach(dir, codec) {
|
|
165
|
+
this.dir = dir;
|
|
166
|
+
this.codec = codec;
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const legacy = path__namespace.join(dir, LEGACY_FILE);
|
|
97
169
|
try {
|
|
98
|
-
if (fs__namespace.existsSync(
|
|
99
|
-
|
|
100
|
-
|
|
170
|
+
if (fs__namespace.existsSync(legacy)) {
|
|
171
|
+
this.adopt(JSON.parse(fs__namespace.readFileSync(legacy, "utf8")), now);
|
|
172
|
+
fs__namespace.rmSync(legacy, { force: true });
|
|
101
173
|
}
|
|
102
174
|
} catch {
|
|
175
|
+
fs__namespace.rmSync(legacy, { force: true });
|
|
176
|
+
}
|
|
177
|
+
const file = path__namespace.join(dir, FILE);
|
|
178
|
+
if (codec && fs__namespace.existsSync(file)) {
|
|
179
|
+
try {
|
|
180
|
+
this.adopt(JSON.parse(codec.decode(fs__namespace.readFileSync(file))), now);
|
|
181
|
+
} catch {
|
|
182
|
+
fs__namespace.rmSync(file, { force: true });
|
|
183
|
+
this.onTamper?.();
|
|
184
|
+
}
|
|
185
|
+
} else if (!codec) {
|
|
186
|
+
fs__namespace.rmSync(file, { force: true });
|
|
187
|
+
}
|
|
188
|
+
this.persist();
|
|
189
|
+
}
|
|
190
|
+
adopt(list, now) {
|
|
191
|
+
if (!Array.isArray(list)) return;
|
|
192
|
+
const known = new Set(this.items.map((i) => i.id));
|
|
193
|
+
for (const raw of list) {
|
|
194
|
+
if (!raw || typeof raw.id !== "string" || typeof raw.payload !== "string" || typeof raw.event_type !== "string") continue;
|
|
195
|
+
if (known.has(raw.id)) continue;
|
|
196
|
+
const created = typeof raw.created === "number" ? raw.created : now;
|
|
197
|
+
if (now - created > MAX_AGE_MS) continue;
|
|
198
|
+
this.items.push({ id: raw.id, event_type: raw.event_type, payload: raw.payload, attempts: Number(raw.attempts) || 0, next: 0, created });
|
|
103
199
|
}
|
|
200
|
+
if (this.items.length > MAX_ITEMS) this.items = this.items.slice(this.items.length - MAX_ITEMS);
|
|
104
201
|
}
|
|
105
202
|
persist() {
|
|
203
|
+
if (!this.dir || !this.codec) return;
|
|
204
|
+
const file = path__namespace.join(this.dir, FILE);
|
|
106
205
|
try {
|
|
107
|
-
|
|
206
|
+
if (this.items.length === 0) {
|
|
207
|
+
fs__namespace.rmSync(file, { force: true });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
211
|
+
fs__namespace.writeFileSync(tmp, this.codec.encode(JSON.stringify(this.items)), { mode: 384 });
|
|
212
|
+
fs__namespace.renameSync(tmp, file);
|
|
108
213
|
} catch {
|
|
109
214
|
}
|
|
110
215
|
}
|
|
@@ -115,13 +220,17 @@ var FileQueue = class {
|
|
|
115
220
|
} catch {
|
|
116
221
|
return;
|
|
117
222
|
}
|
|
118
|
-
|
|
119
|
-
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
this.items.push({ id, event_type: eventType, payload, attempts: 0, next: 0, created: now });
|
|
225
|
+
if (this.items.length > MAX_ITEMS) this.items = this.items.slice(this.items.length - MAX_ITEMS);
|
|
120
226
|
this.persist();
|
|
121
227
|
}
|
|
122
228
|
due(limit) {
|
|
123
229
|
const now = Date.now();
|
|
124
|
-
|
|
230
|
+
this.items = this.items.filter((i) => now - i.created <= MAX_AGE_MS);
|
|
231
|
+
const out = this.items.filter((i) => i.next <= now).slice(0, limit);
|
|
232
|
+
for (const i of out) i.next = now + LEASE_MS;
|
|
233
|
+
return out;
|
|
125
234
|
}
|
|
126
235
|
ackOk(ids) {
|
|
127
236
|
const s = new Set(ids);
|
|
@@ -139,10 +248,45 @@ var FileQueue = class {
|
|
|
139
248
|
this.items = this.items.filter((i) => i.attempts <= 10);
|
|
140
249
|
this.persist();
|
|
141
250
|
}
|
|
251
|
+
clear() {
|
|
252
|
+
this.items = [];
|
|
253
|
+
if (this.dir) {
|
|
254
|
+
fs__namespace.rmSync(path__namespace.join(this.dir, FILE), { force: true });
|
|
255
|
+
fs__namespace.rmSync(path__namespace.join(this.dir, LEGACY_FILE), { force: true });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
142
258
|
size() {
|
|
143
259
|
return this.items.length;
|
|
144
260
|
}
|
|
145
261
|
};
|
|
262
|
+
|
|
263
|
+
// src/index.ts
|
|
264
|
+
var SDK_NAME = "@tillstack/electron";
|
|
265
|
+
var SDK_VERSION = "0.4.0" ;
|
|
266
|
+
function parseDsn(dsn) {
|
|
267
|
+
const m = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);
|
|
268
|
+
if (!m) throw new Error(`TillPulse: invalid DSN: ${dsn}`);
|
|
269
|
+
const scheme = m[1];
|
|
270
|
+
const host = m[3];
|
|
271
|
+
const local = /^(localhost|127\.0\.0\.1|::1)(:\d+)?$/i.test(host);
|
|
272
|
+
if (scheme !== "https" && !local) {
|
|
273
|
+
throw new Error("TillPulse: DSN must use https:// (refusing to send telemetry over cleartext http)");
|
|
274
|
+
}
|
|
275
|
+
return { scheme, host };
|
|
276
|
+
}
|
|
277
|
+
function generateId() {
|
|
278
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
279
|
+
}
|
|
280
|
+
function osCodec(dir, allowPlaintext) {
|
|
281
|
+
try {
|
|
282
|
+
const backend = process.platform === "linux" ? electron.safeStorage.getSelectedStorageBackend?.() : void 0;
|
|
283
|
+
if (electron.safeStorage.isEncryptionAvailable() && backend !== "basic_text" && backend !== "unknown") {
|
|
284
|
+
return gcmCodec(dir, { wrap: (t) => electron.safeStorage.encryptString(t), unwrap: (b) => electron.safeStorage.decryptString(b) });
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
return allowPlaintext ? { encode: (t) => Buffer.from(t, "utf8"), decode: (b) => b.toString("utf8") } : null;
|
|
289
|
+
}
|
|
146
290
|
function osName() {
|
|
147
291
|
if (process.platform === "darwin") return "macOS";
|
|
148
292
|
if (process.platform === "win32") return "Windows";
|
|
@@ -182,7 +326,19 @@ var TillPulseElectronSDK = class {
|
|
|
182
326
|
dir = electron.app.getPath("userData");
|
|
183
327
|
} catch {
|
|
184
328
|
}
|
|
185
|
-
this.queue = new FileQueue(
|
|
329
|
+
this.queue = new FileQueue();
|
|
330
|
+
this.queue.onTamper = () => {
|
|
331
|
+
if (!this.opts?.enableThreatDetection) return;
|
|
332
|
+
this.captureSecurityEvent({
|
|
333
|
+
securityType: "storage_tamper",
|
|
334
|
+
severity: "medium",
|
|
335
|
+
confidence: 0.6,
|
|
336
|
+
message: "Stored TillPulse events failed their integrity check and were discarded"
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
const attach = () => this.queue?.attach(dir, osCodec(dir, options.allowPlaintextQueue === true));
|
|
340
|
+
if (electron.app.isReady()) attach();
|
|
341
|
+
else void electron.app.whenReady().then(attach);
|
|
186
342
|
if (options.enableNativeCrashReporter !== false) {
|
|
187
343
|
try {
|
|
188
344
|
electron.crashReporter.start({
|
|
@@ -283,14 +439,12 @@ var TillPulseElectronSDK = class {
|
|
|
283
439
|
if (this.timer.unref) this.timer.unref();
|
|
284
440
|
if (options.debug) console.log("[TillPulse] electron SDK initialized", { release: options.release });
|
|
285
441
|
}
|
|
286
|
-
// ── Public API ──
|
|
287
442
|
captureException(error) {
|
|
288
443
|
this._captureError(error, "high", "captureException");
|
|
289
444
|
}
|
|
290
445
|
captureMessage(message, level = "info") {
|
|
291
446
|
this._enqueue(this._base("error", level, { exception: { type: "Message", value: message } }));
|
|
292
447
|
}
|
|
293
|
-
// Report a security signal to TillShield (same shape as the other SDKs).
|
|
294
448
|
captureSecurityEvent(input) {
|
|
295
449
|
this._enqueue(
|
|
296
450
|
this._base("security", input.severity ?? "high", {
|
|
@@ -321,6 +475,9 @@ var TillPulseElectronSDK = class {
|
|
|
321
475
|
getQueueStats() {
|
|
322
476
|
return { size: this.queue?.size() ?? 0 };
|
|
323
477
|
}
|
|
478
|
+
clearQueue() {
|
|
479
|
+
this.queue?.clear();
|
|
480
|
+
}
|
|
324
481
|
async flush(useKeepalive = false) {
|
|
325
482
|
if (!this.queue || !this.opts) return false;
|
|
326
483
|
const items = this.queue.due(50);
|
|
@@ -340,12 +497,16 @@ var TillPulseElectronSDK = class {
|
|
|
340
497
|
headers: { "Content-Type": "application/x-sentry-envelope", Authorization: `DSN ${this.opts.dsn}` },
|
|
341
498
|
body: lines.join("\n")
|
|
342
499
|
});
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
500
|
+
const verdict = types.classifyIngestResponse({
|
|
501
|
+
status: res.status,
|
|
502
|
+
contentType: res.headers.get("content-type")
|
|
503
|
+
});
|
|
504
|
+
if (verdict.disposition === "retry") {
|
|
505
|
+
this.queue.ackFail(ids);
|
|
506
|
+
return false;
|
|
346
507
|
}
|
|
347
|
-
this.queue.
|
|
348
|
-
return
|
|
508
|
+
this.queue.ackOk(ids);
|
|
509
|
+
return true;
|
|
349
510
|
} catch {
|
|
350
511
|
this.queue.ackFail(ids);
|
|
351
512
|
return false;
|
|
@@ -360,7 +521,6 @@ var TillPulseElectronSDK = class {
|
|
|
360
521
|
void this.flush(true);
|
|
361
522
|
this.initialized = false;
|
|
362
523
|
}
|
|
363
|
-
// ── Internals ──
|
|
364
524
|
_device() {
|
|
365
525
|
let total = 0;
|
|
366
526
|
let free = 0;
|
|
@@ -462,7 +622,7 @@ var TillPulseElectronSDK = class {
|
|
|
462
622
|
}
|
|
463
623
|
}
|
|
464
624
|
if (!final) return;
|
|
465
|
-
if (!o.disableDefaultPiiScrubbing) final =
|
|
625
|
+
if (!o.disableDefaultPiiScrubbing) final = scrubEvent2(final);
|
|
466
626
|
this.queue.enqueue(final.event_id, final.event_type, final);
|
|
467
627
|
}
|
|
468
628
|
};
|