fingerprint-platform-mcp 0.0.1

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 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/tools/analyze.ts","../../../packages/sdk-snippets/src/snippet-for-framework.ts","../src/util/detect-framework.ts","../src/util/detect-pm.ts","../src/tools/configure.ts","../src/util/diff.ts","../src/tools/explain.ts","../src/tools/install.ts","../src/tools/verify.ts","../src/tools/wire-init.ts"],"sourcesContent":["import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nimport { buildServer } from './server.js';\n\n/**\n * Stdio entrypoint for `fingerprint-platform-mcp`.\n *\n * MCP clients (Claude Desktop, Claude Code, Cursor, Continue.dev,\n * Copilot Chat via mcp-remote) spawn this binary as a subprocess\n * and speak JSON-RPC 2.0 over its stdin / stdout. We attach the\n * server to a `StdioServerTransport` and run until the parent\n * closes the pipes.\n *\n * Stderr is reserved for diagnostics (`console.error`) — MCP\n * clients route it to their own log viewer. Never log to stdout\n * here: that's the JSON-RPC channel and any stray bytes desync\n * the protocol.\n */\nasync function main(): Promise<void> {\n const server = buildServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n // McpServer.connect() resolves once the transport is bound. The\n // process stays alive because stdin holds the event loop open\n // — when the client closes its half of the pipe, Node's stdin\n // emits 'end' and the process exits naturally.\n}\n\nmain().catch((err) => {\n // Last-resort error path — anything that escapes the per-tool\n // try/catch lands here. Log to stderr (NEVER stdout) and exit\n // non-zero so the client's MCP supervisor logs it as a crash.\n console.error('[fingerprint-platform-mcp] fatal:', err);\n process.exit(1);\n});\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nimport { analyzeInputSchema, analyzeProject } from './tools/analyze.js';\nimport {\n generateCloudflareWorker,\n generateCloudflareWorkerInputSchema,\n generateDnsFallback,\n generateDnsFallbackInputSchema,\n setCollectorUrl,\n setCollectorUrlInputSchema,\n} from './tools/configure.js';\nimport { explainInputSchema, explainTopic } from './tools/explain.js';\nimport { installInputSchema, installSdk } from './tools/install.js';\nimport { verifyInstall, verifyInstallInputSchema } from './tools/verify.js';\nimport { wireInit, wireInitInputSchema } from './tools/wire-init.js';\n\nimport type { ToolEnvelope } from './tools/types.js';\n\n/**\n * Build the Fingerprint Platform MCP server. Pure factory — no\n * side effects (no transport bind, no FS open). The caller picks\n * a transport (stdio in `cli.ts`, eventually HTTP/SSE) and calls\n * `server.connect(transport)`.\n *\n * Why a factory instead of a singleton: easier to spin up a fresh\n * server per test in the e2e suite without poisoning the registry\n * with leftover tools.\n */\nexport function buildServer(): McpServer {\n const server = new McpServer(\n {\n name: 'fingerprint-platform-mcp',\n version: '0.0.1',\n },\n {\n capabilities: {\n // We advertise tools only — no resources/prompts in v1. The\n // SDK auto-fills the capability object from the registrations\n // below, so we just declare an empty placeholder to be\n // explicit about the shape.\n tools: {},\n },\n instructions:\n 'Use these tools to install and configure the Fingerprint Platform SDK ' +\n '(`fingerprint-platform-sdk`) in a user project. Standard flow: ' +\n '`analyze_project` → `install_sdk` → `wire_init` → `set_collector_url` → ' +\n '`generate_cloudflare_worker` (recommended) or `generate_dns_fallback` → ' +\n '`verify_install`. Every tool is read-only on the filesystem and returns ' +\n 'a diff/command/env spec for the assistant to apply with its own tools. ' +\n 'Use `explain_topic` to fetch a focused brief on origin-allowlist, ' +\n 'cloudflare-proxy, dns-cname, api-keys, events-vs-identify, sync-vs-async.',\n },\n );\n\n // Helper to serialise our ToolEnvelope into the MCP CallToolResult\n // shape. We pack the structured fields (artifact / meta / nextSteps)\n // into a single markdown text block — that's what every current\n // MCP client renders directly, and it lets the AI parse the\n // structured content via simple regex/section-split prompts.\n function render(envelope: ToolEnvelope): {\n content: { type: 'text'; text: string }[];\n } {\n const parts: string[] = [envelope.summary];\n if (envelope.details) parts.push(envelope.details);\n if (envelope.nextSteps && envelope.nextSteps.length > 0) {\n parts.push(\n '## Next steps\\n\\n' + envelope.nextSteps.map((s, i) => `${i + 1}. ${s}`).join('\\n'),\n );\n }\n if (envelope.artifact) {\n switch (envelope.artifact.kind) {\n case 'diff':\n parts.push('## Diff\\n\\n```diff\\n' + envelope.artifact.content + '\\n```');\n break;\n case 'command':\n parts.push(\n '## Command\\n\\n```bash\\n' +\n envelope.artifact.content +\n '\\n```' +\n (envelope.artifact.cwd ? `\\n_(run in \\`${envelope.artifact.cwd}\\`)_` : ''),\n );\n break;\n case 'env':\n parts.push(\n '## Env vars\\n\\n```bash\\n' +\n Object.entries(envelope.artifact.content)\n .map(([k, v]) => `${k}=${v}`)\n .join('\\n') +\n '\\n```',\n );\n break;\n case 'markdown':\n // already in details when used; no double-render.\n break;\n }\n }\n return { content: [{ type: 'text', text: parts.join('\\n\\n') }] };\n }\n\n server.registerTool(\n 'analyze_project',\n {\n description:\n 'Scan the user project at `cwd` — read package.json + lockfile + framework config — to determine framework / package manager / entry file / whether the SDK is already installed. Read-only. Always call this first.',\n inputSchema: analyzeInputSchema,\n },\n async (args) => render(analyzeProject(args)),\n );\n\n server.registerTool(\n 'install_sdk',\n {\n description:\n 'Emit the install command for `fingerprint-platform-sdk` using the detected (or passed) package manager. Does NOT execute — the AI runs it via its own bash tool.',\n inputSchema: installInputSchema,\n },\n async (args) =>\n render(\n installSdk({\n cwd: args.cwd,\n source: args.source ?? 'npm',\n ...(args.packageManager ? { packageManager: args.packageManager } : {}),\n }),\n ),\n );\n\n server.registerTool(\n 'wire_init',\n {\n description:\n 'Generate a unified diff that places the framework-idiomatic SDK init() call into the right entry file. Reuses `initSnippet()` from `@fingerprint79/sdk-snippets` so the code matches what the dashboard shows.',\n inputSchema: wireInitInputSchema,\n },\n async (args) =>\n render(\n wireInit({\n cwd: args.cwd,\n framework: args.framework,\n entryFile: args.entryFile,\n apiKey: args.apiKey,\n collectorUrl: args.collectorUrl,\n source: args.source ?? 'npm',\n }),\n ),\n );\n\n server.registerTool(\n 'set_collector_url',\n {\n description:\n 'Validate a collector URL and return (a) env-var spec to add to `.env.local` and (b) the dashboard `allowedHosts` change the operator must make manually. The MCP server cannot mutate the dashboard in v1.',\n inputSchema: setCollectorUrlInputSchema,\n },\n async (args) => render(setCollectorUrl(args)),\n );\n\n server.registerTool(\n 'generate_cloudflare_worker',\n {\n description:\n 'Emit a `cloudflare/worker.ts` + `cloudflare/wrangler.toml` as diffs to drop into the user repo. Worker proxies SDK bundle + collector API under `<customDomain>/fpjs/*` for same-origin, ad-block-resistant delivery.',\n inputSchema: generateCloudflareWorkerInputSchema,\n },\n async (args) => render(generateCloudflareWorker(args)),\n );\n\n server.registerTool(\n 'generate_dns_fallback',\n {\n description:\n 'When Cloudflare is not an option, emit DNS CNAME instructions pointing a subdomain at the collector. Keeps cookies first-party but is more ad-block-prone than the Worker route.',\n inputSchema: generateDnsFallbackInputSchema,\n },\n async (args) => render(generateDnsFallback(args)),\n );\n\n server.registerTool(\n 'verify_install',\n {\n description:\n 'Run a post-install sanity checklist on the user project — SDK in deps, init() call grep-able in source, env vars set. Returns a `{ check, status, hint }[]` triple for each item.',\n inputSchema: verifyInstallInputSchema,\n },\n async (args) => render(verifyInstall(args)),\n );\n\n server.registerTool(\n 'explain_topic',\n {\n description:\n 'Return a focused markdown brief on one of: origin-allowlist, cloudflare-proxy, dns-cname, api-keys, events-vs-identify, sync-vs-async. Use when the user asks a \"why\" / \"how\" question about an advanced setup.',\n inputSchema: explainInputSchema,\n },\n async (args) => render(explainTopic(args)),\n );\n\n return server;\n}\n","import { resolve } from 'node:path';\n\nimport { frameworkLabel } from '@fingerprint79/sdk-snippets';\nimport { z } from 'zod';\n\nimport { detectFramework } from '../util/detect-framework.js';\nimport { detectPackageManager } from '../util/detect-pm.js';\n\nimport type { ToolEnvelope } from './types.js';\n\nexport const analyzeInputSchema = {\n cwd: z\n .string()\n .describe(\n 'Absolute path to the user project root. The AI assistant should pass its own current working directory or the project root it has been browsing.',\n ),\n};\n\n/**\n * `analyze_project` — read package.json + lockfile + framework config\n * to determine what we're working with. Pure read; never writes.\n *\n * Returns:\n * - framework + label (one of the 8 supported ids)\n * - packageManager (pnpm/yarn/bun/npm)\n * - entryFile (absolute path) — where wire_init will insert code\n * - hasSDK (boolean) — true if `fingerprint-platform-sdk` is already\n * in deps, in which case the AI can skip install_sdk\n * - rationale — human-readable signal used to make the call\n */\nexport function analyzeProject(input: { cwd: string }): ToolEnvelope {\n const cwd = resolve(input.cwd);\n const scan = detectFramework(cwd);\n const pm = detectPackageManager(cwd);\n\n const nextSteps: string[] = [];\n if (!scan.hasSDK) {\n nextSteps.push(`Call \\`install_sdk\\` with cwd=\"${cwd}\" to install fingerprint-platform-sdk.`);\n }\n if (scan.entryFile) {\n nextSteps.push(\n `Call \\`wire_init\\` with cwd=\"${cwd}\", framework=\"${scan.framework}\", entryFile=\"${scan.entryFile}\" to insert the init snippet.`,\n );\n } else {\n nextSteps.push(\n `Couldn't auto-locate an entry file for ${scan.framework}. Ask the user where they want \\`fp.identify()\\` to fire on app boot, then pass that path to \\`wire_init\\`.`,\n );\n }\n nextSteps.push(\n `Call \\`set_collector_url\\` once you know the user's collector URL — typically https://<their-domain>/fpjs proxied through a Cloudflare Worker.`,\n );\n\n const details =\n `**Project scan**\\n\\n` +\n `- Framework: \\`${scan.framework}\\` (${frameworkLabel(scan.framework)})\\n` +\n `- Package manager: \\`${pm}\\` (detected from lockfile)\\n` +\n `- SDK already installed: ${scan.hasSDK ? 'yes' : 'no'}\\n` +\n `- Entry file: ${scan.entryFile ? `\\`${scan.entryFile}\\`` : '_not auto-detected_'}\\n\\n` +\n `_${scan.rationale}_`;\n\n return {\n summary: `Detected ${frameworkLabel(scan.framework)} project (${pm}, SDK ${scan.hasSDK ? 'installed' : 'missing'}).`,\n details,\n nextSteps,\n meta: {\n cwd,\n framework: scan.framework,\n packageManager: pm,\n entryFile: scan.entryFile ?? null,\n hasSDK: scan.hasSDK,\n },\n };\n}\n","/**\n * Code-snippet builder for the Install Fingerprint wizard.\n *\n * The SDK API is identical across frameworks (`init({collectorUrl,\n * projectKey})` then `fp.identify(...)`), but how an integrator\n * usually mounts it differs — Next.js needs `'use client'` + a\n * client-only dynamic import, React needs a `useEffect`, Angular a\n * service, etc. We hand out the smallest snippet that still feels\n * idiomatic for each framework so a copy-paste lands in roughly the\n * right place.\n *\n * `apiKey` is substituted into the snippet when revealed on the\n * Get-Started page; otherwise the placeholder string from the i18n\n * dictionary takes its place.\n */\n\nexport type Framework =\n | 'javascript'\n | 'javascript-spa'\n | 'nextjs'\n | 'react'\n | 'vue'\n | 'angular'\n | 'svelte'\n | 'preact';\n\nexport type SnippetSource = 'cdn' | 'npm';\n\nexport interface SnippetInput {\n readonly framework: Framework;\n readonly source: SnippetSource;\n /** Plain-text public api key, or the i18n placeholder when masked. */\n readonly apiKey: string;\n /** The customer's custom subdomain pointing at our collector, e.g.\n * `https://fp.theirsite.com`. Always shown — encourages the\n * production pattern over leaking our raw IP in browser source.\n * See `InstallFlow.tsx::Step 3` for the DNS-setup explanation. */\n readonly collectorUrl: string;\n}\n\n/** All 8 framework ids in display order. Mobile is intentionally\n * absent from this enum — that picker lives on the platform step. */\nexport const FRAMEWORKS: ReadonlyArray<{ id: Framework; label: string }> = [\n { id: 'javascript', label: 'JavaScript' },\n { id: 'javascript-spa', label: 'JavaScript SPA' },\n { id: 'nextjs', label: 'Next.js' },\n { id: 'react', label: 'React' },\n { id: 'angular', label: 'Angular' },\n { id: 'vue', label: 'Vue.js' },\n { id: 'preact', label: 'Preact' },\n { id: 'svelte', label: 'Svelte' },\n];\n\nexport function frameworkLabel(id: Framework): string {\n return FRAMEWORKS.find((f) => f.id === id)?.label ?? id;\n}\n\n/** CDN install line — same across frameworks (the bundle is plain\n * IIFE either way). Always links the obfuscated build; never link\n * `dist/fp.js` from a production page — DevTools → Sources reads it\n * in the clear and undoes the obfuscator pipeline.\n *\n * The primary line is the same-origin path served by the Cloudflare\n * Worker the integrator deploys in Step 3 («Production proxy»). The\n * unpkg URL stays as a commented-out fallback for quick dev/staging\n * — adblockers' default lists ship a rule for `unpkg.com`, so in\n * production this URL is one of the first to disappear from\n * document.scripts. */\nexport function cdnInstallSnippet(): string {\n return `<!-- Production: self-hosted through the Cloudflare Worker (Step 3 below) -->\n<script src=\"/fpjs/agent.js\" async></script>\n\n<!-- Dev / staging only — unpkg URL is on every ad-block list -->\n<!-- <script src=\"https://unpkg.com/fingerprint-platform-sdk/dist/fp.obf.js\" async></script> -->`;\n}\n\n/** NPM install line — same across frameworks. */\nexport function npmInstallSnippet(): string {\n return `npm install fingerprint-platform-sdk\n# or pnpm add fingerprint-platform-sdk\n# or yarn add fingerprint-platform-sdk`;\n}\n\n/** Initialise-and-identify snippet, framework-specific. The\n * framework parameter only changes the WRAPPER code — the\n * Fingerprint API calls inside it are identical. */\nexport function initSnippet(input: SnippetInput): string {\n const { framework, source, apiKey, collectorUrl } = input;\n const config = ` collectorUrl: '${collectorUrl}',\n projectKey: '${apiKey}',`;\n\n // CDN init — global `window.FP` from the IIFE. The init call is\n // identical across frameworks (same global, same shape); what\n // differs is WHERE the integrator drops the <script> tag. The\n // framework-specific lead comment surfaces that decision so an\n // operator switching frameworks while parked on the CDN tab sees\n // their picked framework's mounting convention spelled out.\n if (source === 'cdn') {\n return `${cdnPlacementComment(framework)}\n<script>\n // Initialize the agent at application startup.\n const fp = window.FP.init({\n${config}\n });\n\n // Get the visitor and event identifier when you need it.\n fp.identify({ event: 'pageview' }).then((r) => {\n if (r.visitorId) console.log(r.eventId, r.visitorId, r.suspectScore);\n else console.log('event queued:', r.eventId);\n });\n</script>`;\n }\n\n // NPM / bundler path — framework-specific wrapper.\n switch (framework) {\n case 'nextjs':\n return `// app/_components/FpClient.tsx — client component\n'use client';\nimport { useEffect } from 'react';\n\nexport function FpClient() {\n useEffect(() => {\n let cancelled = false;\n // Dynamic import keeps the SDK out of the server bundle — Next.js\n // SSR has no Window, the SDK throws on the canvas calls there.\n import('fingerprint-platform-sdk').then(({ init }) => {\n if (cancelled) return;\n const fp = init({\n${config.replace(/^/gm, ' ')}\n });\n void fp.identify({ event: 'pageview' });\n });\n return () => { cancelled = true; };\n }, []);\n return null;\n}\n\n// Then mount <FpClient /> in your root layout.`;\n\n case 'react':\n return `import { useEffect } from 'react';\nimport { init } from 'fingerprint-platform-sdk';\n\nexport function useFingerprint() {\n useEffect(() => {\n const fp = init({\n${config.replace(/^/gm, ' ')}\n });\n fp.identify({ event: 'pageview' }).then((r) => {\n if (r.visitorId) console.log(r.eventId, r.visitorId, r.suspectScore);\n else console.log('event queued:', r.eventId);\n });\n }, []);\n}\n\n// Call useFingerprint() once in your root component.`;\n\n case 'vue':\n return `// plugins/fingerprint.ts\nimport { init } from 'fingerprint-platform-sdk';\n\nconst fp = init({\n${config}\n});\n\nexport default {\n install(app) {\n app.config.globalProperties.$fp = fp;\n },\n};\n\n// Then in main.ts:\n// app.use(fpPlugin);\n// And from any component:\n// this.$fp.identify({ event: 'pageview' });`;\n\n case 'angular':\n return `// fingerprint.service.ts\nimport { Injectable } from '@angular/core';\nimport { init, type FpSdk } from 'fingerprint-platform-sdk';\n\n@Injectable({ providedIn: 'root' })\nexport class FingerprintService {\n readonly fp: FpSdk = init({\n${config.replace(/^/gm, ' ')}\n });\n}\n\n// Inject in components and call: this.fp.fp.identify({ event: 'pageview' });`;\n\n case 'svelte':\n return `// src/lib/fingerprint.ts\nimport { init } from 'fingerprint-platform-sdk';\n\nexport const fp = init({\n${config}\n});\n\n// In any +page.svelte:\n// <script lang=\"ts\">\n// import { onMount } from 'svelte';\n// import { fp } from '$lib/fingerprint';\n// onMount(() => { void fp.identify({ event: 'pageview' }); });\n// </script>`;\n\n case 'preact':\n return `import { useEffect } from 'preact/hooks';\nimport { init } from 'fingerprint-platform-sdk';\n\nexport function useFingerprint() {\n useEffect(() => {\n const fp = init({\n${config.replace(/^/gm, ' ')}\n });\n void fp.identify({ event: 'pageview' });\n }, []);\n}\n\n// Call useFingerprint() once in your root component.`;\n\n case 'javascript-spa':\n return `// src/fingerprint.js\n// Single-page app pattern: init once, identify() on each route change.\nimport { init } from 'fingerprint-platform-sdk';\n\nexport const fp = init({\n${config}\n});\n\n// In your router's onNavigate hook:\n// fp.identify({ event: 'route:' + path });`;\n\n case 'javascript':\n default:\n return `import { init } from 'fingerprint-platform-sdk';\n\nconst fp = init({\n${config}\n});\n\nfp.identify({ event: 'pageview' }).then((r) => {\n if (r.visitorId) console.log(r.eventId, r.visitorId, r.suspectScore);\n else console.log('event queued:', r.eventId);\n});`;\n }\n}\n\n/** Framework-specific lead-comment for the CDN snippet. The init\n * body itself is identical across frameworks (the IIFE exposes the\n * same `window.FP.init` global), but mounting conventions differ —\n * React puts the <script> in `public/index.html`, Next.js prefers\n * `<Script>` from `next/script` in the root layout, etc. Surfacing\n * the placement decision per-framework makes flipping the framework\n * picker visibly do something even when the CDN tab is parked. */\nfunction cdnPlacementComment(framework: Framework): string {\n switch (framework) {\n case 'nextjs':\n return `<!-- Next.js: prefer next/script with strategy=\"afterInteractive\" in app/layout.tsx\n rather than a raw <script>. The agent attaches to window.FP on first paint. -->`;\n case 'react':\n return `<!-- React: drop <script> in public/index.html (CRA / Vite-React).\n Access window.FP from any component after first paint. -->`;\n case 'vue':\n return `<!-- Vue.js: place <script> in public/index.html.\n Read window.FP inside onMounted / mounted hooks. -->`;\n case 'angular':\n return `<!-- Angular: add <script> to src/index.html.\n Wrap window.FP in an Injectable service for DI ergonomics. -->`;\n case 'svelte':\n return `<!-- Svelte / SvelteKit: add <script> to app.html (SvelteKit) or\n public/index.html. Use onMount() to read window.FP. -->`;\n case 'preact':\n return `<!-- Preact: drop <script> in your root index.html.\n Read window.FP from any component after mount. -->`;\n case 'javascript-spa':\n return `<!-- SPA: include once in your shell HTML. Call fp.identify()\n from your router's per-route hook to track navigation. -->`;\n case 'javascript':\n default:\n return `<!-- Vanilla JS: paste anywhere in <body>. The SDK fires\n identify() on its own once init() resolves. -->`;\n }\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { Framework } from '@fingerprint79/sdk-snippets';\n\n/**\n * Outcome of a project scan. `framework` is the best-match id from\n * the `Framework` union. `entryFile` is the absolute path where the\n * SDK init call should land — undefined when we couldn't locate a\n * conventional entry. `hasSDK` is a flag the AI can branch on to\n * skip `install_sdk` and jump straight to `wire_init` if needed.\n */\nexport interface ProjectScan {\n readonly framework: Framework;\n readonly entryFile: string | undefined;\n readonly hasSDK: boolean;\n /** Free-text rationale — surfaces in the analyze_project response so\n * the AI can echo it back to the user when explaining its plan. */\n readonly rationale: string;\n}\n\ninterface PackageJsonShape {\n readonly dependencies?: Record<string, string>;\n readonly devDependencies?: Record<string, string>;\n readonly peerDependencies?: Record<string, string>;\n}\n\n/**\n * Read package.json deps + the conventional config files to figure\n * out the project's framework. Order matters here: Next.js is a\n * React superset, so we must check `next` before `react`; SvelteKit\n * is a Svelte superset; Nuxt is a Vue superset. The first match\n * wins.\n *\n * When nothing matches confidently, we fall back to `'javascript'`\n * (vanilla) — that's the safest default and lets `wire_init` still\n * emit a copy-paste-able snippet rather than refusing the call.\n */\nexport function detectFramework(cwd: string): ProjectScan {\n const pkgJson = readPackageJson(cwd);\n const deps = collectDeps(pkgJson);\n const sdkInstalled = 'fingerprint-platform-sdk' in deps;\n\n // Order is intentional — check supersets first.\n if ('next' in deps) {\n return scan('nextjs', cwd, sdkInstalled, 'Detected `next` dependency in package.json.');\n }\n if ('nuxt' in deps) {\n // We don't ship a separate Nuxt snippet — Nuxt uses the same Vue\n // plugin pattern, so map to `vue`. Caller can still emit the right\n // installation path because the init code is plugin-based.\n return scan('vue', cwd, sdkInstalled, 'Detected `nuxt` — using the Vue.js plugin pattern.');\n }\n if ('@angular/core' in deps) {\n return scan('angular', cwd, sdkInstalled, 'Detected `@angular/core` dependency.');\n }\n if ('@sveltejs/kit' in deps || 'svelte' in deps) {\n return scan('svelte', cwd, sdkInstalled, 'Detected Svelte (or SvelteKit).');\n }\n if ('preact' in deps) {\n return scan('preact', cwd, sdkInstalled, 'Detected `preact` dependency.');\n }\n if ('vue' in deps) {\n return scan('vue', cwd, sdkInstalled, 'Detected `vue` dependency.');\n }\n if ('react' in deps || 'react-dom' in deps) {\n // Vite-React vs CRA vs plain React app — same snippet shape. We\n // tell apart \"this is an SPA shell\" from \"this is a React lib\"\n // by looking for an entry file; if none, the `javascript` fallback\n // would be silly when react is clearly in deps. Stick with react.\n return scan('react', cwd, sdkInstalled, 'Detected `react` dependency.');\n }\n\n // Config-file-only signals (rare — e.g. someone removed deps from\n // package.json but left the config). These are weaker, so they sit\n // after the deps checks.\n if (existsSync(resolve(cwd, 'next.config.js')) || existsSync(resolve(cwd, 'next.config.ts'))) {\n return scan('nextjs', cwd, sdkInstalled, 'Detected `next.config.*` without explicit dep.');\n }\n if (existsSync(resolve(cwd, 'angular.json'))) {\n return scan('angular', cwd, sdkInstalled, 'Detected `angular.json`.');\n }\n if (\n existsSync(resolve(cwd, 'svelte.config.js')) ||\n existsSync(resolve(cwd, 'svelte.config.ts'))\n ) {\n return scan('svelte', cwd, sdkInstalled, 'Detected `svelte.config.*`.');\n }\n\n // SPA shell with a router but no framework lib? Treat as the SPA\n // wrapper so we emit `router.onNavigate → fp.identify` per-route.\n // Heuristic: presence of a router lib in deps.\n if ('react-router' in deps || 'vue-router' in deps || 'svelte-routing' in deps) {\n return scan(\n 'javascript-spa',\n cwd,\n sdkInstalled,\n 'Detected a router library without a framework lib.',\n );\n }\n\n return scan('javascript', cwd, sdkInstalled, 'No framework signal — falling back to vanilla JS.');\n}\n\n/* ── helpers ───────────────────────────────────────────────────── */\n\nfunction readPackageJson(cwd: string): PackageJsonShape {\n const p = resolve(cwd, 'package.json');\n if (!existsSync(p)) return {};\n try {\n return JSON.parse(readFileSync(p, 'utf8')) as PackageJsonShape;\n } catch {\n // Malformed package.json — treat as empty. The user has bigger\n // problems than our analyzer; we don't want to crash.\n return {};\n }\n}\n\nfunction collectDeps(pkg: PackageJsonShape): Record<string, string> {\n return {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n ...pkg.peerDependencies,\n };\n}\n\nfunction scan(framework: Framework, cwd: string, hasSDK: boolean, rationale: string): ProjectScan {\n return {\n framework,\n entryFile: locateEntryFile(framework, cwd),\n hasSDK,\n rationale,\n };\n}\n\n/**\n * Best-effort guess at the file where the SDK init call goes.\n * Returns `undefined` when no conventional entry exists — the AI\n * will then ask the user, or the `wire_init` tool refuses with a\n * helpful error pointing to the convention for the framework.\n */\nfunction locateEntryFile(framework: Framework, cwd: string): string | undefined {\n const candidates: Record<Framework, readonly string[]> = {\n nextjs: ['app/layout.tsx', 'app/layout.jsx', 'src/app/layout.tsx', 'pages/_app.tsx'],\n react: ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx', 'src/App.tsx'],\n vue: ['src/main.ts', 'src/main.js'],\n angular: ['src/main.ts', 'src/app/app.module.ts'],\n svelte: ['src/routes/+layout.svelte', 'src/main.ts', 'src/main.js'],\n preact: ['src/main.tsx', 'src/index.tsx', 'src/index.jsx'],\n 'javascript-spa': ['src/main.js', 'src/main.ts', 'src/index.js'],\n javascript: ['index.html', 'src/index.html', 'public/index.html'],\n };\n for (const rel of candidates[framework]) {\n const abs = resolve(cwd, rel);\n if (existsSync(abs)) return abs;\n }\n return undefined;\n}\n","import { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\n/**\n * Package managers we know how to emit install commands for. `npm` is\n * the safe fallback when no lockfile is present (matches what `npm\n * init` defaults to and what most tutorials assume).\n */\nexport type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm';\n\n/**\n * Detect the package manager used by the project at `cwd` by looking\n * at lockfiles. We probe in the order pnpm → yarn → bun → npm so a\n * monorepo that has both `pnpm-lock.yaml` and a stray `package-lock.json`\n * (e.g. from a one-off `npm install` on a sub-package) doesn't get\n * misclassified — pnpm's presence is a stronger signal.\n *\n * Returns `'npm'` when nothing is present. Pure sync FS reads — fast\n * enough that this never warrants caching across tool calls, and\n * avoiding state simplifies the MCP request lifecycle.\n */\nexport function detectPackageManager(cwd: string): PackageManager {\n if (existsSync(resolve(cwd, 'pnpm-lock.yaml'))) return 'pnpm';\n if (existsSync(resolve(cwd, 'yarn.lock'))) return 'yarn';\n if (existsSync(resolve(cwd, 'bun.lockb')) || existsSync(resolve(cwd, 'bun.lock'))) return 'bun';\n return 'npm';\n}\n\n/**\n * Render the install command for a given package manager + package.\n * Centralised so the analyze, install and verify tools all emit\n * identical strings — the AI consumer can string-match them when\n * verifying that the user executed the install we proposed.\n */\nexport function renderInstallCommand(pm: PackageManager, pkg: string): string {\n switch (pm) {\n case 'pnpm':\n return `pnpm add ${pkg}`;\n case 'yarn':\n return `yarn add ${pkg}`;\n case 'bun':\n return `bun add ${pkg}`;\n case 'npm':\n default:\n return `npm install ${pkg}`;\n }\n}\n","import { resolve } from 'node:path';\n\nimport { z } from 'zod';\n\nimport { makeNewFileDiff } from '../util/diff.js';\n\nimport type { ToolEnvelope } from './types.js';\n\n/* ── set_collector_url ─────────────────────────────────────────── */\n\nexport const setCollectorUrlInputSchema = {\n projectKey: z.string().describe(\"Project's public API key.\"),\n collectorUrl: z\n .string()\n .url()\n .describe(\n 'Same-origin collector URL — typically https://<their-domain>/fpjs (proxied by a Cloudflare Worker).',\n ),\n};\n\n/**\n * `set_collector_url` — validates the supplied URL and returns:\n * (a) env-var spec (`FP_PROJECT_KEY` + `FP_COLLECTOR_URL`) the user\n * should add to `.env.local` (we don't write the file — diff-\n * not-mutate),\n * (b) the dashboard `allowedHosts` change the operator must make\n * via the Security page (we can't mutate the dashboard from\n * the MCP server in v1).\n */\nexport function setCollectorUrl(input: { projectKey: string; collectorUrl: string }): ToolEnvelope {\n let parsed: URL;\n try {\n parsed = new URL(input.collectorUrl);\n } catch {\n return {\n summary: 'Invalid collector URL.',\n details: `\\`${input.collectorUrl}\\` is not a valid URL. Pass a full https URL like \\`https://yoursite.com/fpjs\\`.`,\n meta: { error: 'invalid_url' },\n };\n }\n if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost') {\n return {\n summary: 'Collector URL must be HTTPS.',\n details:\n 'The browser SDK uses X25519 sealing which requires a secure context (HTTPS or localhost). ' +\n `\\`${parsed.protocol}//${parsed.hostname}\\` would fail at runtime — please serve via HTTPS in production.`,\n meta: { error: 'http_in_production' },\n };\n }\n\n const originForAllowedHosts = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}`;\n\n return {\n summary: `Use \\`${input.collectorUrl}\\` as the collector URL.`,\n details:\n `**1) Add to your environment** (e.g. \\`.env.local\\`):\\n\\n` +\n '```bash\\n' +\n `FP_PROJECT_KEY=${input.projectKey}\\n` +\n `FP_COLLECTOR_URL=${input.collectorUrl}\\n` +\n '```\\n\\n' +\n `**2) Add origin to dashboard allowedHosts.** Open the Fingerprint ` +\n `dashboard → Security → Allowed hosts and add:\\n\\n` +\n `\\`${originForAllowedHosts}\\`\\n\\n` +\n `Without this entry the collector rejects requests from your site ` +\n `with a 401 \\`origin_not_allowed\\`. (We can't mutate the dashboard from ` +\n `the MCP server in v1; this step is manual.)`,\n nextSteps: [\n `Add the two env vars above to \\`.env.local\\` (or your secret manager).`,\n `Open the dashboard's Security page and add \\`${originForAllowedHosts}\\` to allowedHosts.`,\n `Call \\`generate_cloudflare_worker\\` if you want a same-origin proxy on /fpjs/*.`,\n ],\n artifact: {\n kind: 'env',\n content: {\n FP_PROJECT_KEY: input.projectKey,\n FP_COLLECTOR_URL: input.collectorUrl,\n },\n },\n meta: { origin: originForAllowedHosts, collectorUrl: input.collectorUrl },\n };\n}\n\n/* ── generate_cloudflare_worker ─────────────────────────────────── */\n\nexport const generateCloudflareWorkerInputSchema = {\n cwd: z.string().describe('Absolute path to the user project root — for the diff header.'),\n collectorHost: z\n .string()\n .describe(\n 'Hostname of the Fingerprint collector to proxy to, without scheme. E.g. `collector.yoursite.com` or `89.124.91.162`.',\n ),\n customDomain: z\n .string()\n .describe(\n \"Your site's domain, e.g. `yoursite.com`. The Worker routes `<customDomain>/fpjs/*`.\",\n ),\n};\n\n/**\n * `generate_cloudflare_worker` — produces the Worker script body\n * plus `wrangler.toml` as two diffs the AI can apply with its edit\n * tool. The Worker proxies BOTH `/fpjs/agent.js` (SDK bundle) AND\n * the collector API endpoints under `/fpjs/v1/*` (ingest, result,\n * comparison, pk). Cookies stay first-party, ad-blockers blind.\n */\nexport function generateCloudflareWorker(input: {\n cwd: string;\n collectorHost: string;\n customDomain: string;\n}): ToolEnvelope {\n const cwd = resolve(input.cwd);\n const workerPath = resolve(cwd, 'cloudflare/worker.ts');\n const wranglerPath = resolve(cwd, 'cloudflare/wrangler.toml');\n\n // Strip protocol if the user accidentally included it.\n const host = input.collectorHost.replace(/^https?:\\/\\//, '').replace(/\\/+$/, '');\n const domain = input.customDomain.replace(/^https?:\\/\\//, '').replace(/\\/+$/, '');\n\n const workerScript = cloudflareWorkerTemplate(host);\n const wranglerToml = wranglerTomlTemplate(domain);\n\n const workerDiff = makeNewFileDiff({ cwd, filePath: workerPath, content: workerScript });\n const wranglerDiff = makeNewFileDiff({ cwd, filePath: wranglerPath, content: wranglerToml });\n\n return {\n summary: `Cloudflare Worker template for \\`${domain}/fpjs/*\\` → \\`${host}\\`.`,\n details:\n `Two files to write into the user's repo:\\n\\n` +\n `**\\`cloudflare/worker.ts\\`** — the Worker script body.\\n` +\n `**\\`cloudflare/wrangler.toml\\`** — Wrangler deploy config (route + zone).\\n\\n` +\n `Both diffs are pasted below. Apply with your edit tool, then run\\n\\n` +\n '```bash\\n' +\n 'cd cloudflare && wrangler deploy\\n' +\n '```\\n\\n' +\n `from the project root. After deploy, the Worker is live on the route\\n` +\n `pattern \\`${domain}/fpjs/*\\`. The SDK then loads same-origin: cookies\\n` +\n `stay first-party, ad-blockers can't pattern-match a cross-origin path.\\n\\n` +\n `--- ${workerPath} ---\\n\\n\\`\\`\\`diff\\n${workerDiff}\\n\\`\\`\\`\\n\\n` +\n `--- ${wranglerPath} ---\\n\\n\\`\\`\\`diff\\n${wranglerDiff}\\n\\`\\`\\``,\n nextSteps: [\n `Apply both diffs to create the cloudflare/ directory.`,\n `Install Wrangler: \\`npm i -g wrangler\\` (or use \\`npx wrangler\\`).`,\n `Authenticate: \\`wrangler login\\`.`,\n `Deploy: \\`cd cloudflare && wrangler deploy\\`.`,\n `Update \\`FP_COLLECTOR_URL\\` env var to \\`https://${domain}/fpjs\\`.`,\n `Add \\`https://${domain}\\` to dashboard allowedHosts via the Security page.`,\n ],\n // Single diff payload that concatenates both files — git apply\n // accepts multi-file unified diffs.\n artifact: { kind: 'diff', content: `${workerDiff}\\n${wranglerDiff}` },\n meta: {\n workerPath,\n wranglerPath,\n route: `${domain}/fpjs/*`,\n collectorHost: host,\n },\n };\n}\n\n/* ── generate_dns_fallback ──────────────────────────────────────── */\n\nexport const generateDnsFallbackInputSchema = {\n subdomain: z\n .string()\n .describe('Subdomain to point at the collector, e.g. `fp.yoursite.com`. Must be a FQDN.'),\n collectorHost: z.string().describe('Hostname (or IP) of the Fingerprint collector.'),\n};\n\n/**\n * `generate_dns_fallback` — when Cloudflare isn't an option,\n * point a DNS CNAME from `fp.yoursite.com` to the collector\n * directly. Less ad-block-proof than the Worker route (a dedicated\n * subdomain is easier for filter rules to catch than a same-origin\n * path) but still keeps cookies first-party.\n */\nexport function generateDnsFallback(input: {\n subdomain: string;\n collectorHost: string;\n}): ToolEnvelope {\n const host = input.collectorHost.replace(/^https?:\\/\\//, '').replace(/\\/+$/, '');\n // Determine the apex from the subdomain — naively split on the\n // first dot. Good enough for the DNS instructions; the user will\n // know which zone to edit.\n const apex = input.subdomain.split('.').slice(1).join('.');\n\n return {\n summary: `DNS CNAME \\`${input.subdomain}\\` → \\`${host}\\`.`,\n details:\n `Add the following DNS record in your registrar's zone editor for ` +\n `\\`${apex}\\`:\\n\\n` +\n '```text\\n' +\n `Type: CNAME\\n` +\n `Name: ${input.subdomain.replace(`.${apex}`, '')}\\n` +\n `Value: ${host}\\n` +\n `TTL: 300 (5 min — bump after verification)\\n` +\n '```\\n\\n' +\n `Once propagated, set:\\n\\n` +\n '```bash\\n' +\n `FP_COLLECTOR_URL=https://${input.subdomain}\\n` +\n '```\\n\\n' +\n `Then add \\`https://${input.subdomain}\\` to the dashboard's ` +\n `allowedHosts on the Security page.\\n\\n` +\n `_Trade-off vs the Cloudflare Worker route: a dedicated subdomain ` +\n `is **easier for ad-block filter rules to catch** than a same-origin ` +\n `path like \\`yoursite.com/fpjs/*\\`. Use the Worker if you can; this ` +\n `is the fallback when Cloudflare isn't on the table._`,\n nextSteps: [\n `Create the CNAME record in your DNS provider.`,\n `Wait for propagation (\\`dig ${input.subdomain}\\` should return the CNAME).`,\n `Update \\`FP_COLLECTOR_URL\\` and dashboard allowedHosts.`,\n ],\n meta: { subdomain: input.subdomain, collectorHost: host, apex },\n };\n}\n\n/* ── templates ─────────────────────────────────────────────────── */\n\nfunction cloudflareWorkerTemplate(collectorHost: string): string {\n return `/**\n * Fingerprint Platform same-origin proxy.\n *\n * Generated by fingerprint-platform-mcp.\n *\n * Route pattern: <yoursite>/fpjs/*\n *\n * Proxies BOTH the SDK bundle (served on /fpjs/agent.js) AND every\n * collector endpoint (/fpjs/v1/ingest, /fpjs/v1/result/<id>,\n * /fpjs/v1/pk, /fpjs/v1/comparison). Cookies stay first-party,\n * ad-blockers cannot pattern-match a same-origin path.\n *\n * The integrator's app backend is NEVER in the fingerprint request\n * path — that would couple uptime and add latency. The Worker\n * fans out directly to ${collectorHost}.\n */\n\nconst COLLECTOR_ORIGIN = \"https://${collectorHost}\";\n\nexport default {\n async fetch(req: Request): Promise<Response> {\n const url = new URL(req.url);\n\n // SDK bundle.\n if (url.pathname === \"/fpjs/agent.js\") {\n const upstream = new URL(\"/v1/agent.js\", COLLECTOR_ORIGIN);\n return forward(req, upstream);\n }\n\n // Collector API surface under /fpjs/v1/*.\n if (url.pathname.startsWith(\"/fpjs/v1/\")) {\n const upstream = new URL(url.pathname.replace(/^\\\\/fpjs/, \"\"), COLLECTOR_ORIGIN);\n upstream.search = url.search;\n return forward(req, upstream);\n }\n\n // Unknown path under /fpjs/ — 404 cleanly so misconfigured route\n // patterns don't accidentally proxy the user's whole site.\n return new Response(\"not found\", { status: 404 });\n },\n};\n\nasync function forward(req: Request, upstream: URL): Promise<Response> {\n const headers = new Headers(req.headers);\n // Preserve the original client IP for the collector's IP-class\n // enrichment — Cloudflare exposes it via cf-connecting-ip.\n const clientIp = headers.get(\"cf-connecting-ip\") ?? headers.get(\"x-forwarded-for\");\n if (clientIp) headers.set(\"x-forwarded-for\", clientIp);\n // Strip the Host header so the upstream sees its own hostname.\n headers.delete(\"host\");\n\n const init: RequestInit = {\n method: req.method,\n headers,\n body: req.method === \"GET\" || req.method === \"HEAD\" ? undefined : req.body,\n redirect: \"manual\",\n };\n return fetch(upstream.toString(), init);\n}\n`;\n}\n\nfunction wranglerTomlTemplate(customDomain: string): string {\n return `name = \"fingerprint-proxy\"\nmain = \"worker.ts\"\ncompatibility_date = \"2025-01-01\"\n\n# Attach the Worker to your domain. Replace the zone_name if it\n# differs from the domain. Wrangler will prompt for the zone on\n# first deploy if you remove the zone_name line.\nroutes = [\n { pattern = \"${customDomain}/fpjs/*\", zone_name = \"${customDomain}\" },\n]\n`;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { relative } from 'node:path';\n\nimport { createPatch } from 'diff';\n\n/**\n * Build a unified-diff patch from an existing file's current contents\n * to its proposed new contents. Returned as a string ready to drop\n * into a `git apply` invocation. The path inside the patch is\n * RELATIVE to `cwd` so the AI assistant can apply it from the\n * project root without needing to rewrite the header.\n *\n * The `diff` package's `createPatch` emits standard unified format\n * (3 lines of context by default). We pass empty `oldHeader` /\n * `newHeader` to keep the output compact — the `--- a/<path>` /\n * `+++ b/<path>` lines `git apply` cares about are generated\n * regardless.\n */\nexport function makeUnifiedDiff(opts: {\n readonly cwd: string;\n readonly filePath: string;\n readonly newContent: string;\n}): string {\n const oldContent = existsSync(opts.filePath) ? readFileSync(opts.filePath, 'utf8') : '';\n const rel = relative(opts.cwd, opts.filePath).split('\\\\').join('/');\n return createPatch(rel, oldContent, opts.newContent, '', '', { context: 3 });\n}\n\n/**\n * Build a \"new file\" diff — same shape as makeUnifiedDiff but\n * always emits a `/dev/null → b/<path>` header, suitable for files\n * the assistant should create from scratch (e.g. the Cloudflare\n * Worker template). The `diff` lib handles this automatically when\n * `oldContent` is the empty string, but we wrap it for callers who\n * want explicit intent in the call site.\n */\nexport function makeNewFileDiff(opts: {\n readonly cwd: string;\n readonly filePath: string;\n readonly content: string;\n}): string {\n const rel = relative(opts.cwd, opts.filePath).split('\\\\').join('/');\n return createPatch(rel, '', opts.content, '', '', { context: 3 });\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { z } from 'zod';\n\nimport type { ToolEnvelope } from './types.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Topic ids → bundled markdown filename. We define the union here\n * (rather than scanning `docs/` at runtime) so a typo in the input\n * fails Zod validation before we even open the FS, and so an AI\n * model can be confident the topic list matches its training memory.\n */\nconst TOPIC_FILES: Record<string, string> = {\n 'origin-allowlist': 'origin-allowlist.md',\n 'cloudflare-proxy': 'cloudflare-proxy.md',\n 'dns-cname': 'dns-cname.md',\n 'api-keys': 'api-keys.md',\n 'events-vs-identify': 'events-vs-identify.md',\n 'sync-vs-async': 'sync-vs-async.md',\n};\n\nexport const explainInputSchema = {\n topic: z\n .enum([\n 'origin-allowlist',\n 'cloudflare-proxy',\n 'dns-cname',\n 'api-keys',\n 'events-vs-identify',\n 'sync-vs-async',\n ])\n .describe(\n 'Topic id. The AI assistant picks one based on the user question. Each topic returns a focused markdown brief — pasting raw docs into the conversation tends to drown out the actionable bits.',\n ),\n};\n\n/**\n * `explain_topic` — return a focused markdown brief on one of our\n * six advanced-setup topics. Pure read of the bundled `docs/` dir.\n *\n * The `docs/` files are copied into `dist/docs/` by tsup's\n * `onSuccess` step (see `tsup.config.ts`). When running under\n * `tsx` (`pnpm dev`), we resolve back to `src/docs/`. The path\n * fallback handles both.\n */\nexport function explainTopic(input: { topic: keyof typeof TOPIC_FILES }): ToolEnvelope {\n const filename = TOPIC_FILES[input.topic];\n if (!filename) {\n return {\n summary: `Unknown topic \"${input.topic}\".`,\n details: `Known topics: ${Object.keys(TOPIC_FILES).join(', ')}.`,\n meta: { error: 'unknown_topic' },\n };\n }\n\n // dist/docs/<name>.md when bundled, src/docs/<name>.md under tsx.\n const candidates = [\n resolve(__dirname, '..', 'docs', filename),\n resolve(__dirname, 'docs', filename),\n resolve(__dirname, '..', '..', 'src', 'docs', filename),\n ];\n for (const path of candidates) {\n try {\n const content = readFileSync(path, 'utf8');\n return {\n summary: `Returned docs/${filename} (${content.length} chars).`,\n details: content,\n artifact: { kind: 'markdown', content },\n meta: { topic: input.topic, path },\n };\n } catch {\n // try next candidate\n }\n }\n\n return {\n summary: `docs/${filename} not found at runtime.`,\n details:\n `Looked at: ${candidates.join(', ')}. This is a build issue — the ` +\n `tsup config should be copying src/docs/ into dist/docs/. Re-run ` +\n `\\`pnpm build\\` and try again.`,\n meta: { error: 'doc_missing', topic: input.topic },\n };\n}\n","import { resolve } from 'node:path';\n\nimport { z } from 'zod';\n\nimport {\n detectPackageManager,\n renderInstallCommand,\n type PackageManager,\n} from '../util/detect-pm.js';\n\nimport type { ToolEnvelope } from './types.js';\n\nexport const installInputSchema = {\n cwd: z.string().describe('Absolute path to the user project root.'),\n packageManager: z\n .enum(['pnpm', 'yarn', 'bun', 'npm'])\n .optional()\n .describe(\n 'Override the auto-detected package manager. Most callers should let analyze_project decide.',\n ),\n source: z\n .enum(['npm', 'cdn'])\n .default('npm')\n .describe(\n \"Install source. `npm` is the default (recommended). `cdn` returns the <script> tag snippet instead — useful when the user explicitly doesn't want to add an npm dep.\",\n ),\n};\n\n/**\n * `install_sdk` — returns the install command (or CDN snippet) for\n * `fingerprint-platform-sdk`. Does NOT execute. The AI runs it via\n * its own bash/shell tool — this is the diff-not-mutate contract we\n * keep across every tool.\n */\nexport function installSdk(input: {\n cwd: string;\n packageManager?: PackageManager;\n source?: 'npm' | 'cdn';\n}): ToolEnvelope {\n const cwd = resolve(input.cwd);\n const source = input.source ?? 'npm';\n\n if (source === 'cdn') {\n return {\n summary: 'CDN install — no package manager command needed.',\n details:\n 'Add the following `<script>` tag to your HTML shell ' +\n '(e.g. `public/index.html`, `app.html`, or the framework-specific ' +\n 'entry document). Once it loads, `window.FP.init({...})` is available.\\n\\n' +\n '```html\\n<script src=\"/fpjs/agent.js\" async></script>\\n```\\n\\n' +\n '_The recommended `/fpjs/agent.js` path is served by your Cloudflare ' +\n 'Worker proxy (see `generate_cloudflare_worker`). The unpkg URL ' +\n 'works for local dev but is on every ad-block list — do not ship ' +\n 'it to production._',\n nextSteps: [`Call \\`wire_init\\` to insert the init() call into the right entry file.`],\n artifact: {\n kind: 'markdown',\n content: '<script src=\"/fpjs/agent.js\" async></script>',\n },\n meta: { source: 'cdn', cwd },\n };\n }\n\n const pm = input.packageManager ?? detectPackageManager(cwd);\n const cmd = renderInstallCommand(pm, 'fingerprint-platform-sdk');\n\n return {\n summary: `Install via ${pm}: \\`${cmd}\\``,\n details:\n `Run the following command in the project root (\\`${cwd}\\`):\\n\\n` +\n '```bash\\n' +\n cmd +\n '\\n```\\n\\n' +\n `_Detected package manager: ${pm}. If this is wrong, pass \\`packageManager\\` explicitly._`,\n nextSteps: [\n `Execute the command above via your bash tool.`,\n `Then call \\`wire_init\\` to insert the SDK init() call into the right entry file.`,\n ],\n artifact: { kind: 'command', content: cmd, cwd },\n meta: { source: 'npm', packageManager: pm, cwd, command: cmd },\n };\n}\n","import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { z } from 'zod';\n\nimport { detectFramework } from '../util/detect-framework.js';\n\nimport type { ToolEnvelope } from './types.js';\n\nexport const verifyInstallInputSchema = {\n cwd: z.string().describe('Absolute path to the user project root.'),\n};\n\ninterface Check {\n readonly name: string;\n readonly status: 'ok' | 'warn' | 'fail';\n readonly hint: string;\n}\n\n/**\n * `verify_install` — sanity checklist run after the AI applied the\n * `install_sdk` + `wire_init` diffs. We re-analyze the project and\n * report a structured list:\n *\n * - SDK dependency present\n * - Init call grep-able somewhere in the source tree\n * - Env vars set (FP_PROJECT_KEY + FP_COLLECTOR_URL — in shell env\n * or a `.env*` file)\n *\n * Doesn't fire a real /v1/ingest request — that would require us to\n * spawn a headless browser, and we're a Node CLI. The AI can do\n * that step itself if asked (it has Playwright tools).\n */\nexport function verifyInstall(input: { cwd: string }): ToolEnvelope {\n const cwd = resolve(input.cwd);\n const scan = detectFramework(cwd);\n const checks: Check[] = [];\n\n // 1) SDK in deps.\n checks.push({\n name: 'fingerprint-platform-sdk in dependencies',\n status: scan.hasSDK ? 'ok' : 'fail',\n hint: scan.hasSDK\n ? `Detected via package.json.`\n : `Run \\`install_sdk\\` and apply its install command.`,\n });\n\n // 2) Init call somewhere in src/. We grep for `FP.init(` and the\n // ESM `init(` import — either signals the user's code references\n // the SDK. False positives are unlikely (we're in a project that\n // just installed `fingerprint-platform-sdk`).\n const initCallFound = grepForInitCall(cwd);\n checks.push({\n name: 'SDK `init(...)` call present in source tree',\n status: initCallFound ? 'ok' : 'warn',\n hint: initCallFound\n ? `Found a reference to \\`init({...})\\` or \\`window.FP.init\\` in the source.`\n : `Couldn't find an \\`init({...})\\` call. Either re-run \\`wire_init\\` to ` +\n `place the snippet, or confirm the user mounted the generated module.`,\n });\n\n // 3) Env vars. We accept either real env (process.env) or a `.env*`\n // file in the project root.\n const envCheck = envVarStatus(cwd);\n checks.push({\n name: 'FP_PROJECT_KEY + FP_COLLECTOR_URL configured',\n status: envCheck.both ? 'ok' : envCheck.either ? 'warn' : 'fail',\n hint: envCheck.hint,\n });\n\n const failCount = checks.filter((c) => c.status === 'fail').length;\n const warnCount = checks.filter((c) => c.status === 'warn').length;\n const summary =\n failCount > 0\n ? `${failCount} check failed, ${warnCount} warning. See checklist.`\n : warnCount > 0\n ? `Mostly good — ${warnCount} thing(s) to confirm manually.`\n : 'All checks green. Send a test event and look for it on the dashboard.';\n\n return {\n summary,\n details:\n `**Verification checklist**\\n\\n` +\n checks.map((c) => `- ${badge(c.status)} **${c.name}** — ${c.hint}`).join('\\n'),\n nextSteps:\n failCount === 0 && warnCount === 0\n ? [\n `Trigger an event by reloading the page where the SDK is mounted.`,\n `Open the dashboard → Identification view; the event should appear within ~2s.`,\n ]\n : [`Resolve the failed / warned checks above and call \\`verify_install\\` again.`],\n meta: { checks, framework: scan.framework },\n };\n}\n\n/* ── helpers ───────────────────────────────────────────────────── */\n\nfunction badge(status: 'ok' | 'warn' | 'fail'): string {\n return status === 'ok' ? '✓' : status === 'warn' ? '⚠' : '✗';\n}\n\nfunction grepForInitCall(cwd: string): boolean {\n // Single-shot tree walk capped at 5000 files to avoid runaway\n // recursion on giant monorepos. We're only sanity-checking — a\n // real grep is the user's job.\n let count = 0;\n const stack: string[] = [cwd];\n const seen = new Set<string>();\n while (stack.length > 0 && count < 5000) {\n const dir = stack.pop();\n if (!dir || seen.has(dir)) continue;\n seen.add(dir);\n if (/[\\\\/](node_modules|dist|\\.next|build|coverage|\\.turbo)$/.test(dir)) continue;\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n continue;\n }\n for (const name of entries) {\n count++;\n if (count >= 5000) break;\n const abs = resolve(dir, name);\n let stat;\n try {\n stat = statSync(abs);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n stack.push(abs);\n continue;\n }\n if (!/\\.(t|j)sx?$|\\.svelte$|\\.vue$|\\.html$/.test(name)) continue;\n try {\n const raw = readFileSync(abs, 'utf8');\n if (/\\bFP\\.init\\(|\\binit\\(\\s*\\{\\s*(collectorUrl|projectKey)/m.test(raw)) {\n return true;\n }\n } catch {\n // unreadable; ignore.\n }\n }\n }\n return false;\n}\n\nfunction envVarStatus(cwd: string): { both: boolean; either: boolean; hint: string } {\n const fromProcess = {\n key: Boolean(process.env['FP_PROJECT_KEY']),\n url: Boolean(process.env['FP_COLLECTOR_URL']),\n };\n const dotenvFiles = ['.env', '.env.local', '.env.development', '.env.production'];\n const seenInDotenv = { key: false, url: false };\n for (const name of dotenvFiles) {\n const p = resolve(cwd, name);\n if (!existsSync(p)) continue;\n try {\n const raw = readFileSync(p, 'utf8');\n if (/^\\s*FP_PROJECT_KEY\\s*=/m.test(raw)) seenInDotenv.key = true;\n if (/^\\s*FP_COLLECTOR_URL\\s*=/m.test(raw)) seenInDotenv.url = true;\n } catch {\n // ignore\n }\n }\n const keyOk = fromProcess.key || seenInDotenv.key;\n const urlOk = fromProcess.url || seenInDotenv.url;\n const both = keyOk && urlOk;\n const either = keyOk || urlOk;\n const missing: string[] = [];\n if (!keyOk) missing.push('FP_PROJECT_KEY');\n if (!urlOk) missing.push('FP_COLLECTOR_URL');\n return {\n both,\n either,\n hint: both\n ? `Found both vars (in process.env or a .env file).`\n : `Missing: ${missing.join(', ')}. Add to \\`.env.local\\` via \\`set_collector_url\\`.`,\n };\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\n\nimport { initSnippet, frameworkLabel, type Framework } from '@fingerprint79/sdk-snippets';\nimport { z } from 'zod';\n\nimport { makeUnifiedDiff, makeNewFileDiff } from '../util/diff.js';\n\nimport type { ToolEnvelope } from './types.js';\n\nexport const wireInitInputSchema = {\n cwd: z.string().describe('Absolute path to the user project root.'),\n framework: z\n .enum(['javascript', 'javascript-spa', 'nextjs', 'react', 'vue', 'angular', 'svelte', 'preact'])\n .describe('Framework id from `analyze_project`.'),\n entryFile: z\n .string()\n .describe(\n 'Absolute path to the file that should host the SDK init call (from `analyze_project.meta.entryFile`).',\n ),\n apiKey: z\n .string()\n .describe(\n \"Project's public API key. Substitute the value from the dashboard's API keys page or env.FP_PROJECT_KEY.\",\n ),\n collectorUrl: z\n .string()\n .url()\n .describe(\n 'Same-origin collector URL — typically https://<their-domain>/fpjs (proxied by a Cloudflare Worker).',\n ),\n source: z\n .enum(['npm', 'cdn'])\n .default('npm')\n .describe('Match whatever `install_sdk` ran with. Defaults to `npm`.'),\n};\n\n/**\n * `wire_init` — produce a unified diff that inserts the framework-\n * idiomatic init snippet into the user's entry file. Reuses\n * `initSnippet()` from `@fingerprint79/sdk-snippets` so the dashboard's\n * snippet panel and this MCP tool emit byte-identical code.\n *\n * Strategy per framework:\n * - `nextjs` / `react` / `preact`: insert as a NEW client component\n * file `app/_components/FpClient.tsx` (or\n * `src/components/FpClient.tsx`) — much safer than splicing into\n * an existing `app/layout.tsx` whose JSX shape we can't predict.\n * Caller mounts <FpClient /> in their root tree as a follow-up.\n * - `vue` / `svelte`: insert as a NEW plugin module the user\n * registers.\n * - `angular`: insert as a NEW Injectable service.\n * - `javascript` / `javascript-spa`: append the script to the\n * entry file directly (the file is already a script entry).\n *\n * For HTML entries (vanilla source=cdn), we splice a `<script>` tag\n * into the existing file using `makeUnifiedDiff` against the actual\n * file contents.\n */\nexport function wireInit(input: {\n cwd: string;\n framework: Framework;\n entryFile: string;\n apiKey: string;\n collectorUrl: string;\n source?: 'npm' | 'cdn';\n}): ToolEnvelope {\n const cwd = resolve(input.cwd);\n const entryFile = resolve(input.entryFile);\n const source = input.source ?? 'npm';\n\n const snippet = initSnippet({\n framework: input.framework,\n source,\n apiKey: input.apiKey,\n collectorUrl: input.collectorUrl,\n });\n\n // CDN flow: append <script> tag to the entry HTML.\n if (source === 'cdn') {\n if (!existsSync(entryFile)) {\n return missingEntryFile(entryFile, input.framework);\n }\n const current = readFileSync(entryFile, 'utf8');\n const next = appendBeforeBodyClose(current, snippet);\n const diff = makeUnifiedDiff({ cwd, filePath: entryFile, newContent: next });\n return successDiff(diff, entryFile, input.framework, source, [\n `Apply the diff to \\`${entryFile}\\` via your edit tool.`,\n `Reload the page and confirm \\`window.FP.init\\` is defined in DevTools.`,\n ]);\n }\n\n // NPM flow: emit a NEW file with the per-framework module wrapper.\n // The chosen path follows each framework's convention.\n const targetFile = perFrameworkTargetFile(input.framework, cwd);\n const diff = makeNewFileDiff({ cwd, filePath: targetFile, content: snippet });\n\n const mountHint = perFrameworkMountHint(input.framework);\n return successDiff(diff, targetFile, input.framework, source, [\n `Apply the diff to create \\`${targetFile}\\` via your edit tool.`,\n mountHint,\n `Run the app and confirm the SDK boots — open DevTools, you should see the eventId / visitorId logged.`,\n ]);\n}\n\n/* ── helpers ───────────────────────────────────────────────────── */\n\nfunction successDiff(\n diff: string,\n filePath: string,\n framework: Framework,\n source: 'npm' | 'cdn',\n nextSteps: string[],\n): ToolEnvelope {\n return {\n summary: `Wrote ${source} init for ${frameworkLabel(framework)} into \\`${filePath}\\`.`,\n details:\n `Apply the unified diff below with \\`git apply\\` (or your AI ` +\n `assistant's edit tool). The patch is against \\`${filePath}\\` and ` +\n `is generated from the same \\`initSnippet()\\` helper the dashboard's ` +\n `Get Started page uses — so what you see here matches the install ` +\n `snippet shown there byte-for-byte.\\n\\n` +\n '```diff\\n' +\n diff +\n '\\n```',\n nextSteps,\n artifact: { kind: 'diff', content: diff },\n meta: { framework, source, filePath },\n };\n}\n\nfunction missingEntryFile(entryFile: string, framework: Framework): ToolEnvelope {\n return {\n summary: `Entry file \\`${entryFile}\\` not found.`,\n details:\n `The path we were given doesn't exist. Re-run \\`analyze_project\\` ` +\n `to refresh the detection, or ask the user where their ${frameworkLabel(framework)} ` +\n `entry document lives (typically \\`public/index.html\\`, \\`app.html\\`, or the ` +\n `framework's root template).`,\n nextSteps: [`Call \\`analyze_project\\` again to re-detect, or accept the user's manual path.`],\n meta: { error: 'entry_not_found', entryFile },\n };\n}\n\n/**\n * Per-framework target path for the NEW init module. Mirrors the\n * code comments inside `initSnippet()` (\"// app/_components/FpClient.tsx\",\n * \"// plugins/fingerprint.ts\", etc.).\n */\nfunction perFrameworkTargetFile(framework: Framework, cwd: string): string {\n const map: Record<Framework, string> = {\n nextjs: 'app/_components/FpClient.tsx',\n react: 'src/hooks/useFingerprint.ts',\n vue: 'src/plugins/fingerprint.ts',\n angular: 'src/app/fingerprint.service.ts',\n svelte: 'src/lib/fingerprint.ts',\n preact: 'src/hooks/useFingerprint.ts',\n 'javascript-spa': 'src/fingerprint.ts',\n javascript: 'src/fingerprint.ts',\n };\n // Use `dirname` once to be safe across platforms before joining.\n return resolve(cwd, map[framework]);\n}\n\nfunction perFrameworkMountHint(framework: Framework): string {\n switch (framework) {\n case 'nextjs':\n return 'Mount `<FpClient />` once in `app/layout.tsx` so it boots on every route.';\n case 'react':\n case 'preact':\n return 'Call `useFingerprint()` once in your root `App` component.';\n case 'vue':\n return 'In `main.ts`: `import fpPlugin from \"./plugins/fingerprint\"; app.use(fpPlugin);`. Then from any component call `this.$fp.identify({ event: \"pageview\" })`.';\n case 'angular':\n return 'Inject `FingerprintService` in any component and call `this.fp.fp.identify({ event: \"pageview\" })`.';\n case 'svelte':\n return 'In a `+page.svelte`: `import { fp } from \"$lib/fingerprint\"; onMount(() => fp.identify({ event: \"pageview\" }));`.';\n case 'javascript-spa':\n return \"Hook your router's `onNavigate` to call `fp.identify({ event: 'route:' + path })`.\";\n case 'javascript':\n default:\n return 'Import the module from your entry point so the `fp.identify()` call fires on load.';\n }\n}\n\n/**\n * Splice the snippet immediately before the closing `</body>` tag.\n * If no `</body>` is present (loose HTML or a server-rendered shell\n * that doesn't include it), append at end-of-file.\n */\nfunction appendBeforeBodyClose(html: string, snippet: string): string {\n const lower = html.toLowerCase();\n const idx = lower.lastIndexOf('</body>');\n if (idx === -1) {\n const trailingNewline = html.endsWith('\\n') ? '' : '\\n';\n return `${html}${trailingNewline}${snippet}\\n`;\n }\n const before = html.slice(0, idx);\n const after = html.slice(idx);\n return `${before}${snippet}\\n${after}`;\n}\n\n// Pin `dirname` so unused-imports lint doesn't strip it (we want the\n// helper available for future per-framework heuristics that need the\n// directory of the entry file).\nvoid dirname;\n"],"mappings":";;;AAAA,SAAS,4BAA4B;;;ACArC,SAAS,iBAAiB;;;ACA1B,SAAS,WAAAA,gBAAe;;;AC0CjB,IAAM,aAA8D;EACzE,EAAE,IAAI,cAAc,OAAO,aAAY;EACvC,EAAE,IAAI,kBAAkB,OAAO,iBAAgB;EAC/C,EAAE,IAAI,UAAU,OAAO,UAAS;EAChC,EAAE,IAAI,SAAS,OAAO,QAAO;EAC7B,EAAE,IAAI,WAAW,OAAO,UAAS;EACjC,EAAE,IAAI,OAAO,OAAO,SAAQ;EAC5B,EAAE,IAAI,UAAU,OAAO,SAAQ;EAC/B,EAAE,IAAI,UAAU,OAAO,SAAQ;;AAG3B,SAAU,eAAe,IAAa;AAC1C,SAAO,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS;AACvD;AA+BM,SAAU,YAAY,OAAmB;AAC7C,QAAM,EAAE,WAAW,QAAQ,QAAQ,aAAY,IAAK;AACpD,QAAM,SAAS,oBAAoB,YAAY;mBAC9B,MAAM;AAQvB,MAAI,WAAW,OAAO;AACpB,WAAO,GAAG,oBAAoB,SAAS,CAAC;;;;EAI1C,MAAM;;;;;;;;;EASN;AAGA,UAAQ,WAAW;IACjB,KAAK;AACH,aAAO;;;;;;;;;;;;EAYX,OAAO,QAAQ,OAAO,IAAI,CAAC;;;;;;;;;;IAWzB,KAAK;AACH,aAAO;;;;;;EAMX,OAAO,QAAQ,OAAO,MAAM,CAAC;;;;;;;;;;IAW3B,KAAK;AACH,aAAO;;;;EAIX,MAAM;;;;;;;;;;;;;IAcJ,KAAK;AACH,aAAO;;;;;;;EAOX,OAAO,QAAQ,OAAO,MAAM,CAAC;;;;;IAM3B,KAAK;AACH,aAAO;;;;EAIX,MAAM;;;;;;;;;IAUJ,KAAK;AACH,aAAO;;;;;;EAMX,OAAO,QAAQ,OAAO,MAAM,CAAC;;;;;;;IAQ3B,KAAK;AACH,aAAO;;;;;EAKX,MAAM;;;;;IAMJ,KAAK;IACL;AACE,aAAO;;;EAGX,MAAM;;;;;;;EAON;AACF;AASA,SAAS,oBAAoB,WAAoB;AAC/C,UAAQ,WAAW;IACjB,KAAK;AACH,aAAO;;IAET,KAAK;AACH,aAAO;;IAET,KAAK;AACH,aAAO;;IAET,KAAK;AACH,aAAO;;IAET,KAAK;AACH,aAAO;;IAET,KAAK;AACH,aAAO;;IAET,KAAK;AACH,aAAO;;IAET,KAAK;IACL;AACE,aAAO;;EAEX;AACF;;;ADvRA,SAAS,SAAS;;;AEHlB,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AAqCjB,SAAS,gBAAgB,KAA0B;AACxD,QAAM,UAAU,gBAAgB,GAAG;AACnC,QAAM,OAAO,YAAY,OAAO;AAChC,QAAM,eAAe,8BAA8B;AAGnD,MAAI,UAAU,MAAM;AAClB,WAAO,KAAK,UAAU,KAAK,cAAc,6CAA6C;AAAA,EACxF;AACA,MAAI,UAAU,MAAM;AAIlB,WAAO,KAAK,OAAO,KAAK,cAAc,yDAAoD;AAAA,EAC5F;AACA,MAAI,mBAAmB,MAAM;AAC3B,WAAO,KAAK,WAAW,KAAK,cAAc,sCAAsC;AAAA,EAClF;AACA,MAAI,mBAAmB,QAAQ,YAAY,MAAM;AAC/C,WAAO,KAAK,UAAU,KAAK,cAAc,iCAAiC;AAAA,EAC5E;AACA,MAAI,YAAY,MAAM;AACpB,WAAO,KAAK,UAAU,KAAK,cAAc,+BAA+B;AAAA,EAC1E;AACA,MAAI,SAAS,MAAM;AACjB,WAAO,KAAK,OAAO,KAAK,cAAc,4BAA4B;AAAA,EACpE;AACA,MAAI,WAAW,QAAQ,eAAe,MAAM;AAK1C,WAAO,KAAK,SAAS,KAAK,cAAc,8BAA8B;AAAA,EACxE;AAKA,MAAI,WAAW,QAAQ,KAAK,gBAAgB,CAAC,KAAK,WAAW,QAAQ,KAAK,gBAAgB,CAAC,GAAG;AAC5F,WAAO,KAAK,UAAU,KAAK,cAAc,gDAAgD;AAAA,EAC3F;AACA,MAAI,WAAW,QAAQ,KAAK,cAAc,CAAC,GAAG;AAC5C,WAAO,KAAK,WAAW,KAAK,cAAc,0BAA0B;AAAA,EACtE;AACA,MACE,WAAW,QAAQ,KAAK,kBAAkB,CAAC,KAC3C,WAAW,QAAQ,KAAK,kBAAkB,CAAC,GAC3C;AACA,WAAO,KAAK,UAAU,KAAK,cAAc,6BAA6B;AAAA,EACxE;AAKA,MAAI,kBAAkB,QAAQ,gBAAgB,QAAQ,oBAAoB,MAAM;AAC9E,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,KAAK,cAAc,KAAK,cAAc,wDAAmD;AAClG;AAIA,SAAS,gBAAgB,KAA+B;AACtD,QAAM,IAAI,QAAQ,KAAK,cAAc;AACrC,MAAI,CAAC,WAAW,CAAC,EAAG,QAAO,CAAC;AAC5B,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,GAAG,MAAM,CAAC;AAAA,EAC3C,QAAQ;AAGN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,YAAY,KAA+C;AAClE,SAAO;AAAA,IACL,GAAG,IAAI;AAAA,IACP,GAAG,IAAI;AAAA,IACP,GAAG,IAAI;AAAA,EACT;AACF;AAEA,SAAS,KAAK,WAAsB,KAAa,QAAiB,WAAgC;AAChG,SAAO;AAAA,IACL;AAAA,IACA,WAAW,gBAAgB,WAAW,GAAG;AAAA,IACzC;AAAA,IACA;AAAA,EACF;AACF;AAQA,SAAS,gBAAgB,WAAsB,KAAiC;AAC9E,QAAM,aAAmD;AAAA,IACvD,QAAQ,CAAC,kBAAkB,kBAAkB,sBAAsB,gBAAgB;AAAA,IACnF,OAAO,CAAC,gBAAgB,gBAAgB,iBAAiB,iBAAiB,aAAa;AAAA,IACvF,KAAK,CAAC,eAAe,aAAa;AAAA,IAClC,SAAS,CAAC,eAAe,uBAAuB;AAAA,IAChD,QAAQ,CAAC,6BAA6B,eAAe,aAAa;AAAA,IAClE,QAAQ,CAAC,gBAAgB,iBAAiB,eAAe;AAAA,IACzD,kBAAkB,CAAC,eAAe,eAAe,cAAc;AAAA,IAC/D,YAAY,CAAC,cAAc,kBAAkB,mBAAmB;AAAA,EAClE;AACA,aAAW,OAAO,WAAW,SAAS,GAAG;AACvC,UAAM,MAAM,QAAQ,KAAK,GAAG;AAC5B,QAAI,WAAW,GAAG,EAAG,QAAO;AAAA,EAC9B;AACA,SAAO;AACT;;;AC7JA,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,WAAAC,gBAAe;AAoBjB,SAAS,qBAAqB,KAA6B;AAChE,MAAID,YAAWC,SAAQ,KAAK,gBAAgB,CAAC,EAAG,QAAO;AACvD,MAAID,YAAWC,SAAQ,KAAK,WAAW,CAAC,EAAG,QAAO;AAClD,MAAID,YAAWC,SAAQ,KAAK,WAAW,CAAC,KAAKD,YAAWC,SAAQ,KAAK,UAAU,CAAC,EAAG,QAAO;AAC1F,SAAO;AACT;AAQO,SAAS,qBAAqB,IAAoB,KAAqB;AAC5E,UAAQ,IAAI;AAAA,IACV,KAAK;AACH,aAAO,YAAY,GAAG;AAAA,IACxB,KAAK;AACH,aAAO,YAAY,GAAG;AAAA,IACxB,KAAK;AACH,aAAO,WAAW,GAAG;AAAA,IACvB,KAAK;AAAA,IACL;AACE,aAAO,eAAe,GAAG;AAAA,EAC7B;AACF;;;AHpCO,IAAM,qBAAqB;AAAA,EAChC,KAAK,EACF,OAAO,EACP;AAAA,IACC;AAAA,EACF;AACJ;AAcO,SAAS,eAAe,OAAsC;AACnE,QAAM,MAAMC,SAAQ,MAAM,GAAG;AAC7B,QAAMC,QAAO,gBAAgB,GAAG;AAChC,QAAM,KAAK,qBAAqB,GAAG;AAEnC,QAAM,YAAsB,CAAC;AAC7B,MAAI,CAACA,MAAK,QAAQ;AAChB,cAAU,KAAK,kCAAkC,GAAG,wCAAwC;AAAA,EAC9F;AACA,MAAIA,MAAK,WAAW;AAClB,cAAU;AAAA,MACR,gCAAgC,GAAG,iBAAiBA,MAAK,SAAS,iBAAiBA,MAAK,SAAS;AAAA,IACnG;AAAA,EACF,OAAO;AACL,cAAU;AAAA,MACR,0CAA0CA,MAAK,SAAS;AAAA,IAC1D;AAAA,EACF;AACA,YAAU;AAAA,IACR;AAAA,EACF;AAEA,QAAM,UACJ;AAAA;AAAA,iBACkBA,MAAK,SAAS,OAAO,eAAeA,MAAK,SAAS,CAAC;AAAA,uBAC7C,EAAE;AAAA,2BACEA,MAAK,SAAS,QAAQ,IAAI;AAAA,gBACrCA,MAAK,YAAY,KAAKA,MAAK,SAAS,OAAO,qBAAqB;AAAA;AAAA,GAC7EA,MAAK,SAAS;AAEpB,SAAO;AAAA,IACL,SAAS,YAAY,eAAeA,MAAK,SAAS,CAAC,aAAa,EAAE,SAASA,MAAK,SAAS,cAAc,SAAS;AAAA,IAChH;AAAA,IACA;AAAA,IACA,MAAM;AAAA,MACJ;AAAA,MACA,WAAWA,MAAK;AAAA,MAChB,gBAAgB;AAAA,MAChB,WAAWA,MAAK,aAAa;AAAA,MAC7B,QAAQA,MAAK;AAAA,IACf;AAAA,EACF;AACF;;;AIxEA,SAAS,WAAAC,gBAAe;AAExB,SAAS,KAAAC,UAAS;;;ACFlB,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,gBAAgB;AAEzB,SAAS,mBAAmB;AAerB,SAAS,gBAAgB,MAIrB;AACT,QAAM,aAAaD,YAAW,KAAK,QAAQ,IAAIC,cAAa,KAAK,UAAU,MAAM,IAAI;AACrF,QAAM,MAAM,SAAS,KAAK,KAAK,KAAK,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,GAAG;AAClE,SAAO,YAAY,KAAK,YAAY,KAAK,YAAY,IAAI,IAAI,EAAE,SAAS,EAAE,CAAC;AAC7E;AAUO,SAAS,gBAAgB,MAIrB;AACT,QAAM,MAAM,SAAS,KAAK,KAAK,KAAK,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,GAAG;AAClE,SAAO,YAAY,KAAK,IAAI,KAAK,SAAS,IAAI,IAAI,EAAE,SAAS,EAAE,CAAC;AAClE;;;ADjCO,IAAM,6BAA6B;AAAA,EACxC,YAAYC,GAAE,OAAO,EAAE,SAAS,2BAA2B;AAAA,EAC3D,cAAcA,GACX,OAAO,EACP,IAAI,EACJ;AAAA,IACC;AAAA,EACF;AACJ;AAWO,SAAS,gBAAgB,OAAmE;AACjG,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,MAAM,YAAY;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,KAAK,MAAM,YAAY;AAAA,MAChC,MAAM,EAAE,OAAO,cAAc;AAAA,IAC/B;AAAA,EACF;AACA,MAAI,OAAO,aAAa,YAAY,OAAO,aAAa,aAAa;AACnE,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SACE,+FACK,OAAO,QAAQ,KAAK,OAAO,QAAQ;AAAA,MAC1C,MAAM,EAAE,OAAO,qBAAqB;AAAA,IACtC;AAAA,EACF;AAEA,QAAM,wBAAwB,GAAG,OAAO,QAAQ,KAAK,OAAO,QAAQ,GAAG,OAAO,OAAO,IAAI,OAAO,IAAI,KAAK,EAAE;AAE3G,SAAO;AAAA,IACL,SAAS,SAAS,MAAM,YAAY;AAAA,IACpC,SACE;AAAA;AAAA;AAAA,iBAEkB,MAAM,UAAU;AAAA,mBACd,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAIjC,qBAAqB;AAAA;AAAA;AAAA,IAI5B,WAAW;AAAA,MACT;AAAA,MACA,gDAAgD,qBAAqB;AAAA,MACrE;AAAA,IACF;AAAA,IACA,UAAU;AAAA,MACR,MAAM;AAAA,MACN,SAAS;AAAA,QACP,gBAAgB,MAAM;AAAA,QACtB,kBAAkB,MAAM;AAAA,MAC1B;AAAA,IACF;AAAA,IACA,MAAM,EAAE,QAAQ,uBAAuB,cAAc,MAAM,aAAa;AAAA,EAC1E;AACF;AAIO,IAAM,sCAAsC;AAAA,EACjD,KAAKA,GAAE,OAAO,EAAE,SAAS,oEAA+D;AAAA,EACxF,eAAeA,GACZ,OAAO,EACP;AAAA,IACC;AAAA,EACF;AAAA,EACF,cAAcA,GACX,OAAO,EACP;AAAA,IACC;AAAA,EACF;AACJ;AASO,SAAS,yBAAyB,OAIxB;AACf,QAAM,MAAMC,SAAQ,MAAM,GAAG;AAC7B,QAAM,aAAaA,SAAQ,KAAK,sBAAsB;AACtD,QAAM,eAAeA,SAAQ,KAAK,0BAA0B;AAG5D,QAAM,OAAO,MAAM,cAAc,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAC/E,QAAM,SAAS,MAAM,aAAa,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAEhF,QAAM,eAAe,yBAAyB,IAAI;AAClD,QAAM,eAAe,qBAAqB,MAAM;AAEhD,QAAM,aAAa,gBAAgB,EAAE,KAAK,UAAU,YAAY,SAAS,aAAa,CAAC;AACvF,QAAM,eAAe,gBAAgB,EAAE,KAAK,UAAU,cAAc,SAAS,aAAa,CAAC;AAE3F,SAAO;AAAA,IACL,SAAS,oCAAoC,MAAM,sBAAiB,IAAI;AAAA,IACxE,SACE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAQa,MAAM;AAAA;AAAA;AAAA,MAEZ,UAAU;AAAA;AAAA;AAAA,EAAuB,UAAU;AAAA;AAAA;AAAA,MAC3C,YAAY;AAAA;AAAA;AAAA,EAAuB,YAAY;AAAA;AAAA,IACxD,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,oDAAoD,MAAM;AAAA,MAC1D,iBAAiB,MAAM;AAAA,IACzB;AAAA;AAAA;AAAA,IAGA,UAAU,EAAE,MAAM,QAAQ,SAAS,GAAG,UAAU;AAAA,EAAK,YAAY,GAAG;AAAA,IACpE,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,OAAO,GAAG,MAAM;AAAA,MAChB,eAAe;AAAA,IACjB;AAAA,EACF;AACF;AAIO,IAAM,iCAAiC;AAAA,EAC5C,WAAWD,GACR,OAAO,EACP,SAAS,8EAA8E;AAAA,EAC1F,eAAeA,GAAE,OAAO,EAAE,SAAS,gDAAgD;AACrF;AASO,SAAS,oBAAoB,OAGnB;AACf,QAAM,OAAO,MAAM,cAAc,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAI/E,QAAM,OAAO,MAAM,UAAU,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG;AAEzD,SAAO;AAAA,IACL,SAAS,eAAe,MAAM,SAAS,eAAU,IAAI;AAAA,IACrD,SACE,sEACK,IAAI;AAAA;AAAA;AAAA;AAAA,SAGC,MAAM,UAAU,QAAQ,IAAI,IAAI,IAAI,EAAE,CAAC;AAAA,SACvC,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAKc,MAAM,SAAS;AAAA;AAAA;AAAA,qBAErB,MAAM,SAAS;AAAA;AAAA;AAAA,IAMvC,WAAW;AAAA,MACT;AAAA,MACA,+BAA+B,MAAM,SAAS;AAAA,MAC9C;AAAA,IACF;AAAA,IACA,MAAM,EAAE,WAAW,MAAM,WAAW,eAAe,MAAM,KAAK;AAAA,EAChE;AACF;AAIA,SAAS,yBAAyB,eAA+B;AAC/D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAciB,aAAa;AAAA;AAAA;AAAA,oCAGH,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2CjD;AAEA,SAAS,qBAAqB,cAA8B;AAC1D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAQQ,YAAY,0BAA0B,YAAY;AAAA;AAAA;AAGnE;;;AEpSA,SAAS,gBAAAE,qBAAoB;AAC7B,SAAS,SAAS,WAAAC,gBAAe;AACjC,SAAS,qBAAqB;AAE9B,SAAS,KAAAC,UAAS;AAIlB,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAQxD,IAAM,cAAsC;AAAA,EAC1C,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,sBAAsB;AAAA,EACtB,iBAAiB;AACnB;AAEO,IAAM,qBAAqB;AAAA,EAChC,OAAOA,GACJ,KAAK;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA;AAAA,IACC;AAAA,EACF;AACJ;AAWO,SAAS,aAAa,OAA0D;AACrF,QAAM,WAAW,YAAY,MAAM,KAAK;AACxC,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,MACL,SAAS,kBAAkB,MAAM,KAAK;AAAA,MACtC,SAAS,iBAAiB,OAAO,KAAK,WAAW,EAAE,KAAK,IAAI,CAAC;AAAA,MAC7D,MAAM,EAAE,OAAO,gBAAgB;AAAA,IACjC;AAAA,EACF;AAGA,QAAM,aAAa;AAAA,IACjBD,SAAQ,WAAW,MAAM,QAAQ,QAAQ;AAAA,IACzCA,SAAQ,WAAW,QAAQ,QAAQ;AAAA,IACnCA,SAAQ,WAAW,MAAM,MAAM,OAAO,QAAQ,QAAQ;AAAA,EACxD;AACA,aAAW,QAAQ,YAAY;AAC7B,QAAI;AACF,YAAM,UAAUD,cAAa,MAAM,MAAM;AACzC,aAAO;AAAA,QACL,SAAS,iBAAiB,QAAQ,KAAK,QAAQ,MAAM;AAAA,QACrD,SAAS;AAAA,QACT,UAAU,EAAE,MAAM,YAAY,QAAQ;AAAA,QACtC,MAAM,EAAE,OAAO,MAAM,OAAO,KAAK;AAAA,MACnC;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS,QAAQ,QAAQ;AAAA,IACzB,SACE,cAAc,WAAW,KAAK,IAAI,CAAC;AAAA,IAGrC,MAAM,EAAE,OAAO,eAAe,OAAO,MAAM,MAAM;AAAA,EACnD;AACF;;;ACvFA,SAAS,WAAAG,gBAAe;AAExB,SAAS,KAAAC,UAAS;AAUX,IAAM,qBAAqB;AAAA,EAChC,KAAKC,GAAE,OAAO,EAAE,SAAS,yCAAyC;AAAA,EAClE,gBAAgBA,GACb,KAAK,CAAC,QAAQ,QAAQ,OAAO,KAAK,CAAC,EACnC,SAAS,EACT;AAAA,IACC;AAAA,EACF;AAAA,EACF,QAAQA,GACL,KAAK,CAAC,OAAO,KAAK,CAAC,EACnB,QAAQ,KAAK,EACb;AAAA,IACC;AAAA,EACF;AACJ;AAQO,SAAS,WAAW,OAIV;AACf,QAAM,MAAMC,SAAQ,MAAM,GAAG;AAC7B,QAAM,SAAS,MAAM,UAAU;AAE/B,MAAI,WAAW,OAAO;AACpB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SACE;AAAA,MAQF,WAAW,CAAC,yEAAyE;AAAA,MACrF,UAAU;AAAA,QACR,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,MACA,MAAM,EAAE,QAAQ,OAAO,IAAI;AAAA,IAC7B;AAAA,EACF;AAEA,QAAM,KAAK,MAAM,kBAAkB,qBAAqB,GAAG;AAC3D,QAAM,MAAM,qBAAqB,IAAI,0BAA0B;AAE/D,SAAO;AAAA,IACL,SAAS,eAAe,EAAE,OAAO,GAAG;AAAA,IACpC,SACE,oDAAoD,GAAG;AAAA;AAAA;AAAA,IAEvD,MACA;AAAA;AAAA;AAAA,6BAC8B,EAAE;AAAA,IAClC,WAAW;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU,EAAE,MAAM,WAAW,SAAS,KAAK,IAAI;AAAA,IAC/C,MAAM,EAAE,QAAQ,OAAO,gBAAgB,IAAI,KAAK,SAAS,IAAI;AAAA,EAC/D;AACF;;;ACjFA,SAAS,cAAAC,aAAY,aAAa,gBAAAC,eAAc,gBAAgB;AAChE,SAAS,WAAAC,gBAAe;AAExB,SAAS,KAAAC,UAAS;AAMX,IAAM,2BAA2B;AAAA,EACtC,KAAKC,GAAE,OAAO,EAAE,SAAS,yCAAyC;AACpE;AAsBO,SAAS,cAAc,OAAsC;AAClE,QAAM,MAAMC,SAAQ,MAAM,GAAG;AAC7B,QAAMC,QAAO,gBAAgB,GAAG;AAChC,QAAM,SAAkB,CAAC;AAGzB,SAAO,KAAK;AAAA,IACV,MAAM;AAAA,IACN,QAAQA,MAAK,SAAS,OAAO;AAAA,IAC7B,MAAMA,MAAK,SACP,+BACA;AAAA,EACN,CAAC;AAMD,QAAM,gBAAgB,gBAAgB,GAAG;AACzC,SAAO,KAAK;AAAA,IACV,MAAM;AAAA,IACN,QAAQ,gBAAgB,OAAO;AAAA,IAC/B,MAAM,gBACF,8EACA;AAAA,EAEN,CAAC;AAID,QAAM,WAAW,aAAa,GAAG;AACjC,SAAO,KAAK;AAAA,IACV,MAAM;AAAA,IACN,QAAQ,SAAS,OAAO,OAAO,SAAS,SAAS,SAAS;AAAA,IAC1D,MAAM,SAAS;AAAA,EACjB,CAAC;AAED,QAAM,YAAY,OAAO,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,EAAE;AAC5D,QAAM,YAAY,OAAO,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,EAAE;AAC5D,QAAM,UACJ,YAAY,IACR,GAAG,SAAS,kBAAkB,SAAS,6BACvC,YAAY,IACV,sBAAiB,SAAS,mCAC1B;AAER,SAAO;AAAA,IACL;AAAA,IACA,SACE;AAAA;AAAA,IACA,OAAO,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,aAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI;AAAA,IAC/E,WACE,cAAc,KAAK,cAAc,IAC7B;AAAA,MACE;AAAA,MACA;AAAA,IACF,IACA,CAAC,6EAA6E;AAAA,IACpF,MAAM,EAAE,QAAQ,WAAWA,MAAK,UAAU;AAAA,EAC5C;AACF;AAIA,SAAS,MAAM,QAAwC;AACrD,SAAO,WAAW,OAAO,WAAM,WAAW,SAAS,WAAM;AAC3D;AAEA,SAAS,gBAAgB,KAAsB;AAI7C,MAAI,QAAQ;AACZ,QAAM,QAAkB,CAAC,GAAG;AAC5B,QAAM,OAAO,oBAAI,IAAY;AAC7B,SAAO,MAAM,SAAS,KAAK,QAAQ,KAAM;AACvC,UAAM,MAAM,MAAM,IAAI;AACtB,QAAI,CAAC,OAAO,KAAK,IAAI,GAAG,EAAG;AAC3B,SAAK,IAAI,GAAG;AACZ,QAAI,0DAA0D,KAAK,GAAG,EAAG;AACzE,QAAI;AACJ,QAAI;AACF,gBAAU,YAAY,GAAG;AAAA,IAC3B,QAAQ;AACN;AAAA,IACF;AACA,eAAW,QAAQ,SAAS;AAC1B;AACA,UAAI,SAAS,IAAM;AACnB,YAAM,MAAMD,SAAQ,KAAK,IAAI;AAC7B,UAAI;AACJ,UAAI;AACF,eAAO,SAAS,GAAG;AAAA,MACrB,QAAQ;AACN;AAAA,MACF;AACA,UAAI,KAAK,YAAY,GAAG;AACtB,cAAM,KAAK,GAAG;AACd;AAAA,MACF;AACA,UAAI,CAAC,uCAAuC,KAAK,IAAI,EAAG;AACxD,UAAI;AACF,cAAM,MAAME,cAAa,KAAK,MAAM;AACpC,YAAI,0DAA0D,KAAK,GAAG,GAAG;AACvE,iBAAO;AAAA,QACT;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,KAA+D;AACnF,QAAM,cAAc;AAAA,IAClB,KAAK,QAAQ,QAAQ,IAAI,gBAAgB,CAAC;AAAA,IAC1C,KAAK,QAAQ,QAAQ,IAAI,kBAAkB,CAAC;AAAA,EAC9C;AACA,QAAM,cAAc,CAAC,QAAQ,cAAc,oBAAoB,iBAAiB;AAChF,QAAM,eAAe,EAAE,KAAK,OAAO,KAAK,MAAM;AAC9C,aAAW,QAAQ,aAAa;AAC9B,UAAM,IAAIF,SAAQ,KAAK,IAAI;AAC3B,QAAI,CAACG,YAAW,CAAC,EAAG;AACpB,QAAI;AACF,YAAM,MAAMD,cAAa,GAAG,MAAM;AAClC,UAAI,0BAA0B,KAAK,GAAG,EAAG,cAAa,MAAM;AAC5D,UAAI,4BAA4B,KAAK,GAAG,EAAG,cAAa,MAAM;AAAA,IAChE,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,QAAQ,YAAY,OAAO,aAAa;AAC9C,QAAM,QAAQ,YAAY,OAAO,aAAa;AAC9C,QAAM,OAAO,SAAS;AACtB,QAAM,SAAS,SAAS;AACxB,QAAM,UAAoB,CAAC;AAC3B,MAAI,CAAC,MAAO,SAAQ,KAAK,gBAAgB;AACzC,MAAI,CAAC,MAAO,SAAQ,KAAK,kBAAkB;AAC3C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,MAAM,OACF,qDACA,YAAY,QAAQ,KAAK,IAAI,CAAC;AAAA,EACpC;AACF;;;ACnLA,SAAS,cAAAE,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,WAAAC,UAAS,WAAAC,gBAAe;AAGjC,SAAS,KAAAC,UAAS;AAMX,IAAM,sBAAsB;AAAA,EACjC,KAAKC,GAAE,OAAO,EAAE,SAAS,yCAAyC;AAAA,EAClE,WAAWA,GACR,KAAK,CAAC,cAAc,kBAAkB,UAAU,SAAS,OAAO,WAAW,UAAU,QAAQ,CAAC,EAC9F,SAAS,sCAAsC;AAAA,EAClD,WAAWA,GACR,OAAO,EACP;AAAA,IACC;AAAA,EACF;AAAA,EACF,QAAQA,GACL,OAAO,EACP;AAAA,IACC;AAAA,EACF;AAAA,EACF,cAAcA,GACX,OAAO,EACP,IAAI,EACJ;AAAA,IACC;AAAA,EACF;AAAA,EACF,QAAQA,GACL,KAAK,CAAC,OAAO,KAAK,CAAC,EACnB,QAAQ,KAAK,EACb,SAAS,2DAA2D;AACzE;AAwBO,SAAS,SAAS,OAOR;AACf,QAAM,MAAMC,SAAQ,MAAM,GAAG;AAC7B,QAAM,YAAYA,SAAQ,MAAM,SAAS;AACzC,QAAM,SAAS,MAAM,UAAU;AAE/B,QAAM,UAAU,YAAY;AAAA,IAC1B,WAAW,MAAM;AAAA,IACjB;AAAA,IACA,QAAQ,MAAM;AAAA,IACd,cAAc,MAAM;AAAA,EACtB,CAAC;AAGD,MAAI,WAAW,OAAO;AACpB,QAAI,CAACC,YAAW,SAAS,GAAG;AAC1B,aAAO,iBAAiB,WAAW,MAAM,SAAS;AAAA,IACpD;AACA,UAAM,UAAUC,cAAa,WAAW,MAAM;AAC9C,UAAM,OAAO,sBAAsB,SAAS,OAAO;AACnD,UAAMC,QAAO,gBAAgB,EAAE,KAAK,UAAU,WAAW,YAAY,KAAK,CAAC;AAC3E,WAAO,YAAYA,OAAM,WAAW,MAAM,WAAW,QAAQ;AAAA,MAC3D,uBAAuB,SAAS;AAAA,MAChC;AAAA,IACF,CAAC;AAAA,EACH;AAIA,QAAM,aAAa,uBAAuB,MAAM,WAAW,GAAG;AAC9D,QAAM,OAAO,gBAAgB,EAAE,KAAK,UAAU,YAAY,SAAS,QAAQ,CAAC;AAE5E,QAAM,YAAY,sBAAsB,MAAM,SAAS;AACvD,SAAO,YAAY,MAAM,YAAY,MAAM,WAAW,QAAQ;AAAA,IAC5D,8BAA8B,UAAU;AAAA,IACxC;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAIA,SAAS,YACP,MACA,UACA,WACA,QACA,WACc;AACd,SAAO;AAAA,IACL,SAAS,SAAS,MAAM,aAAa,eAAe,SAAS,CAAC,WAAW,QAAQ;AAAA,IACjF,SACE,8GACkD,QAAQ;AAAA;AAAA;AAAA,IAK1D,OACA;AAAA,IACF;AAAA,IACA,UAAU,EAAE,MAAM,QAAQ,SAAS,KAAK;AAAA,IACxC,MAAM,EAAE,WAAW,QAAQ,SAAS;AAAA,EACtC;AACF;AAEA,SAAS,iBAAiB,WAAmB,WAAoC;AAC/E,SAAO;AAAA,IACL,SAAS,gBAAgB,SAAS;AAAA,IAClC,SACE,0HACyD,eAAe,SAAS,CAAC;AAAA,IAGpF,WAAW,CAAC,gFAAgF;AAAA,IAC5F,MAAM,EAAE,OAAO,mBAAmB,UAAU;AAAA,EAC9C;AACF;AAOA,SAAS,uBAAuB,WAAsB,KAAqB;AACzE,QAAM,MAAiC;AAAA,IACrC,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,KAAK;AAAA,IACL,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,YAAY;AAAA,EACd;AAEA,SAAOH,SAAQ,KAAK,IAAI,SAAS,CAAC;AACpC;AAEA,SAAS,sBAAsB,WAA8B;AAC3D,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAOA,SAAS,sBAAsB,MAAc,SAAyB;AACpE,QAAM,QAAQ,KAAK,YAAY;AAC/B,QAAM,MAAM,MAAM,YAAY,SAAS;AACvC,MAAI,QAAQ,IAAI;AACd,UAAM,kBAAkB,KAAK,SAAS,IAAI,IAAI,KAAK;AACnD,WAAO,GAAG,IAAI,GAAG,eAAe,GAAG,OAAO;AAAA;AAAA,EAC5C;AACA,QAAM,SAAS,KAAK,MAAM,GAAG,GAAG;AAChC,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,SAAO,GAAG,MAAM,GAAG,OAAO;AAAA,EAAK,KAAK;AACtC;;;AV5KO,SAAS,cAAyB;AACvC,QAAM,SAAS,IAAI;AAAA,IACjB;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,QAKZ,OAAO,CAAC;AAAA,MACV;AAAA,MACA,cACE;AAAA,IAQJ;AAAA,EACF;AAOA,WAAS,OAAO,UAEd;AACA,UAAM,QAAkB,CAAC,SAAS,OAAO;AACzC,QAAI,SAAS,QAAS,OAAM,KAAK,SAAS,OAAO;AACjD,QAAI,SAAS,aAAa,SAAS,UAAU,SAAS,GAAG;AACvD,YAAM;AAAA,QACJ,sBAAsB,SAAS,UAAU,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI;AAAA,MACpF;AAAA,IACF;AACA,QAAI,SAAS,UAAU;AACrB,cAAQ,SAAS,SAAS,MAAM;AAAA,QAC9B,KAAK;AACH,gBAAM,KAAK,yBAAyB,SAAS,SAAS,UAAU,OAAO;AACvE;AAAA,QACF,KAAK;AACH,gBAAM;AAAA,YACJ,4BACE,SAAS,SAAS,UAClB,WACC,SAAS,SAAS,MAAM;AAAA,aAAgB,SAAS,SAAS,GAAG,SAAS;AAAA,UAC3E;AACA;AAAA,QACF,KAAK;AACH,gBAAM;AAAA,YACJ,6BACE,OAAO,QAAQ,SAAS,SAAS,OAAO,EACrC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAC3B,KAAK,IAAI,IACZ;AAAA,UACJ;AACA;AAAA,QACF,KAAK;AAEH;AAAA,MACJ;AAAA,IACF;AACA,WAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,EAAE;AAAA,EACjE;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SAAS,OAAO,eAAe,IAAI,CAAC;AAAA,EAC7C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SACL;AAAA,MACE,WAAW;AAAA,QACT,KAAK,KAAK;AAAA,QACV,QAAQ,KAAK,UAAU;AAAA,QACvB,GAAI,KAAK,iBAAiB,EAAE,gBAAgB,KAAK,eAAe,IAAI,CAAC;AAAA,MACvE,CAAC;AAAA,IACH;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SACL;AAAA,MACE,SAAS;AAAA,QACP,KAAK,KAAK;AAAA,QACV,WAAW,KAAK;AAAA,QAChB,WAAW,KAAK;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,cAAc,KAAK;AAAA,QACnB,QAAQ,KAAK,UAAU;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SAAS,OAAO,gBAAgB,IAAI,CAAC;AAAA,EAC9C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SAAS,OAAO,yBAAyB,IAAI,CAAC;AAAA,EACvD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SAAS,OAAO,oBAAoB,IAAI,CAAC;AAAA,EAClD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SAAS,OAAO,cAAc,IAAI,CAAC;AAAA,EAC5C;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,OAAO,SAAS,OAAO,aAAa,IAAI,CAAC;AAAA,EAC3C;AAEA,SAAO;AACT;;;ADnLA,eAAe,OAAsB;AACnC,QAAM,SAAS,YAAY;AAC3B,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAKhC;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AAIpB,UAAQ,MAAM,qCAAqC,GAAG;AACtD,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["resolve","existsSync","resolve","resolve","scan","resolve","z","existsSync","readFileSync","z","resolve","readFileSync","resolve","z","resolve","z","z","resolve","existsSync","readFileSync","resolve","z","z","resolve","scan","readFileSync","existsSync","existsSync","readFileSync","dirname","resolve","z","z","resolve","existsSync","readFileSync","diff"]}
@@ -0,0 +1,38 @@
1
+ # API keys
2
+
3
+ The Fingerprint Platform issues two distinct keys per project, each with a different trust scope and use site.
4
+
5
+ ## 1) Public API key — `FP_PROJECT_KEY`
6
+
7
+ A random opaque base64 string the browser SDK sends with every event as `X-Project-Key`. **Safe to embed in client-side code** — it identifies the project, not the operator. The collector enforces an `allowedHosts` allowlist (see `origin-allowlist`) so a leaked public key alone can't be used to forge events from arbitrary origins.
8
+
9
+ Get it from: Dashboard → **API keys** → copy the public key for your project.
10
+
11
+ Use in:
12
+
13
+ - `.env.local` → `FP_PROJECT_KEY=...`
14
+ - Cloudflare Pages / Vercel env vars
15
+ - Inline in the SDK init call when you don't have an env-var layer
16
+
17
+ ## 2) Server API secret — `RESULT_API_KEY`
18
+
19
+ A bearer token sent from your backend to retrieve scoring verdicts after the collector has finished scoring an event. **Server-side ONLY** — never expose in browser code or commit to a public repo. Server APIs:
20
+
21
+ - `GET /v1/result/<eventId>` — retrieve the verdict for an event.
22
+ - `POST /v1/comparison` — submit a SHA-256 identifier hash to find cross-references.
23
+
24
+ Get it from: Dashboard → **API keys** → reveal the secret. The dashboard masks it by default and only shows the full value when you click reveal.
25
+
26
+ Use in:
27
+
28
+ - Server `.env` / secret manager → `RESULT_API_KEY=...`
29
+ - Cloudflare Worker `wrangler.toml` (`vars` section, or `wrangler secret put`)
30
+ - Never in a client bundle
31
+
32
+ ## Rotation
33
+
34
+ The dashboard supports rotating both keys without downtime. After rotation, both the old and the new value remain valid for 24h so you have a deploy window. After 24h, the old key starts returning 401.
35
+
36
+ ## Forgetting which key is which
37
+
38
+ Public keys start with a project slug prefix (e.g. `pk_abc123...`). Secrets start with `sk_`. If something is failing with `401 unknown_project` from the browser, it's a public-key issue. If `401 unauthorized` from a server, it's a secret issue.
@@ -0,0 +1,45 @@
1
+ # Cloudflare Worker proxy
2
+
3
+ The recommended production deployment is a **Cloudflare Worker** attached to a route on the integrator's own domain — typically `yoursite.com/fpjs/*` — that proxies BOTH the SDK bundle AND the collector API endpoints.
4
+
5
+ ## Why a Worker beats other proxies
6
+
7
+ - **Same-origin.** Browser sees every fingerprint request hit `yoursite.com/fpjs/...`. Cookies stay first-party. Ad-blockers cannot pattern-match a cross-origin path; rule lists target host-based patterns (`*.fingerprint.com`, `unpkg.com`) far more aggressively than path-based ones.
8
+ - **Off the app server.** The Worker runs at the edge, not in your origin VPC. The integrator's web tier is NEVER in the hot path for fingerprint events — so your app uptime is decoupled from ours, and your app servers don't burn CPU on every event.
9
+ - **No certificate gymnastics.** Cloudflare handles TLS termination at the edge.
10
+
11
+ ## What the Worker proxies
12
+
13
+ | Source path | Upstream |
14
+ | ---------------------------------- | ---------------------------------------------------- |
15
+ | `yoursite.com/fpjs/agent.js` | Fingerprint SDK bundle (obfuscated production build) |
16
+ | `yoursite.com/fpjs/v1/ingest` | Collector — event ingestion |
17
+ | `yoursite.com/fpjs/v1/result/<id>` | Collector — verdict retrieval |
18
+ | `yoursite.com/fpjs/v1/pk` | Collector — server public key rotation |
19
+ | `yoursite.com/fpjs/v1/comparison` | Server-side cross-reference API |
20
+
21
+ The `generate_cloudflare_worker` MCP tool emits the script + `wrangler.toml` ready to deploy with `wrangler deploy`.
22
+
23
+ ## Deploy steps
24
+
25
+ ```bash
26
+ # 1) Install Wrangler
27
+ npm i -g wrangler # or: npx wrangler ...
28
+ wrangler login
29
+
30
+ # 2) From the generated cloudflare/ directory
31
+ cd cloudflare
32
+ wrangler deploy
33
+ ```
34
+
35
+ After deploy:
36
+
37
+ 1. Browser requests to `yoursite.com/fpjs/agent.js` return the SDK bundle.
38
+ 2. Update your env: `FP_COLLECTOR_URL=https://yoursite.com/fpjs`.
39
+ 3. Add `https://yoursite.com` to the dashboard's `allowedHosts` (Security page).
40
+
41
+ ## What NOT to do
42
+
43
+ - **Don't reverse-proxy through your app server.** Putting `/v1/ingest` behind your Nginx / your Node app means every fingerprint event burns CPU on your origin AND ties our uptime to yours. Use the Worker.
44
+ - **Don't link `unpkg.com/fingerprint-platform-sdk` from production.** Default ad-block lists include `unpkg.com`; that script tag disappears for any user running uBlock Origin / Brave Shields.
45
+ - **Don't share Workers across projects.** Each project should have its own Worker so a misconfigured route on tenant A's Worker can't accidentally proxy tenant B's traffic.
@@ -0,0 +1,40 @@
1
+ # DNS CNAME fallback
2
+
3
+ When Cloudflare isn't an option, point a DNS CNAME from `fp.yoursite.com` directly at the collector. This keeps cookies first-party (the SDK loads from a subdomain of the same registered domain) but is **less ad-block-proof** than the Cloudflare Worker route — a dedicated subdomain is easier for filter rules to catch than a same-origin path like `yoursite.com/fpjs/*`.
4
+
5
+ ## Setup
6
+
7
+ In your DNS provider's zone editor for `yoursite.com`:
8
+
9
+ ```
10
+ Type: CNAME
11
+ Name: fp
12
+ Value: <collector-host>
13
+ TTL: 300 (5 min — bump once you've confirmed it works)
14
+ ```
15
+
16
+ Then update your application:
17
+
18
+ ```bash
19
+ FP_COLLECTOR_URL=https://fp.yoursite.com
20
+ ```
21
+
22
+ And add `https://fp.yoursite.com` to the dashboard's `allowedHosts` on the Security page.
23
+
24
+ ## When this is the right choice
25
+
26
+ - You can't (or won't) put anything in front of Cloudflare — e.g. the customer is on a non-Cloudflare CDN like Fastly or Akamai and won't run a parallel Worker.
27
+ - You're in a dev / staging environment where ad-block resistance doesn't matter.
28
+ - You're on a corporate network that already blocks `*.cloudflare.com` (rare).
29
+
30
+ ## Trade-offs vs the Worker
31
+
32
+ | | Worker route (`/fpjs/*`) | DNS CNAME (`fp.yoursite.com`) |
33
+ | ------------------- | ------------------------ | ----------------------------- |
34
+ | Cookies first-party | yes | yes |
35
+ | Ad-block resistance | high (same-origin path) | medium (dedicated subdomain) |
36
+ | Latency vs origin | edge | direct to collector |
37
+ | Setup complexity | wrangler login + deploy | one DNS record |
38
+ | Cost | free Cloudflare tier | DNS only |
39
+
40
+ For production traffic you care about, prefer the Worker. The CNAME is the fallback.
@@ -0,0 +1,41 @@
1
+ # `identify()` events and what to put in them
2
+
3
+ `fp.identify({ event, outcome?, accountHint?, metadata? })` is the SDK's only network call after `init()`. Every call sends one event to the collector and resolves with `{ eventId, requestId, ip, visitorId?, suspectScore? }` once scoring completes.
4
+
5
+ ## Picking the `event` string
6
+
7
+ The `event` field is free-form, but operators score and chart by it — pick stable, low-cardinality values:
8
+
9
+ | Common events | When to fire |
10
+ | ------------------ | ------------------------------------------------------------------------- |
11
+ | `pageview` | On every route load — useful as the "this device is alive" baseline |
12
+ | `login` | After a login form submit, before / instead of the server-side auth check |
13
+ | `signup` | On signup form submit |
14
+ | `password_reset` | When a user starts a reset flow |
15
+ | `checkout` | On payment / cart submit |
16
+ | `route:/some/path` | SPA-friendly variant when you don't otherwise have semantic events |
17
+
18
+ Don't burn cardinality on per-user-id event names — that bloats the events index and makes per-event aggregates useless. Use `accountHint.userId` for the user binding (see below).
19
+
20
+ ## `outcome` for outcomes-based rules
21
+
22
+ For events that have a binary success/fail outcome (login, signup, etc.), pass `outcome: 'success' | 'failed'`. This unlocks the rate-window rules:
23
+
24
+ - `ato_login_brute_force` — fires when too many `login` + `outcome:failed` events hit the same user in 5 min.
25
+ - `ato_credential_stuffing` — fires when too many distinct visitors touch the same user in 24h.
26
+
27
+ ## `accountHint` binding
28
+
29
+ Send `accountHint: { userId, createdAt? }` once you know who the user is — for `login` it's right after the auth check; for `signup` it's right after creation.
30
+
31
+ - `userId` — your application's user id (NOT email — that's PII). Anchors visitor↔user links so the same device showing up on multiple accounts trips multi-acc rules.
32
+ - `createdAt` — epoch milliseconds of when this user signed up. Lets the platform fire `rapid_account_creation` and `multi_acc_velocity` rules (creation age < 60s, < 1h respectively).
33
+
34
+ ## `metadata`
35
+
36
+ A free-form `Record<string, string | number | boolean>` you can attach to the event for your own debugging. Stored alongside the event; not used by the rule engine. Useful for stamping a campaign id, request id from your backend, etc.
37
+
38
+ ## Latency expectations
39
+
40
+ - `identify()` resolves with a real `visitorId` once the worker has scored the event — typically 30-100 ms after the request lands.
41
+ - For latency-sensitive flows (checkout, login submit), fire `identify()` BEFORE the form submit (e.g. on focus of the submit button) and `await` it just before the network call. By the time the user clicks, the verdict is already in flight.
@@ -0,0 +1,34 @@
1
+ # Origin allowlist (`allowedHosts`)
2
+
3
+ The collector enforces an HTTP `Origin` allowlist per project to reject events that didn't come from a page the project owner controls. Without this, anyone who knows your project's public API key could spam your collector from any origin.
4
+
5
+ ## Where it's configured
6
+
7
+ Dashboard → **Security** → **Allowed hosts**. Each entry is an origin: scheme + host + (optional) port.
8
+
9
+ Examples:
10
+
11
+ ```
12
+ https://yoursite.com
13
+ https://www.yoursite.com
14
+ https://staging.yoursite.com
15
+ http://localhost:5173
16
+ ```
17
+
18
+ ## How requests are matched
19
+
20
+ The collector compares the request's `Origin` header (or, when absent, the parsed `Referer`) against the allowlist. Match is case-insensitive on host, exact on scheme and port. A trailing slash is ignored.
21
+
22
+ ## Common mistakes
23
+
24
+ - **Forgetting `https://` for the scheme** — `yoursite.com` won't match. Always include the protocol.
25
+ - **Wildcards aren't supported in v1.** If you have many subdomains, list each one (`https://app.yoursite.com`, `https://api.yoursite.com`, …). Roadmap item to add `*.yoursite.com` syntax.
26
+ - **Forgetting the dev origin.** Add `http://localhost:5173` (Vite) or `http://localhost:3000` (Next.js / CRA) so the SDK works in `pnpm dev`.
27
+
28
+ ## Cloudflare Worker proxy interaction
29
+
30
+ If you proxy through a Cloudflare Worker at `yoursite.com/fpjs/*`, the browser still sends `Origin: https://yoursite.com` — that's the entry you must add to `allowedHosts`. The Worker doesn't change the origin; it only rewrites the path.
31
+
32
+ ## Verifying
33
+
34
+ After adding an origin, fire a test event from the SDK. If you get a 401 with `error: "origin_not_allowed"`, the entry isn't matching — check the scheme + port + host exactly.
@@ -0,0 +1,42 @@
1
+ # Sync vs async (`identify` vs `identifyBeacon`)
2
+
3
+ The SDK ships two ways to dispatch an event:
4
+
5
+ ## `fp.identify({ event })` — default (sync)
6
+
7
+ Sends the event via `fetch()` and returns a promise that resolves with the scoring result. Use this when you need the verdict before continuing (login submit, checkout, signup).
8
+
9
+ ```ts
10
+ const result = await fp.identify({ event: 'login', outcome: 'success' });
11
+ if (result.suspectScore > 50) {
12
+ showCaptchaChallenge();
13
+ }
14
+ ```
15
+
16
+ Latency: ~30-100 ms typically (first request, cold visitor) or single-digit ms once the visitor's signals are cached on the worker.
17
+
18
+ ## `fp.identify({ event, preferBeacon: true })` — async fire-and-forget
19
+
20
+ Uses `navigator.sendBeacon()` to ship the event, which the browser flushes during page-unload. Returns immediately with `{ eventId }` — no scoring result back. Use this for telemetry-style events where you don't need the verdict synchronously (pageview, scroll milestones).
21
+
22
+ ```ts
23
+ fp.identify({ event: 'pageview', preferBeacon: true });
24
+ // returns ~instantly; the event flushes during navigation/unload
25
+ ```
26
+
27
+ `sendBeacon` survives navigation — even `pageview` events fired right before the user clicks a link still reach the collector. With `fetch()` you'd often lose those because the request is cancelled when the page unloads.
28
+
29
+ ## When to use which
30
+
31
+ | Flow | Method | Why |
32
+ | ------------------------- | ----------------- | -------------------------------------------------- |
33
+ | Login submit | sync `identify()` | Need verdict before showing "logged in" |
34
+ | Checkout submit | sync `identify()` | Risk decision must precede the charge |
35
+ | Signup submit | sync `identify()` | Anti-abuse decision before account creation |
36
+ | Pageview | beacon | Telemetry; verdict not needed |
37
+ | Scroll / engagement event | beacon | Telemetry; verdict not needed |
38
+ | Route change (SPA) | beacon | Async is fine — next decision point will call sync |
39
+
40
+ ## Mixing both
41
+
42
+ Common pattern: fire a beacon `pageview` on every route, and a sync `identify({ event: 'login' })` only when the user is about to submit the login form. The collector deduplicates by `(visitorId, ts)` so you don't get double-counted events.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "fingerprint-platform-mcp",
3
+ "version": "0.0.1",
4
+ "description": "Model Context Protocol server that lets Claude / Cursor / Copilot Chat install and configure the Fingerprint Platform SDK in a user's project — analyze the codebase, propose a diff, generate the Cloudflare Worker proxy, verify the install.",
5
+ "type": "module",
6
+ "main": "./dist/cli.js",
7
+ "bin": {
8
+ "fingerprint-platform-mcp": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "keywords": [
19
+ "fingerprint",
20
+ "device-id",
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "claude",
24
+ "cursor",
25
+ "ai-assistant",
26
+ "sdk",
27
+ "install"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.4",
32
+ "diff": "^7.0.0",
33
+ "zod": "^3.23.8",
34
+ "@fingerprint79/sdk-snippets": "0.0.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/diff": "^7.0.0",
38
+ "tsup": "^8.3.5",
39
+ "tsx": "^4.19.1"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "typecheck": "tsc -b --noEmit",
44
+ "lint": "eslint src",
45
+ "test": "vitest run --passWithNoTests",
46
+ "dev": "tsx watch src/cli.ts",
47
+ "start": "node dist/cli.js",
48
+ "clean": "rimraf dist .turbo *.tsbuildinfo"
49
+ }
50
+ }