infinite-tag 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/dist/src/apply.d.ts +14 -0
  4. package/dist/src/apply.js +84 -0
  5. package/dist/src/cli.d.ts +2 -0
  6. package/dist/src/cli.js +416 -0
  7. package/dist/src/frameworks/index.d.ts +4 -0
  8. package/dist/src/frameworks/index.js +16 -0
  9. package/dist/src/frameworks/managed-files.d.ts +10 -0
  10. package/dist/src/frameworks/managed-files.js +104 -0
  11. package/dist/src/frameworks/next-app-router.d.ts +2 -0
  12. package/dist/src/frameworks/next-app-router.js +187 -0
  13. package/dist/src/frameworks/next-pages-router.d.ts +2 -0
  14. package/dist/src/frameworks/next-pages-router.js +169 -0
  15. package/dist/src/frameworks/shared.d.ts +17 -0
  16. package/dist/src/frameworks/shared.js +136 -0
  17. package/dist/src/frameworks/static-html.d.ts +2 -0
  18. package/dist/src/frameworks/static-html.js +137 -0
  19. package/dist/src/frameworks/vite-react.d.ts +2 -0
  20. package/dist/src/frameworks/vite-react.js +274 -0
  21. package/dist/src/index.d.ts +12 -0
  22. package/dist/src/index.js +11 -0
  23. package/dist/src/inspect.d.ts +10 -0
  24. package/dist/src/inspect.js +150 -0
  25. package/dist/src/manifest.d.ts +10 -0
  26. package/dist/src/manifest.js +83 -0
  27. package/dist/src/package-manager.d.ts +6 -0
  28. package/dist/src/package-manager.js +110 -0
  29. package/dist/src/plan.d.ts +9 -0
  30. package/dist/src/plan.js +97 -0
  31. package/dist/src/providers/ga4.d.ts +2 -0
  32. package/dist/src/providers/ga4.js +73 -0
  33. package/dist/src/providers/index.d.ts +3 -0
  34. package/dist/src/providers/index.js +11 -0
  35. package/dist/src/providers/posthog.d.ts +2 -0
  36. package/dist/src/providers/posthog.js +77 -0
  37. package/dist/src/providers/validate.d.ts +31 -0
  38. package/dist/src/providers/validate.js +110 -0
  39. package/dist/src/providers/x.d.ts +2 -0
  40. package/dist/src/providers/x.js +76 -0
  41. package/dist/src/render.d.ts +25 -0
  42. package/dist/src/render.js +260 -0
  43. package/dist/src/types.d.ts +151 -0
  44. package/dist/src/types.js +8 -0
  45. package/dist/src/uninstall.d.ts +7 -0
  46. package/dist/src/uninstall.js +66 -0
  47. package/dist/src/verify.d.ts +5 -0
  48. package/dist/src/verify.js +50 -0
  49. package/dist/src/workspace-artifacts.d.ts +41 -0
  50. package/dist/src/workspace-artifacts.js +171 -0
  51. package/package.json +27 -0
