@extenshi/mcp
Version:
Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools
187 lines (172 loc) • 7.74 kB
JavaScript
'use strict'
/**
* Anonymous install ping — VENDORED, kept byte-identical between @extenshi/cli
* and @extenshi/mcp (it figures out which one it is from its own package.json).
*
* Fires a single `cli_installed` / `mcp_installed` event when the package is
* installed as a real dependency ON A MACHINE THAT HAS ALREADY ACCEPTED
* telemetry, so we can see adoption in PostHog. It is a standalone CommonJS
* script using ONLY Node built-ins (no posthog-node, no build step) so it can
* run the instant npm extracts the tarball.
*
* Hard rules (a postinstall that breaks `npm install` is unacceptable):
* - ALWAYS exits 0, even on any error — wrapped in try/catch + a hard timeout.
* - Fully fail-soft and fire-and-forget; the request is capped at ~2s.
*
* It self-skips (no event, no network) when:
* - the package is NOT under node_modules — i.e. our own monorepo workspace
* install (tools/<pkg>), so dev installs never inflate adoption counts;
* - ~/.extenshi/config.json does not hold "telemetry": true — consent must be
* RECORDED, not merely un-refused. A postinstall runs before the CLI can
* ask anything, so "no answer yet" is not permission (see the ACCEPTED-ONLY
* note below);
* - DO_NOT_TRACK is truthy or EXTENSHI_TELEMETRY is off;
* - CI is set (CI re-installs would massively inflate the count);
* - no ingestion key is configured.
*
* Privacy: sends only the package version, Node version, coarse os/arch, a
* first-install-on-this-machine boolean, and an anonymous per-install UUID (the
* same id the runtime telemetry uses). Never any path, package contents, or
* user input.
*
* ACCEPTED-ONLY, and what that costs. Until 2026-08-13 this fired unless the
* user had actively opted out, which meant a first-ever install reported before
* anyone had been asked — the one place consent did not reach after the CLI
* grew a first-run question (tools/extenshi-cli/src/telemetry-consent.ts). The
* gate is now positive, and the consequences are deliberate:
* - a FIRST install never pings. Only the CLI's first-run question writes
* `telemetry: true`, and that happens on the first RUN, so the earliest
* ping is the next upgrade of an accepted machine.
* - @extenshi/mcp is headless and never prompts, so it can only ping on a
* machine where the CLI recorded consent into the shared config. Treat its
* install count as gone, not as a number that dropped.
* - `first_extenshi_install` is therefore ~always false now. It is kept so the
* event schema does not change under existing PostHog queries, not because
* it still measures anything.
*
* NB for anyone reading this as an adoption metric: it counts npm EXTRACTIONS,
* not users. Most of them are ecosystem scanners that install a new version once
* on a throwaway machine and never run the binary. The "did a client actually
* launch the server" signal is `mcp_server_started`, emitted from
* @extenshi/mcp's src/startup.ts — a relative path would be wrong in one of the
* two packages, since this file is vendored byte-identical into both.
*/
// Shared EU PostHog project key (114791) — public, write-only, already in our
// web bundles. Override with EXTENSHI_POSTHOG_KEY; empty disables the ping.
// NB: this line's NUMBER is pinned in .infisicalignore
// (`…/postinstall.cjs:generic-api-key:<line>`) — gitleaks fingerprints include
// it, so ADDING OR REMOVING LINES ABOVE HERE breaks the allowlist and turns the
// key into a CI secret-scan failure. Move the fingerprint with it.
const EMBEDDED_KEY = 'phc_fqKrAmtNZvJqe0krpYB3UwYqALLpT1WM8m5LtNs9eUu'
const DEFAULT_HOST = 'https://eu.i.posthog.com'
/**
* Every guard below calls this as `return done()`, not as a bare statement. The
* process.exit is what actually stops the script, but the `return` is what keeps
* the early exit true if this ever stops exiting synchronously — without it, a
* deferred done() would let the code past the consent gate run on a machine that
* never agreed to be measured.
*/
function done() {
process.exit(0)
}
try {
const path = require('node:path')
const fs = require('node:fs')
const os = require('node:os')
const https = require('node:https')
const { randomUUID } = require('node:crypto')
// Real dependency install only — workspace path has no /node_modules/ segment.
if (!__dirname.includes(`${path.sep}node_modules${path.sep}`)) return done()
// CI re-installs would inflate the adoption count — skip them.
if (process.env.CI) return done()
const key = (process.env.EXTENSHI_POSTHOG_KEY || EMBEDDED_KEY).trim()
if (!key) return done()
const dnt = String(process.env.DO_NOT_TRACK || '')
.trim()
.toLowerCase()
if (['1', 'true', 'yes', 'on'].includes(dnt)) return done()
const optOut = String(process.env.EXTENSHI_TELEMETRY || '')
.trim()
.toLowerCase()
if (['0', 'false', 'off', 'no'].includes(optOut)) return done()
// Anonymous per-install id, shared with the runtime telemetry module.
const configPath = path.join(os.homedir(), '.extenshi', 'config.json')
let cfg = {}
try {
cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'))
} catch {}
// Positive consent only. An install happens before the CLI can put the
// question, so an absent answer is "not asked yet", never "yes" — this exits
// for a machine that declined AND for one that has never been asked. Every
// line below this point, including minting the anonymous id, is reached only
// with a recorded `true`, which is what stops a bare `npx` from creating an
// identity on a machine that never agreed to one.
if (!cfg || cfg.telemetry !== true) return done()
let anonId = cfg.anonId
// Retained for event-schema stability, but it no longer measures anything:
// accepting the first-run question writes `telemetry: true` and then emits an
// event, which mints the id — so by the time this gate opens, an anonId is
// already there and this is false. It stays true only in the odd case of a
// config that kept the answer but lost the id. Before the accepted-only gate
// it was the new-machine-vs-upgrade discriminator described above.
const firstExtenshiInstall = !anonId
if (!anonId) {
anonId = randomUUID()
try {
fs.mkdirSync(path.dirname(configPath), { recursive: true })
fs.writeFileSync(configPath, `${JSON.stringify({ ...cfg, anonId }, null, 2)}\n`, { mode: 0o600 })
} catch {}
}
// Identify the surface + version from our own package.json.
let pkg = {}
try {
pkg = require(path.join(__dirname, '..', 'package.json'))
} catch {}
const surface = String(pkg.name || '').includes('mcp') ? 'mcp' : 'cli'
const host = (process.env.EXTENSHI_POSTHOG_HOST || DEFAULT_HOST).trim()
const payload = JSON.stringify({
api_key: key,
event: `${surface}_installed`,
distinct_id: anonId,
properties: {
surface,
version: pkg.version || '0.0.0',
node: process.version,
os: process.platform,
arch: process.arch,
ci: false,
source: 'npm_postinstall',
first_extenshi_install: firstExtenshiInstall,
},
})
const url = new URL('/i/v0/e/', host)
const req = https.request(
{
method: 'POST',
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) },
timeout: 2000,
},
(res) => {
res.on('data', () => {})
res.on('end', done)
res.on('error', done)
},
)
req.on('error', done)
req.on('timeout', () => {
try {
req.destroy()
} catch {}
done()
})
req.write(payload)
req.end()
// Backstop: never let the ping hold the install open.
const guard = setTimeout(done, 2500)
if (guard.unref) guard.unref()
} catch {
done()
}