@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 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
- // src/scrub.ts
36
+ // ../scrub-core/dist/rules.js
35
37
  var SCRUB_RULES = [
36
- { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[aws_key]" },
37
- { pattern: /(?:aws_secret|secret_access_key|AWS_SECRET)[^A-Za-z0-9+/]*([A-Za-z0-9+/]{40})\b/gi, replacement: "[aws_secret]" },
38
- { pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: "[gcp_key]" },
39
- { pattern: /\b(?:ghp|gho|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[github_token]" },
40
- { pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g, replacement: "[github_token]" },
41
- { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
42
- { pattern: /\bxapp-\d-[A-Za-z0-9-]{10,}\b/g, replacement: "[slack_token]" },
43
- { pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, replacement: "[jwt]" },
44
- { pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: "Bearer [token]" },
45
- { pattern: /\b(?:sk|pk|ak|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, replacement: "[apikey]" },
46
- { pattern: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g, replacement: "[email]" },
47
- { pattern: /\b[A-Z0-9]{10}\b(?=\s*confirmed|\s*transaction|\s*M-?Pesa)/gi, replacement: "[mpesa_tx]" },
48
- { pattern: /\b(?:\d[ -]*?){13,19}\b/g, replacement: "[card]" },
49
- { pattern: /\b\d{13}\b/g, replacement: "[id_number]" },
50
- { pattern: /\b\d{11}\b/g, replacement: "[id_number]" },
51
- { pattern: /\+?\d{1,3}[\s.-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}\b/g, replacement: "[phone]" },
52
- { 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 }
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") return value;
58
- 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));
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 { pattern, replacement } of SCRUB_RULES) json = json.replace(pattern, replacement);
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/index.ts
76
- var SDK_NAME = "@tillstack/electron";
77
- var SDK_VERSION = "0.1.0";
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
- function generateId() {
90
- 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
+ };
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(dir) {
158
+ constructor() {
94
159
  this.items = [];
95
- this.max = 500;
96
- 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);
97
169
  try {
98
- if (fs__namespace.existsSync(this.file)) {
99
- const parsed = JSON.parse(fs__namespace.readFileSync(this.file, "utf8"));
100
- 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 });
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
- 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);
108
213
  } catch {
109
214
  }
110
215
  }
@@ -115,13 +220,17 @@ var FileQueue = class {
115
220
  } catch {
116
221
  return;
117
222
  }
118
- this.items.push({ id, event_type: eventType, payload, attempts: 0, next: 0 });
119
- 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);
120
226
  this.persist();
121
227
  }
122
228
  due(limit) {
123
229
  const now = Date.now();
124
- 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;
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(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);
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
- if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) {
344
- this.queue.ackOk(ids);
345
- return true;
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.ackFail(ids);
348
- return false;
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 = scrubEvent(final);
625
+ if (!o.disableDefaultPiiScrubbing) final = scrubEvent2(final);
466
626
  this.queue.enqueue(final.event_id, final.event_type, final);
467
627
  }
468
628
  };