exe-watcher 0.2.70 → 0.2.72
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 +22 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/cli.js +928 -329
- package/package.json +4 -2
- package/vendor/odd-core-analytics/LICENSE +21 -0
- package/vendor/odd-core-analytics/PROVENANCE.json +28 -0
- package/vendor/odd-core-analytics/README.md +196 -0
- package/vendor/odd-core-analytics/dist/privacy/client.d.ts +58 -0
- package/vendor/odd-core-analytics/dist/privacy/client.js +195 -0
- package/vendor/odd-core-analytics/dist/privacy/index.d.ts +2 -0
- package/vendor/odd-core-analytics/dist/privacy/index.js +18 -0
- package/vendor/odd-core-analytics/dist/privacy/schema.d.ts +46 -0
- package/vendor/odd-core-analytics/dist/privacy/schema.js +82 -0
- package/vendor/odd-core-analytics/gateway/server.mjs +128 -0
- package/vendor/odd-core-analytics/package.json +13 -0
|
@@ -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
|
+
}
|