@@ -0,0 +1,83 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { assertConfinedManifestFileEntry, resolveConfinedAppRoot, writeFileAtomic } from "./frameworks/shared.js";
5
+ export const installManifestRelativePath = ".infinite/install.json";
6
+ export function installManifestPath(root) {
7
+ return join(root, installManifestRelativePath);
8
+ }
9
+ export function readInstallManifest(root) {
10
+ const manifestPath = installManifestPath(root);
11
+ if (!existsSync(manifestPath)) {
12
+ return null;
13
+ }
14
+ const raw = readFileSync(manifestPath, "utf8");
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ }
19
+ catch {
20
+ throw new Error("Corrupt .infinite/install.json — cannot parse manifest. Remove it manually to reset.");
21
+ }
22
+ if (!isInstallManifestShape(parsed)) {
23
+ throw new Error("Corrupt .infinite/install.json — manifest is missing expected fields. Remove it manually to reset.");
24
+ }
25
+ assertManifestConfined(root, parsed);
26
+ return parsed;
27
+ }
28
+ // A tampered install.json must never drive reads/writes/removals outside the
29
+ // workspace root. Validating here — the single place the manifest is read from
30
+ // disk — confines every consumer (uninstall, verify, inspect) at once.
31
+ function assertManifestConfined(root, manifest) {
32
+ resolveConfinedAppRoot(root, manifest.appRoot);
33
+ for (const relativePath of manifest.files) {
34
+ assertConfinedManifestFileEntry(root, relativePath);
35
+ }
36
+ }
37
+ function isInstallManifestShape(value) {
38
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
39
+ return false;
40
+ }
41
+ const candidate = value;
42
+ return (typeof candidate.workspaceId === "string" &&
43
+ typeof candidate.appRoot === "string" &&
44
+ typeof candidate.framework === "string" &&
45
+ Array.isArray(candidate.providers) &&
46
+ Array.isArray(candidate.files) &&
47
+ Array.isArray(candidate.envKeys) &&
48
+ typeof candidate.contentHashes === "object" &&
49
+ candidate.contentHashes !== null);
50
+ }
51
+ export function writeInstallManifest(root, manifest) {
52
+ const manifestPath = installManifestPath(root);
53
+ writeFileAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
54
+ return manifestPath;
55
+ }
56
+ export function writeInstallManifestIfChanged(root, manifest) {
57
+ const manifestPath = installManifestPath(root);
58
+ const nextContents = `${JSON.stringify(manifest, null, 2)}\n`;
59
+ const currentContents = existsSync(manifestPath) ? readFileSync(manifestPath, "utf8") : null;
60
+ if (currentContents === nextContents) {
61
+ return {
62
+ changed: false,
63
+ manifestPath
64
+ };
65
+ }
66
+ writeFileAtomic(manifestPath, nextContents);
67
+ return {
68
+ changed: true,
69
+ manifestPath
70
+ };
71
+ }
72
+ export function computeContentHashes(root, files) {
73
+ const contentHashes = {};
74
+ for (const relativePath of files) {
75
+ const absolutePath = join(root, relativePath);
76
+ if (!existsSync(absolutePath)) {
77
+ continue;
78
+ }
79
+ const hash = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
80
+ contentHashes[relativePath] = hash;
81
+ }
82
+ return contentHashes;
83
+ }
@@ -0,0 +1,6 @@
1
+ import type { PackageManager, PackageManagerCommands, PackageManagerDetection } from "./types.js";
2
+ export declare function detectPackageManager(root: string, override?: PackageManager): PackageManagerDetection;
3
+ export declare function buildPackageManagerCommands(manager: PackageManager, options: {
4
+ pinnedVersion: string;
5
+ workspaceId: string;
6
+ }): PackageManagerCommands;
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const lockfileOrder = [
5
+ { manager: "pnpm", files: ["pnpm-lock.yaml"] },
6
+ { manager: "npm", files: ["package-lock.json"] },
7
+ { manager: "yarn", files: ["yarn.lock"] },
8
+ { manager: "bun", files: ["bun.lock", "bun.lockb"] }
9
+ ];
10
+ function resolvePackageRoot() {
11
+ for (const relativePath of ["..", "../.."]) {
12
+ const candidate = fileURLToPath(new URL(relativePath, import.meta.url));
13
+ if (existsSync(join(candidate, "package.json"))) {
14
+ return candidate;
15
+ }
16
+ }
17
+ throw new Error("Unable to resolve the infinite-tag package root.");
18
+ }
19
+ const packageRoot = resolvePackageRoot();
20
+ const instrumentPackage = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
21
+ const instrumentCliEntry = join(packageRoot, "dist/src/cli.js");
22
+ const repoRoot = join(packageRoot, "../..");
23
+ const instrumentBinaryName = Object.keys(instrumentPackage.bin ?? {})[0] ?? "infinite-tag";
24
+ function shellQuote(value) {
25
+ return `'${value.replaceAll("'", `'\\''`)}'`;
26
+ }
27
+ function buildPublishedCommands(manager, options) {
28
+ const pinnedPackage = `${instrumentPackage.name}@${options.pinnedVersion}`;
29
+ const workspaceFlag = `--workspace ${options.workspaceId}`;
30
+ switch (manager) {
31
+ case "npm":
32
+ return {
33
+ packageManager: manager,
34
+ oneOff: `npm exec ${pinnedPackage} -- install ${workspaceFlag}`,
35
+ repeatableInstall: `npm install -D ${pinnedPackage}`,
36
+ repeatableRun: `npm exec ${instrumentBinaryName} -- install ${workspaceFlag}`
37
+ };
38
+ case "pnpm":
39
+ return {
40
+ packageManager: manager,
41
+ oneOff: `pnpm dlx ${pinnedPackage} install ${workspaceFlag}`,
42
+ repeatableInstall: `pnpm add -D ${pinnedPackage}`,
43
+ repeatableRun: `pnpm exec ${instrumentBinaryName} install ${workspaceFlag}`
44
+ };
45
+ case "yarn":
46
+ return {
47
+ packageManager: manager,
48
+ oneOff: `yarn dlx ${pinnedPackage} install ${workspaceFlag}`,
49
+ repeatableInstall: `yarn add -D ${pinnedPackage}`,
50
+ repeatableRun: `yarn ${instrumentBinaryName} install ${workspaceFlag}`
51
+ };
52
+ case "bun":
53
+ return {
54
+ packageManager: manager,
55
+ oneOff: `bunx ${pinnedPackage} install ${workspaceFlag}`,
56
+ repeatableInstall: `bun add -d ${pinnedPackage}`,
57
+ repeatableRun: `bunx ${instrumentBinaryName} install ${workspaceFlag}`
58
+ };
59
+ }
60
+ }
61
+ function buildLocalWorkspaceCommand(options) {
62
+ return [
63
+ `pnpm --dir ${shellQuote(repoRoot)} --filter ${instrumentPackage.name} build`,
64
+ `node ${shellQuote(instrumentCliEntry)} install --root ${shellQuote(repoRoot)} --workspace ${options.workspaceId}`
65
+ ].join(" && ");
66
+ }
67
+ export function detectPackageManager(root, override) {
68
+ if (override) {
69
+ return {
70
+ kind: override,
71
+ reason: "override",
72
+ lockfiles: []
73
+ };
74
+ }
75
+ const matches = lockfileOrder.flatMap((entry) => entry.files
76
+ .filter((file) => existsSync(join(root, file)))
77
+ .map((file) => ({ manager: entry.manager, file })));
78
+ if (matches.length === 0) {
79
+ return {
80
+ kind: "unknown",
81
+ reason: "no-lockfile",
82
+ lockfiles: []
83
+ };
84
+ }
85
+ const uniqueManagers = [...new Set(matches.map((match) => match.manager))];
86
+ if (uniqueManagers.length > 1) {
87
+ return {
88
+ kind: "ambiguous",
89
+ reason: "multiple-lockfiles",
90
+ lockfiles: matches.map((match) => match.file)
91
+ };
92
+ }
93
+ return {
94
+ kind: uniqueManagers[0],
95
+ reason: "lockfile",
96
+ lockfiles: matches.map((match) => match.file)
97
+ };
98
+ }
99
+ export function buildPackageManagerCommands(manager, options) {
100
+ const publishedCommands = buildPublishedCommands(manager, options);
101
+ if (instrumentPackage.private !== true) {
102
+ return publishedCommands;
103
+ }
104
+ return {
105
+ packageManager: manager,
106
+ oneOff: buildLocalWorkspaceCommand({ workspaceId: options.workspaceId }),
107
+ repeatableInstall: `After publishing ${instrumentPackage.name}, install it with: ${publishedCommands.repeatableInstall}`,
108
+ repeatableRun: `After publishing ${instrumentPackage.name}, re-run it with: ${publishedCommands.repeatableRun}`
109
+ };
110
+ }
@@ -0,0 +1,9 @@
1
+ import type { InspectResult, InstallPlan, PackageManager, WorkspaceInstallArtifacts } from "./types.js";
2
+ export interface PlanInstallationOptions {
3
+ root: string;
4
+ inspect?: InspectResult;
5
+ workspaceId?: string;
6
+ packageManager?: PackageManager;
7
+ artifacts: WorkspaceInstallArtifacts;
8
+ }
9
+ export declare function planInstallation(options: PlanInstallationOptions): InstallPlan;
@@ -0,0 +1,97 @@
1
+ import { join } from "node:path";
2
+ import { getFrameworkAdapter, isSupportedFramework } from "./frameworks/index.js";
3
+ import { normalizeAppRelativePath } from "./frameworks/shared.js";
4
+ import { getProviderAdapter } from "./providers/index.js";
5
+ import { detectUnmanagedProviders, inspectWorkspace } from "./inspect.js";
6
+ const providerOrder = ["ga4", "posthog", "x"];
7
+ function selectedProviders(artifacts) {
8
+ return providerOrder.filter((providerId) => artifacts[providerId] !== undefined);
9
+ }
10
+ export function planInstallation(options) {
11
+ const inspectResult = options.inspect ??
12
+ inspectWorkspace(options.root, {
13
+ packageManager: options.packageManager
14
+ });
15
+ const providers = selectedProviders(options.artifacts);
16
+ const assumptions = [...inspectResult.assumptions];
17
+ const blockers = [...inspectResult.blockers];
18
+ if (!isSupportedFramework(inspectResult.framework)) {
19
+ if (!blockers.includes("Unsupported repository shape for instrumentation.")) {
20
+ blockers.push("Unsupported repository shape for instrumentation.");
21
+ }
22
+ return {
23
+ framework: inspectResult.framework,
24
+ providers,
25
+ files: [],
26
+ envKeys: [],
27
+ applyMode: "plan-only",
28
+ instructions: [],
29
+ assumptions,
30
+ blockers,
31
+ confidence: Math.min(inspectResult.confidence, 0.45),
32
+ appRoot: inspectResult.appRoot,
33
+ packageManager: inspectResult.packageManager,
34
+ repoStatus: inspectResult.repoStatus,
35
+ workspaceId: options.workspaceId,
36
+ artifacts: options.artifacts
37
+ };
38
+ }
39
+ if (providers.length === 0) {
40
+ blockers.push("No supported public install artifacts were provided.");
41
+ }
42
+ const appRootAbsolute = inspectResult.appRoot === "." ? options.root : join(options.root, inspectResult.appRoot);
43
+ const frameworkAdapter = getFrameworkAdapter(inspectResult.framework);
44
+ const frameworkDraft = frameworkAdapter?.plan(appRootAbsolute);
45
+ if (frameworkDraft) {
46
+ assumptions.push(...frameworkDraft.assumptions);
47
+ blockers.push(...frameworkDraft.blockers);
48
+ }
49
+ const unmanagedProviders = detectUnmanagedProviders(appRootAbsolute);
50
+ const envKeys = [];
51
+ const instructions = [];
52
+ for (const providerId of providers) {
53
+ const adapter = getProviderAdapter(providerId);
54
+ if (unmanagedProviders.includes(providerId)) {
55
+ blockers.push(`Existing ${adapter.displayName} analytics wiring was detected in this repo and is not managed by Infinite. Remove or migrate the existing ${adapter.displayName} tag before installing it with infinite-tag.`);
56
+ }
57
+ const providerPlan = adapter.plan(inspectResult.framework, options.artifacts[providerId]);
58
+ assumptions.push(...providerPlan.assumptions);
59
+ blockers.push(...providerPlan.blockers);
60
+ instructions.push(...providerPlan.instructions);
61
+ envKeys.push(...adapter.envKeys(inspectResult.framework));
62
+ }
63
+ const uniqueEnvKeys = [...new Set(envKeys)];
64
+ const files = (frameworkDraft?.files ?? []).map((file) => normalizeAppRelativePath(inspectResult.appRoot, file));
65
+ const frameworkInstructions = (frameworkDraft?.instructions ?? []).map((instruction) => ({
66
+ ...instruction,
67
+ path: normalizeAppRelativePath(inspectResult.appRoot, instruction.path)
68
+ }));
69
+ const providerInstructions = instructions.map((instruction) => ({
70
+ ...instruction,
71
+ path: normalizeAppRelativePath(inspectResult.appRoot, instruction.path)
72
+ }));
73
+ const applyMode = frameworkDraft?.applyMode ?? "plan-only";
74
+ let confidence = frameworkDraft?.confidence ?? inspectResult.confidence;
75
+ if (providers.length > 0) {
76
+ confidence = Math.min(0.99, confidence + Math.min(providers.length, 3) * 0.03);
77
+ }
78
+ if (blockers.length > 0) {
79
+ confidence = Math.min(confidence, 0.45);
80
+ }
81
+ return {
82
+ framework: inspectResult.framework,
83
+ providers,
84
+ files,
85
+ envKeys: uniqueEnvKeys,
86
+ applyMode,
87
+ instructions: [...frameworkInstructions, ...providerInstructions],
88
+ assumptions: [...new Set(assumptions)],
89
+ blockers: [...new Set(blockers)],
90
+ confidence,
91
+ appRoot: inspectResult.appRoot,
92
+ packageManager: inspectResult.packageManager,
93
+ repoStatus: inspectResult.repoStatus,
94
+ workspaceId: options.workspaceId,
95
+ artifacts: options.artifacts
96
+ };
97
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProviderAdapter } from "../types.js";
2
+ export declare const ga4ProviderAdapter: ProviderAdapter;
@@ -0,0 +1,73 @@
1
+ import { jsLiteral, urlQueryValue, validateGa4MeasurementId } from "./validate.js";
2
+ function frameworkEnvKeys(framework) {
3
+ switch (framework) {
4
+ case "next-app-router":
5
+ case "next-pages-router":
6
+ return ["NEXT_PUBLIC_GA4_MEASUREMENT_ID"];
7
+ case "vite-react":
8
+ return ["VITE_GA4_MEASUREMENT_ID"];
9
+ case "static-html":
10
+ return [];
11
+ }
12
+ }
13
+ export const ga4ProviderAdapter = {
14
+ id: "ga4",
15
+ displayName: "GA4",
16
+ envKeys(framework) {
17
+ return frameworkEnvKeys(framework);
18
+ },
19
+ plan(framework, artifact) {
20
+ const measurementId = artifact && typeof artifact === "object" && "measurementId" in artifact
21
+ ? artifact.measurementId
22
+ : undefined;
23
+ const invalid = validateGa4MeasurementId(measurementId);
24
+ if (invalid) {
25
+ return { assumptions: [], blockers: [invalid], instructions: [] };
26
+ }
27
+ const instructions = [
28
+ {
29
+ path: frameworkInstructionPath(framework),
30
+ action: framework === "static-html" ? "modify" : "create",
31
+ description: framework === "static-html"
32
+ ? "Inject the GA4 public loader and gtag bootstrap into index.html."
33
+ : "Add the GA4 public loader and gtag bootstrap to the managed analytics module.",
34
+ provider: "ga4",
35
+ snippet: framework === "static-html"
36
+ ? buildHtmlSnippet(measurementId)
37
+ : buildBootstrapSnippet(measurementId)
38
+ }
39
+ ];
40
+ return {
41
+ assumptions: ["GA4 wiring will use only the public measurementId artifact."],
42
+ blockers: [],
43
+ instructions
44
+ };
45
+ }
46
+ };
47
+ function frameworkInstructionPath(framework) {
48
+ switch (framework) {
49
+ case "static-html":
50
+ return "index.html";
51
+ case "vite-react":
52
+ return "src/lib/infinite-analytics.ts";
53
+ case "next-app-router":
54
+ case "next-pages-router":
55
+ return "lib/infinite-analytics.ts";
56
+ }
57
+ }
58
+ function buildBootstrapSnippet(measurementId) {
59
+ return [
60
+ "window.dataLayer = window.dataLayer || [];",
61
+ "function gtag(){window.dataLayer.push(arguments);}",
62
+ "gtag('js', new Date());",
63
+ `gtag('config', ${jsLiteral(measurementId)});`
64
+ ].join("\n");
65
+ }
66
+ function buildHtmlSnippet(measurementId) {
67
+ return [
68
+ `<script async src="https://www.googletagmanager.com/gtag/js?id=${urlQueryValue(measurementId)}"></script>`,
69
+ "<script>",
70
+ buildBootstrapSnippet(measurementId),
71
+ "</script>"
72
+ ].join("\n");
73
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProviderAdapter, ProviderId } from "../types.js";
2
+ export declare const providerAdapters: Record<ProviderId, ProviderAdapter>;
3
+ export declare function getProviderAdapter(providerId: ProviderId): ProviderAdapter;
@@ -0,0 +1,11 @@
1
+ import { ga4ProviderAdapter } from "./ga4.js";
2
+ import { posthogProviderAdapter } from "./posthog.js";
3
+ import { xProviderAdapter } from "./x.js";
4
+ export const providerAdapters = {
5
+ ga4: ga4ProviderAdapter,
6
+ posthog: posthogProviderAdapter,
7
+ x: xProviderAdapter
8
+ };
9
+ export function getProviderAdapter(providerId) {
10
+ return providerAdapters[providerId];
11
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProviderAdapter } from "../types.js";
2
+ export declare const posthogProviderAdapter: ProviderAdapter;
@@ -0,0 +1,77 @@
1
+ import { jsLiteral, normalizePosthogApiHost, validatePosthogProjectKey } from "./validate.js";
2
+ function frameworkEnvKeys(framework) {
3
+ switch (framework) {
4
+ case "next-app-router":
5
+ case "next-pages-router":
6
+ return ["NEXT_PUBLIC_POSTHOG_API_HOST", "NEXT_PUBLIC_POSTHOG_KEY"];
7
+ case "vite-react":
8
+ return ["VITE_POSTHOG_API_HOST", "VITE_POSTHOG_KEY"];
9
+ case "static-html":
10
+ return [];
11
+ }
12
+ }
13
+ export const posthogProviderAdapter = {
14
+ id: "posthog",
15
+ displayName: "PostHog",
16
+ envKeys(framework) {
17
+ return frameworkEnvKeys(framework);
18
+ },
19
+ plan(framework, artifact) {
20
+ const projectKey = artifact && typeof artifact === "object" && "projectKey" in artifact
21
+ ? artifact.projectKey
22
+ : undefined;
23
+ const apiHost = artifact && typeof artifact === "object" && "apiHost" in artifact ? artifact.apiHost : undefined;
24
+ const blockers = [];
25
+ const keyError = validatePosthogProjectKey(projectKey);
26
+ if (keyError) {
27
+ blockers.push(keyError);
28
+ }
29
+ const host = normalizePosthogApiHost(apiHost);
30
+ const apiHostOrigin = "origin" in host ? host.origin : undefined;
31
+ if ("error" in host) {
32
+ blockers.push(host.error);
33
+ }
34
+ const ready = blockers.length === 0 && typeof projectKey === "string" && apiHostOrigin !== undefined;
35
+ return {
36
+ assumptions: ready
37
+ ? ["PostHog wiring will use only the public projectKey and apiHost artifacts."]
38
+ : [],
39
+ blockers,
40
+ instructions: ready
41
+ ? [
42
+ {
43
+ path: frameworkInstructionPath(framework),
44
+ action: framework === "static-html" ? "modify" : "create",
45
+ description: framework === "static-html"
46
+ ? "Inject the PostHog public bootstrap snippet into index.html."
47
+ : "Add the PostHog public bootstrap snippet to the managed analytics module.",
48
+ provider: "posthog",
49
+ snippet: framework === "static-html"
50
+ ? wrapHtmlSnippet(buildBootstrapSnippet(projectKey, apiHostOrigin))
51
+ : buildBootstrapSnippet(projectKey, apiHostOrigin)
52
+ }
53
+ ]
54
+ : []
55
+ };
56
+ }
57
+ };
58
+ function frameworkInstructionPath(framework) {
59
+ switch (framework) {
60
+ case "static-html":
61
+ return "index.html";
62
+ case "vite-react":
63
+ return "src/lib/infinite-analytics.ts";
64
+ case "next-app-router":
65
+ case "next-pages-router":
66
+ return "lib/infinite-analytics.ts";
67
+ }
68
+ }
69
+ function buildBootstrapSnippet(projectKey, apiHost) {
70
+ return [
71
+ "!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split('.');2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement('script')).type='text/javascript',p.crossOrigin='anonymous',p.async=!0,p.src=s.api_host.replace('.i.posthog.com','-assets.i.posthog.com')+'/static/array.js',(r=t.getElementsByTagName('script')[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a='posthog',u.people=u.people||[],u.toString=function(t){var e='posthog';return'posthog'!==a&&(e+='.'+a),t||(e+=' (stub)'),e},u.people.toString=function(){return u.toString(1)+'.people'},o='init capture register register_once unregister reset opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing set_config people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove_group people.set person.set_once person.unset person.increment person.append person.union person.track_charge person.clear_charges person.delete_user group.set group.set_once group.unset group.remove group.union group.track group.identify group.set_property feature_flags.isFeatureEnabled feature_flags.getFeatureFlag feature_flags.getFeatureFlagPayload feature_flags.reloadFeatureFlags feature_flags.updateEarlyAccessFeatureEnrollment feature_flags.getEarlyAccessFeatures feature_flags.onFeatureFlags sessionRecording.startSessionRecording sessionRecording.stopSessionRecording sessionRecording.getSessionRecordingUrl'.split(' '),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);",
72
+ `posthog.init(${jsLiteral(projectKey)}, { api_host: ${jsLiteral(apiHost)}, persistence: 'localStorage+cookie' });`
73
+ ].join("\n");
74
+ }
75
+ function wrapHtmlSnippet(source) {
76
+ return ["<script>", source, "</script>"].join("\n");
77
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Strict validation + script-safe embedding for the PUBLIC analytics artifacts that get
3
+ * written into a founder's source code.
4
+ *
5
+ * Once infinite-tag ships as a public CLI, these values arrive as user-supplied
6
+ * flags (`--ga4-measurement-id`, `--posthog-project-key`, `--posthog-api-host`, …) or via
7
+ * `--artifact-file`. A malformed or hostile value must be REJECTED as a plan blocker, never
8
+ * interpolated raw into the founder's JS/HTML. Validation is the primary defense; the
9
+ * embedding helpers are belt-and-suspenders so even a validation gap cannot break out of a
10
+ * string literal or `</script>` block.
11
+ */
12
+ export declare function validateGa4MeasurementId(value: unknown): string | null;
13
+ export declare function validatePosthogProjectKey(value: unknown): string | null;
14
+ /** Validates the PostHog apiHost and returns its clean host (preserving path, no credentials/query/hash). */
15
+ export declare function normalizePosthogApiHost(value: unknown): {
16
+ origin: string;
17
+ } | {
18
+ error: string;
19
+ };
20
+ export declare function validateXPixelId(value: unknown): string | null;
21
+ export declare function validateXEventTagIds(value: unknown): string | null;
22
+ /**
23
+ * Serialize a value as a JS literal that is safe to inline inside a `<script>` block.
24
+ * JSON.stringify handles quotes/backslashes/newlines; we additionally escape `<` (stops a
25
+ * value from closing the script element via `</script>`) and the raw line terminators
26
+ * U+2028 / U+2029 (legal in HTML/JSON but illegal mid JS string literal). Code points are
27
+ * compared numerically so the source stays ASCII-only.
28
+ */
29
+ export declare function jsLiteral(value: unknown): string;
30
+ /** Safe value for a URL query parameter such as the GA4 loader's `?id=...`. */
31
+ export declare function urlQueryValue(value: string): string;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Strict validation + script-safe embedding for the PUBLIC analytics artifacts that get
3
+ * written into a founder's source code.
4
+ *
5
+ * Once infinite-tag ships as a public CLI, these values arrive as user-supplied
6
+ * flags (`--ga4-measurement-id`, `--posthog-project-key`, `--posthog-api-host`, …) or via
7
+ * `--artifact-file`. A malformed or hostile value must be REJECTED as a plan blocker, never
8
+ * interpolated raw into the founder's JS/HTML. Validation is the primary defense; the
9
+ * embedding helpers are belt-and-suspenders so even a validation gap cannot break out of a
10
+ * string literal or `</script>` block.
11
+ */
12
+ // GA4/gtag measurement-style IDs: G-XXXX (GA4), and the other prefixes the gtag loader
13
+ // accepts (GT- google tag, AW- ads, UA- legacy, DC- floodlight). Alphanumerics + hyphens
14
+ // only — no quotes, angle brackets, slashes or whitespace can pass. At least one
15
+ // alphanumeric is required in the body so all-hyphen bodies like `G-----` are rejected.
16
+ const MEASUREMENT_ID = /^(?:G|GT|AW|UA|DC)-(?=[A-Za-z0-9-]*[A-Za-z0-9])[A-Za-z0-9-]{4,}$/;
17
+ // PostHog public project keys look like `phc_<base62>`. The `phc_` prefix + an
18
+ // alphanumeric-only charset is the security guarantee; we don't gate on length.
19
+ const POSTHOG_PROJECT_KEY = /^phc_[A-Za-z0-9]+$/;
20
+ // X/Twitter pixel ids + event-tag ids: alphanumerics, hyphens and underscores
21
+ // (real event tags look like `tw-<pixel>-<event>`). The charset is what keeps a value
22
+ // from breaking out of a string literal or `</script>`.
23
+ const X_ID = /^[A-Za-z0-9_-]{2,}$/;
24
+ export function validateGa4MeasurementId(value) {
25
+ if (typeof value !== "string" || value.trim().length === 0) {
26
+ return "GA4 requires a public measurementId before planning can continue.";
27
+ }
28
+ if (!MEASUREMENT_ID.test(value)) {
29
+ return `GA4 measurementId ${JSON.stringify(value)} is not a valid Measurement ID (expected e.g. G-XXXXXXXXXX).`;
30
+ }
31
+ return null;
32
+ }
33
+ export function validatePosthogProjectKey(value) {
34
+ if (typeof value !== "string" || value.trim().length === 0) {
35
+ return "PostHog requires a public projectKey before planning can continue.";
36
+ }
37
+ if (!POSTHOG_PROJECT_KEY.test(value)) {
38
+ return `PostHog projectKey ${JSON.stringify(value)} is not a valid public project key (expected phc_...).`;
39
+ }
40
+ return null;
41
+ }
42
+ /** Validates the PostHog apiHost and returns its clean host (preserving path, no credentials/query/hash). */
43
+ export function normalizePosthogApiHost(value) {
44
+ if (typeof value !== "string" || value.trim().length === 0) {
45
+ return { error: "PostHog requires a public apiHost before planning can continue." };
46
+ }
47
+ let url;
48
+ try {
49
+ url = new URL(value);
50
+ }
51
+ catch {
52
+ return { error: `PostHog apiHost ${JSON.stringify(value)} is not a valid URL.` };
53
+ }
54
+ if (url.protocol !== "https:") {
55
+ return { error: `PostHog apiHost must be an https URL (got ${JSON.stringify(value)}).` };
56
+ }
57
+ if (url.username || url.password) {
58
+ return { error: "PostHog apiHost must not contain embedded credentials." };
59
+ }
60
+ // Preserve the pathname so reverse-proxy configs like https://app.example.com/ingest work
61
+ // correctly. Strip any trailing slash, query string, and fragment — those must never ride
62
+ // along into the snippet.
63
+ const pathname = url.pathname.replace(/\/$/, "");
64
+ return { origin: url.origin + pathname };
65
+ }
66
+ export function validateXPixelId(value) {
67
+ if (typeof value !== "string" || value.trim().length === 0) {
68
+ return "X requires a public pixelId before planning can continue.";
69
+ }
70
+ if (!X_ID.test(value)) {
71
+ return `X pixelId ${JSON.stringify(value)} is not a valid pixel id.`;
72
+ }
73
+ return null;
74
+ }
75
+ export function validateXEventTagIds(value) {
76
+ if (!Array.isArray(value) || value.length === 0) {
77
+ return "X requires at least one public eventTagId before planning can continue.";
78
+ }
79
+ for (const id of value) {
80
+ if (typeof id !== "string" || !X_ID.test(id)) {
81
+ return `X eventTagId ${JSON.stringify(id)} is not a valid event tag id.`;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ /**
87
+ * Serialize a value as a JS literal that is safe to inline inside a `<script>` block.
88
+ * JSON.stringify handles quotes/backslashes/newlines; we additionally escape `<` (stops a
89
+ * value from closing the script element via `</script>`) and the raw line terminators
90
+ * U+2028 / U+2029 (legal in HTML/JSON but illegal mid JS string literal). Code points are
91
+ * compared numerically so the source stays ASCII-only.
92
+ */
93
+ export function jsLiteral(value) {
94
+ const json = JSON.stringify(value) ?? "undefined";
95
+ let out = "";
96
+ for (const ch of json) {
97
+ const code = ch.charCodeAt(0);
98
+ if (ch === "<" || code === 0x2028 || code === 0x2029) {
99
+ out += "\\u" + code.toString(16).padStart(4, "0");
100
+ }
101
+ else {
102
+ out += ch;
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ /** Safe value for a URL query parameter such as the GA4 loader's `?id=...`. */
108
+ export function urlQueryValue(value) {
109
+ return encodeURIComponent(value);
110
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProviderAdapter } from "../types.js";
2
+ export declare const xProviderAdapter: ProviderAdapter;