@tillstack/electron 0.3.0 → 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 +206 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +207 -52
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
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
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var types = require('@tillstack/types');
|
|
6
6
|
var electron = require('electron');
|
|
7
7
|
var os = require('os');
|
|
8
|
+
var crypto = require('crypto');
|
|
8
9
|
var fs = require('fs');
|
|
9
10
|
var path = require('path');
|
|
10
11
|
|
|
@@ -32,31 +33,65 @@ var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
|
32
33
|
|
|
33
34
|
// src/index.ts
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
+
// ../scrub-core/dist/rules.js
|
|
36
37
|
var SCRUB_RULES = [
|
|
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:
|
|
53
|
-
{ 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 }
|
|
54
69
|
];
|
|
55
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
|
|
56
89
|
var MAX_REDACT_DEPTH = 24;
|
|
57
90
|
function redactByKey(value, depth = 0) {
|
|
58
|
-
if (depth > MAX_REDACT_DEPTH || value === null || typeof value !== "object")
|
|
59
|
-
|
|
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));
|
|
60
95
|
const out = {};
|
|
61
96
|
for (const [k, v] of Object.entries(value)) {
|
|
62
97
|
out[k] = SENSITIVE_KEY.test(k) ? "[redacted]" : redactByKey(v, depth + 1);
|
|
@@ -66,46 +101,115 @@ function redactByKey(value, depth = 0) {
|
|
|
66
101
|
function scrubEvent(event) {
|
|
67
102
|
try {
|
|
68
103
|
let json = JSON.stringify(redactByKey(event));
|
|
69
|
-
for (const
|
|
104
|
+
for (const rule of SCRUB_RULES)
|
|
105
|
+
json = applyRule(rule, json);
|
|
70
106
|
return JSON.parse(json);
|
|
71
107
|
} catch {
|
|
72
108
|
return event;
|
|
73
109
|
}
|
|
74
110
|
}
|
|
75
111
|
|
|
76
|
-
// src/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
function parseDsn(dsn) {
|
|
80
|
-
const m = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);
|
|
81
|
-
if (!m) throw new Error(`TillPulse: invalid DSN: ${dsn}`);
|
|
82
|
-
const scheme = m[1];
|
|
83
|
-
const host = m[3];
|
|
84
|
-
const local = /^(localhost|127\.0\.0\.1|::1)(:\d+)?$/i.test(host);
|
|
85
|
-
if (scheme !== "https" && !local) {
|
|
86
|
-
throw new Error("TillPulse: DSN must use https:// (refusing to send telemetry over cleartext http)");
|
|
87
|
-
}
|
|
88
|
-
return { scheme, host };
|
|
112
|
+
// src/scrub.ts
|
|
113
|
+
function scrubEvent2(event) {
|
|
114
|
+
return scrubEvent(event);
|
|
89
115
|
}
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
};
|
|
92
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;
|
|
93
157
|
var FileQueue = class {
|
|
94
|
-
constructor(
|
|
158
|
+
constructor() {
|
|
95
159
|
this.items = [];
|
|
96
|
-
this.
|
|
97
|
-
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);
|
|
98
169
|
try {
|
|
99
|
-
if (fs__namespace.existsSync(
|
|
100
|
-
|
|
101
|
-
|
|
170
|
+
if (fs__namespace.existsSync(legacy)) {
|
|
171
|
+
this.adopt(JSON.parse(fs__namespace.readFileSync(legacy, "utf8")), now);
|
|
172
|
+
fs__namespace.rmSync(legacy, { force: true });
|
|
102
173
|
}
|
|
103
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 });
|
|
104
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 });
|
|
199
|
+
}
|
|
200
|
+
if (this.items.length > MAX_ITEMS) this.items = this.items.slice(this.items.length - MAX_ITEMS);
|
|
105
201
|
}
|
|
106
202
|
persist() {
|
|
203
|
+
if (!this.dir || !this.codec) return;
|
|
204
|
+
const file = path__namespace.join(this.dir, FILE);
|
|
107
205
|
try {
|
|
108
|
-
|
|
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);
|
|
109
213
|
} catch {
|
|
110
214
|
}
|
|
111
215
|
}
|
|
@@ -116,13 +220,17 @@ var FileQueue = class {
|
|
|
116
220
|
} catch {
|
|
117
221
|
return;
|
|
118
222
|
}
|
|
119
|
-
|
|
120
|
-
|
|
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);
|
|
121
226
|
this.persist();
|
|
122
227
|
}
|
|
123
228
|
due(limit) {
|
|
124
229
|
const now = Date.now();
|
|
125
|
-
|
|
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;
|
|
126
234
|
}
|
|
127
235
|
ackOk(ids) {
|
|
128
236
|
const s = new Set(ids);
|
|
@@ -140,10 +248,45 @@ var FileQueue = class {
|
|
|
140
248
|
this.items = this.items.filter((i) => i.attempts <= 10);
|
|
141
249
|
this.persist();
|
|
142
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
|
+
}
|
|
143
258
|
size() {
|
|
144
259
|
return this.items.length;
|
|
145
260
|
}
|
|
146
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
|
+
}
|
|
147
290
|
function osName() {
|
|
148
291
|
if (process.platform === "darwin") return "macOS";
|
|
149
292
|
if (process.platform === "win32") return "Windows";
|
|
@@ -183,7 +326,19 @@ var TillPulseElectronSDK = class {
|
|
|
183
326
|
dir = electron.app.getPath("userData");
|
|
184
327
|
} catch {
|
|
185
328
|
}
|
|
186
|
-
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);
|
|
187
342
|
if (options.enableNativeCrashReporter !== false) {
|
|
188
343
|
try {
|
|
189
344
|
electron.crashReporter.start({
|
|
@@ -284,14 +439,12 @@ var TillPulseElectronSDK = class {
|
|
|
284
439
|
if (this.timer.unref) this.timer.unref();
|
|
285
440
|
if (options.debug) console.log("[TillPulse] electron SDK initialized", { release: options.release });
|
|
286
441
|
}
|
|
287
|
-
// ── Public API ──
|
|
288
442
|
captureException(error) {
|
|
289
443
|
this._captureError(error, "high", "captureException");
|
|
290
444
|
}
|
|
291
445
|
captureMessage(message, level = "info") {
|
|
292
446
|
this._enqueue(this._base("error", level, { exception: { type: "Message", value: message } }));
|
|
293
447
|
}
|
|
294
|
-
// Report a security signal to TillShield (same shape as the other SDKs).
|
|
295
448
|
captureSecurityEvent(input) {
|
|
296
449
|
this._enqueue(
|
|
297
450
|
this._base("security", input.severity ?? "high", {
|
|
@@ -322,6 +475,9 @@ var TillPulseElectronSDK = class {
|
|
|
322
475
|
getQueueStats() {
|
|
323
476
|
return { size: this.queue?.size() ?? 0 };
|
|
324
477
|
}
|
|
478
|
+
clearQueue() {
|
|
479
|
+
this.queue?.clear();
|
|
480
|
+
}
|
|
325
481
|
async flush(useKeepalive = false) {
|
|
326
482
|
if (!this.queue || !this.opts) return false;
|
|
327
483
|
const items = this.queue.due(50);
|
|
@@ -365,7 +521,6 @@ var TillPulseElectronSDK = class {
|
|
|
365
521
|
void this.flush(true);
|
|
366
522
|
this.initialized = false;
|
|
367
523
|
}
|
|
368
|
-
// ── Internals ──
|
|
369
524
|
_device() {
|
|
370
525
|
let total = 0;
|
|
371
526
|
let free = 0;
|
|
@@ -467,7 +622,7 @@ var TillPulseElectronSDK = class {
|
|
|
467
622
|
}
|
|
468
623
|
}
|
|
469
624
|
if (!final) return;
|
|
470
|
-
if (!o.disableDefaultPiiScrubbing) final =
|
|
625
|
+
if (!o.disableDefaultPiiScrubbing) final = scrubEvent2(final);
|
|
471
626
|
this.queue.enqueue(final.event_id, final.event_type, final);
|
|
472
627
|
}
|
|
473
628
|
};
|