@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 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
- // src/scrub.ts
36
+ // ../scrub-core/dist/rules.js
36
37
  var SCRUB_RULES = [
37
- { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[aws_key]" },
38
- { pattern: /(?:aws_secret|secret_access_key|AWS_SECRET)[^A-Za-z0-9+/]*([A-Za-z0-9+/]{40})\b/gi, replacement: "[aws_secret]" },
39
- { pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: "[gcp_key]" },
40
- { pattern: /\b(?:ghp|gho|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[github_token]" },
41
- { pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g, replacement: "[github_token]" },
42
- { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
43
- { pattern: /\bxapp-\d-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
44
- { pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, replacement: "[jwt]" },
45
- { pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: "Bearer [token]" },
46
- { pattern: /\b(?:sk|pk|ak|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, replacement: "[apikey]" },
47
- { pattern: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g, replacement: "[email]" },
48
- { pattern: /\b[A-Z0-9]{10}\b(?=\s*confirmed|\s*transaction|\s*M-?Pesa)/gi, replacement: "[mpesa_tx]" },
49
- { pattern: /\b(?:\d[ -]*?){13,19}\b/g, replacement: "[card]" },
50
- { pattern: /\b\d{13}\b/g, replacement: "[id_number]" },
51
- { pattern: /\b\d{11}\b/g, replacement: "[id_number]" },
52
- { pattern: /\+?\d{1,3}[\s.-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}\b/g, replacement: "[phone]" },
53
- { pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[ip]" }
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") return value;
59
- if (Array.isArray(value)) return value.map((v) => redactByKey(v, depth + 1));
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 { pattern, replacement } of SCRUB_RULES) json = json.replace(pattern, replacement);
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/index.ts
77
- var SDK_NAME = "@tillstack/electron";
78
- var SDK_VERSION = "0.3.0" ;
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
- function generateId() {
91
- return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
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(dir) {
158
+ constructor() {
95
159
  this.items = [];
96
- this.max = 500;
97
- this.file = path__namespace.join(dir, "tillpulse-queue.json");
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(this.file)) {
100
- const parsed = JSON.parse(fs__namespace.readFileSync(this.file, "utf8"));
101
- if (Array.isArray(parsed)) this.items = parsed;
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
- fs__namespace.writeFileSync(this.file, JSON.stringify(this.items));
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
- this.items.push({ id, event_type: eventType, payload, attempts: 0, next: 0 });
120
- if (this.items.length > this.max) this.items = this.items.slice(this.items.length - this.max);
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
- return this.items.filter((i) => i.next <= now).slice(0, limit);
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(dir);
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 = scrubEvent(final);
625
+ if (!o.disableDefaultPiiScrubbing) final = scrubEvent2(final);
471
626
  this.queue.enqueue(final.event_id, final.event_type, final);
472
627
  }
473
628
  };