exe-watcher 0.2.70 → 0.2.71

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.
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActiveClock = exports.UUID = exports.PLATFORMS = exports.DIAGNOSTICS_DISCLOSURE = exports.USAGE_DISCLOSURE = exports.DEFAULT_FRAMES = exports.ERROR_CATEGORIES = exports.HANSEL_FEATURES = exports.DEFAULT_COLLECTOR = void 0;
4
+ exports.record = record;
5
+ exports.sanitizeMetric = sanitizeMetric;
6
+ exports.sanitizeDiagnostic = sanitizeDiagnostic;
7
+ exports.collectorOrigin = collectorOrigin;
8
+ /** MIT. Counters and structural errors only; never forward caller property bags. */
9
+ exports.DEFAULT_COLLECTOR = 'https://data.odd.community';
10
+ exports.HANSEL_FEATURES = ['home', 'recordings', 'vocabulary', 'shortcuts', 'dictation'];
11
+ exports.ERROR_CATEGORIES = ['crash', 'unhandled_exception', 'render_error', 'ipc_error', 'ml_error'];
12
+ exports.DEFAULT_FRAMES = ['app:///main/index.js', 'app:///renderer/index.js'];
13
+ exports.USAGE_DISCLOSURE = 'Share active time, recording duration, feature counts and outcomes, app version and platform with our own servers. A random ID unique to this app measures returning installations. Never audio, transcripts, prompts, account details or screen recordings.';
14
+ exports.DIAGNOSTICS_DISCLOSURE = 'Share sanitized error categories, app version and code locations with our own Sentry server. Never error text, account details, recordings or memory dumps.';
15
+ exports.PLATFORMS = ['macos', 'windows', 'linux', 'ios', 'android', 'web'];
16
+ exports.UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ const CLASSES = ['Error', 'TypeError', 'RangeError', 'ReferenceError', 'SyntaxError', 'URIError', 'EvalError', 'AggregateError'];
18
+ function record(input) {
19
+ return input !== null && typeof input === 'object' && !Array.isArray(input) ? input : null;
20
+ }
21
+ function sanitizeMetric(input, features = exports.HANSEL_FEATURES) {
22
+ const v = record(input);
23
+ if (!v)
24
+ return null;
25
+ if (v.event === 'active')
26
+ return { event: 'active' };
27
+ if (v.event === 'feature' && typeof v.feature === 'string' && features.includes(v.feature))
28
+ return { event: 'feature', feature: v.feature };
29
+ if (v.event === 'result' && typeof v.feature === 'string' && features.includes(v.feature) && (v.outcome === 'success' || v.outcome === 'failure'))
30
+ return { event: 'result', feature: v.feature, outcome: v.outcome };
31
+ if (v.event === 'usage' && Number.isInteger(v.active_ms) && Number(v.active_ms) > 0 && Number(v.active_ms) <= 60000)
32
+ return { event: 'usage', active_ms: Number(v.active_ms) };
33
+ if (v.event === 'recording' && Number.isInteger(v.recording_ms) && Number(v.recording_ms) > 0 && Number(v.recording_ms) <= 86400000)
34
+ return { event: 'recording', recording_ms: Number(v.recording_ms) };
35
+ return null;
36
+ }
37
+ function sanitizeDiagnostic(input, allowedFrames = exports.DEFAULT_FRAMES) {
38
+ const v = record(input);
39
+ if (!v || v.event !== 'error' || !exports.ERROR_CATEGORIES.includes(v.category))
40
+ return null;
41
+ const frames = [];
42
+ if (Array.isArray(v.frames))
43
+ for (const inputFrame of v.frames.slice(-8)) {
44
+ const f = record(inputFrame);
45
+ if (f && typeof f.filename === 'string' && allowedFrames.includes(f.filename) &&
46
+ Number.isInteger(f.lineno) && Number(f.lineno) > 0 && Number(f.lineno) <= 10000000 &&
47
+ Number.isInteger(f.colno) && Number(f.colno) >= 0 && Number(f.colno) <= 10000000) {
48
+ frames.push({ filename: f.filename, lineno: Number(f.lineno), colno: Number(f.colno) });
49
+ }
50
+ }
51
+ return { event: 'error', category: v.category,
52
+ error_class: typeof v.error_class === 'string' && CLASSES.includes(v.error_class) ? v.error_class : 'Error',
53
+ version: typeof v.version === 'string' && /^\d{1,4}\.\d{1,4}\.\d{1,4}$/.test(v.version) ? v.version : '0.0.0', frames };
54
+ }
55
+ function collectorOrigin(input) {
56
+ const u = new URL(input);
57
+ if (u.protocol !== 'https:' || u.username || u.password || u.pathname !== '/' || u.search || u.hash ||
58
+ /(^|\.)(posthog\.com|posthog\.net|google\.com|googleapis\.com|google-analytics\.com|googletagmanager\.com|firebaseapp\.com|firebaseio\.com)$/.test(u.hostname))
59
+ throw new Error('Use an operator-controlled HTTPS collector origin');
60
+ return u.origin;
61
+ }
62
+ /** Sample with a monotonic clock; reset at consent/focus lifecycle boundaries. */
63
+ class ActiveClock {
64
+ constructor() {
65
+ this.previous = null;
66
+ }
67
+ sample(time, focused, idleSeconds, recording = false) {
68
+ if (!Number.isFinite(time) || !Number.isFinite(idleSeconds) || idleSeconds < 0) {
69
+ this.reset();
70
+ return 0;
71
+ }
72
+ const active = recording || (focused && idleSeconds < 60);
73
+ const previous = this.previous;
74
+ this.previous = { time, active };
75
+ if (!previous || !previous.active || !active)
76
+ return 0;
77
+ const delta = time - previous.time;
78
+ return delta > 0 && delta <= 15000 ? Math.floor(delta) : 0;
79
+ }
80
+ reset() { this.previous = null; }
81
+ }
82
+ exports.ActiveClock = ActiveClock;
@@ -0,0 +1,128 @@
1
+ /** MIT licensed. Restricted public intake for a private PostHog instance. */
2
+ import { createServer } from 'node:http'
3
+ import { readFileSync } from 'node:fs'
4
+ import { pathToFileURL } from 'node:url'
5
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
6
+ const features = new Set(['home', 'recordings', 'vocabulary', 'shortcuts', 'dictation'])
7
+
8
+ export function sanitizeBatch(body, tokens) {
9
+ const project = typeof body?.api_key === 'string' && Object.hasOwn(tokens, body.api_key) ? tokens[body.api_key] : null
10
+ if (!project || typeof project.token !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(project.app) || !Array.isArray(body.batch) || body.batch.length < 1 || body.batch.length > 50) return null
11
+ const batch = []
12
+ for (const row of body.batch) {
13
+ const p = row?.properties
14
+ const time = Date.parse(row?.timestamp)
15
+ if (typeof row?.uuid !== 'string' || !uuid.test(row.uuid) || typeof p?.distinct_id !== 'string' || !uuid.test(p.distinct_id) || !Number.isFinite(time) ||
16
+ time > Date.now() + 300000 || Date.now() - time > 7 * 86400000 || p.app !== project.app) return null
17
+ let metric
18
+ if (row.event === 'app_active') metric = {}
19
+ else if (row.event === 'app_usage' && Number.isInteger(p.active_ms) && p.active_ms > 0 && p.active_ms <= 60000) metric = { active_ms: p.active_ms }
20
+ else if (row.event === 'app_recording' && Number.isInteger(p.recording_ms) && p.recording_ms > 0 && p.recording_ms <= 86400000) metric = { recording_ms: p.recording_ms }
21
+ else if (row.event === 'app_feature' && (project.features ? project.features.includes(p.feature) : features.has(p.feature))) metric = { feature: p.feature }
22
+ else if (row.event === 'app_result' && (project.features ? project.features.includes(p.feature) : features.has(p.feature)) && ['success', 'failure'].includes(p.outcome)) metric = { feature: p.feature, outcome: p.outcome }
23
+ else return null
24
+ const metadata = {}
25
+ if (typeof p.app_version === 'string' && /^\d{1,4}\.\d{1,4}\.\d{1,4}$/.test(p.app_version)) metadata.app_version = p.app_version
26
+ if (['macos', 'windows', 'linux', 'ios', 'android', 'web'].includes(p.app_platform)) metadata.app_platform = p.app_platform
27
+ batch.push({ uuid: row.uuid, timestamp: new Date(time).toISOString(), event: row.event,
28
+ properties: { distinct_id: p.distinct_id, app: project.app, ...metric, ...metadata,
29
+ $process_person_profile: false, $geoip_disable: true } })
30
+ }
31
+ return { api_key: project.token, batch }
32
+ }
33
+
34
+ export function sanitizeDiagnostics(body, projects) {
35
+ const project = typeof body?.api_key === 'string' && Object.hasOwn(projects, body.api_key) ? projects[body.api_key] : null
36
+ if (!project || !/^[a-z][a-z0-9-]{0,63}$/.test(project.app) || !Number.isInteger(project.project_id) || project.project_id < 1 ||
37
+ typeof project.public_key !== 'string' || !/^[a-f0-9]{32}$/.test(project.public_key) ||
38
+ !Array.isArray(body.batch) || body.batch.length < 1 || body.batch.length > 10) return null
39
+ const categories = new Set(['crash', 'unhandled_exception', 'render_error', 'ipc_error', 'ml_error'])
40
+ const classes = new Set(['Error', 'TypeError', 'RangeError', 'ReferenceError', 'SyntaxError', 'URIError', 'EvalError', 'AggregateError'])
41
+ const events = []
42
+ for (const row of body.batch) {
43
+ const d = row?.diagnostic, time = Date.parse(row?.timestamp)
44
+ if (typeof row?.uuid !== 'string' || !uuid.test(row.uuid) || !Number.isFinite(time) ||
45
+ time > Date.now() + 300000 || Date.now() - time > 7 * 86400000 || d?.event !== 'error' || !categories.has(d.category)) return null
46
+ const frames = []
47
+ if (Array.isArray(d.frames)) for (const f of d.frames.slice(-8)) {
48
+ if (f && (project.frames || ['app:///main/index.js', 'app:///renderer/index.js']).includes(f.filename) &&
49
+ Number.isInteger(f.lineno) && f.lineno > 0 && f.lineno <= 10000000 &&
50
+ Number.isInteger(f.colno) && f.colno >= 0 && f.colno <= 10000000) {
51
+ frames.push({ filename: f.filename, lineno: f.lineno, colno: f.colno, in_app: true })
52
+ }
53
+ }
54
+ events.push({ event_id: row.uuid.replaceAll('-', ''), timestamp: new Date(time).toISOString(), platform: ['javascript', 'cocoa', 'java', 'other'].includes(project.platform) ? project.platform : 'javascript', level: 'error',
55
+ release: project.app + '@' + (typeof d.version === 'string' && /^\d{1,4}\.\d{1,4}\.\d{1,4}$/.test(d.version) ? d.version : '0.0.0'),
56
+ exception: { values: [{ type: classes.has(d.error_class) ? d.error_class : 'Error', value: project.app + ' ' + d.category,
57
+ ...(frames.length ? { stacktrace: { frames } } : {}) }] }, tags: { category: d.category, app: project.app } })
58
+ }
59
+ return { project, events }
60
+ }
61
+
62
+ export function makeServer({ upstream, tokens, diagnostics = {}, sentryOrigin = 'http://sentry-ingress', allowedOrigins = [], send = fetch }) {
63
+ let windowStart = Date.now(), requests = 0
64
+ const server = createServer(async (req, res) => {
65
+ res.setHeader('Cache-Control', 'no-store')
66
+ const origin = req.headers.origin
67
+ if (origin && !allowedOrigins.includes(origin)) { res.writeHead(403); res.end(); return }
68
+ if (origin) {
69
+ res.setHeader('Access-Control-Allow-Origin', origin)
70
+ res.setHeader('Vary', 'Origin')
71
+ if (req.method === 'OPTIONS' && ['/batch/', '/errors/'].includes(req.url)) {
72
+ res.setHeader('Access-Control-Allow-Methods', 'POST')
73
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
74
+ res.writeHead(204); res.end(); return
75
+ }
76
+ }
77
+ if (req.method === 'GET' && req.url === '/health') { res.writeHead(200); res.end('ok'); return }
78
+ if (req.method !== 'POST' || !['/batch/', '/errors/'].includes(req.url)) { res.writeHead(404); res.end(); return }
79
+ if (!req.headers['content-type']?.startsWith('application/json')) { res.writeHead(415); res.end(); return }
80
+ if (Date.now() - windowStart > 60000) { windowStart = Date.now(); requests = 0 }
81
+ if (++requests > 3000) { res.writeHead(429, { 'Retry-After': '60' }); res.end(); return }
82
+ try {
83
+ const chunks = []; let length = 0
84
+ for await (const chunk of req) {
85
+ length += chunk.length
86
+ if (length > 65536) { res.writeHead(413); res.end(); return }
87
+ chunks.push(chunk)
88
+ }
89
+ const input = JSON.parse(Buffer.concat(chunks).toString('utf8'))
90
+ if (req.url === '/errors/') {
91
+ const payload = sanitizeDiagnostics(input, diagnostics)
92
+ if (!payload) { res.writeHead(400); res.end(); return }
93
+ const results = await Promise.all(payload.events.map(async event => {
94
+ const response = await send(`${sentryOrigin}/api/${payload.project.project_id}/store/`, {
95
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(4000),
96
+ headers: { 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=7,sentry_key=' + payload.project.public_key },
97
+ body: JSON.stringify(event),
98
+ })
99
+ await response.body?.cancel()
100
+ return response.ok
101
+ }))
102
+ res.writeHead(results.every(Boolean) ? 200 : 503); res.end(); return
103
+ }
104
+ const payload = sanitizeBatch(input, tokens)
105
+ if (!payload) { res.writeHead(400); res.end(); return }
106
+ const response = await send(upstream, {
107
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(4000),
108
+ // Never forward source IP, cookies, auth, user agent, or incoming headers.
109
+ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
110
+ })
111
+ await response.body?.cancel()
112
+ res.writeHead(response.ok ? 200 : 503); res.end()
113
+ } catch { res.writeHead(503); res.end() }
114
+ })
115
+ server.requestTimeout = 10000; server.headersTimeout = 10000
116
+ server.maxConnections = 64
117
+ return server
118
+ }
119
+
120
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
121
+ const tokens = JSON.parse(readFileSync(process.env.PROJECT_TOKENS_FILE, 'utf8'))
122
+ const upstream = new URL(process.env.POSTHOG_UPSTREAM || 'http://proxy:80/batch/')
123
+ if (!['proxy', '127.0.0.1', 'localhost'].includes(upstream.hostname)) throw Error('Upstream must be the private local PostHog service')
124
+ const diagnostics = process.env.SENTRY_PROJECTS_FILE ? JSON.parse(readFileSync(process.env.SENTRY_PROJECTS_FILE, 'utf8')) : {}
125
+ const sentry = new URL(process.env.SENTRY_UPSTREAM || 'http://sentry-ingress')
126
+ if (!['sentry-ingress', '127.0.0.1', 'localhost'].includes(sentry.hostname) || sentry.pathname !== '/' || sentry.username || sentry.password || sentry.search || sentry.hash) throw Error('Invalid private Sentry origin')
127
+ makeServer({ upstream: upstream.href, tokens, diagnostics, sentryOrigin: sentry.origin, allowedOrigins: JSON.parse(process.env.ALLOWED_ORIGINS || '[]') }).listen(8080, '0.0.0.0')
128
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@odd-core/analytics",
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "description": "Pinned Odd Core privacy and gateway entry points",
6
+ "exports": {
7
+ "./privacy": {
8
+ "types": "./dist/privacy/index.d.ts",
9
+ "default": "./dist/privacy/index.js"
10
+ },
11
+ "./gateway": "./gateway/server.mjs"
12
+ }
13
+ }