@mosierdata/emdash-plugin-analytics 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -447,147 +447,11 @@ function createPlugin() {
447
447
  allowedHosts: ["api.roiknowledge.com"],
448
448
  admin: {
449
449
  entry: "@mosierdata/emdash-plugin-analytics/admin",
450
- settingsSchema: {
451
- licenseKey: {
452
- type: "secret",
453
- label: "License Key",
454
- description: "Get this from your MosierData portal. Prefix: qdsh_"
455
- },
456
- gtmEnabled: {
457
- type: "boolean",
458
- label: "Enable Google Tag Manager",
459
- default: false
460
- },
461
- gtmId: {
462
- type: "string",
463
- label: "Google Tag Manager ID",
464
- description: "e.g. GTM-XXXXXXX",
465
- default: ""
466
- },
467
- ga4Enabled: {
468
- type: "boolean",
469
- label: "Enable Google Analytics 4",
470
- default: false
471
- },
472
- ga4Id: {
473
- type: "string",
474
- label: "Google Analytics 4 Measurement ID",
475
- description: "e.g. G-XXXXXXXXXX",
476
- default: ""
477
- },
478
- metaPixelEnabled: {
479
- type: "boolean",
480
- label: "Enable Meta (Facebook) Pixel",
481
- default: false
482
- },
483
- metaPixelId: {
484
- type: "string",
485
- label: "Meta (Facebook) Pixel ID",
486
- description: "Numeric ID from Meta Events Manager",
487
- default: ""
488
- },
489
- linkedInEnabled: {
490
- type: "boolean",
491
- label: "Enable LinkedIn Insights Tag",
492
- default: false
493
- },
494
- linkedInPartnerId: {
495
- type: "string",
496
- label: "LinkedIn Insights Tag Partner ID",
497
- description: "Numeric Partner ID from LinkedIn Campaign Manager",
498
- default: ""
499
- },
500
- tiktokEnabled: {
501
- type: "boolean",
502
- label: "Enable TikTok Pixel",
503
- default: false
504
- },
505
- tiktokPixelId: {
506
- type: "string",
507
- label: "TikTok Pixel ID",
508
- description: "Alphanumeric ID from TikTok Events Manager",
509
- default: ""
510
- },
511
- bingEnabled: {
512
- type: "boolean",
513
- label: "Enable Microsoft (Bing) UET Tag",
514
- default: false
515
- },
516
- bingTagId: {
517
- type: "string",
518
- label: "Microsoft UET Tag ID",
519
- description: "Numeric Tag ID from Microsoft Advertising",
520
- default: ""
521
- },
522
- pinterestEnabled: {
523
- type: "boolean",
524
- label: "Enable Pinterest Tag",
525
- default: false
526
- },
527
- pinterestTagId: {
528
- type: "string",
529
- label: "Pinterest Tag ID",
530
- description: "Numeric Tag ID from Pinterest Ads Manager",
531
- default: ""
532
- },
533
- nextdoorEnabled: {
534
- type: "boolean",
535
- label: "Enable Nextdoor Pixel",
536
- default: false
537
- },
538
- nextdoorPixelId: {
539
- type: "string",
540
- label: "Nextdoor Data Source ID",
541
- description: "UUID from Nextdoor Business Ads dashboard",
542
- default: ""
543
- },
544
- dniSwapNumber: {
545
- type: "string",
546
- label: "Website Phone Number to Swap",
547
- description: "Phone number on your site that AvidTrak will dynamically replace.",
548
- default: ""
549
- },
550
- dniScriptUrl: {
551
- type: "string",
552
- label: "AvidTrak Script URL",
553
- description: "Provided by AvidTrak after provisioning a tracking number.",
554
- default: ""
555
- },
556
- customHeadCode: {
557
- type: "string",
558
- label: "Custom <head> Code",
559
- multiline: true,
560
- default: ""
561
- },
562
- customFooterCode: {
563
- type: "string",
564
- label: "Custom Footer Code",
565
- multiline: true,
566
- default: ""
567
- },
568
- debug: {
569
- type: "boolean",
570
- label: "Debug Mode",
571
- default: false
572
- }
573
- },
574
- pages: [
575
- {
576
- path: "/dashboard",
577
- label: "Marketing ROI",
578
- icon: "chart"
579
- },
580
- {
581
- path: "/tracking",
582
- label: "Tracking Pixels",
583
- icon: "tracking"
584
- },
585
- {
586
- path: "/settings",
587
- label: "License & Google",
588
- icon: "settings"
589
- }
590
- ]
450
+ pages: [{
451
+ path: "/dashboard",
452
+ label: "Marketing ROI",
453
+ icon: "chart"
454
+ }]
591
455
  },
592
456
  hooks: {
593
457
  "plugin:install": async (_event, ctx) => {
@@ -673,6 +537,34 @@ function createPlugin() {
673
537
  "google-oauth/connected": { handler: async (ctx) => {
674
538
  await ctx.kv.set("state:googleConnected", true);
675
539
  return { connected: true };
540
+ } },
541
+ "settings/load": { handler: async (ctx) => {
542
+ const [dniSwapNumber, dniScriptUrl, customHeadCode, customFooterCode, debug] = await Promise.all([
543
+ ctx.kv.get("settings:dniSwapNumber"),
544
+ ctx.kv.get("settings:dniScriptUrl"),
545
+ ctx.kv.get("settings:customHeadCode"),
546
+ ctx.kv.get("settings:customFooterCode"),
547
+ ctx.kv.get("settings:debug")
548
+ ]);
549
+ return {
550
+ dniSwapNumber: dniSwapNumber ?? "",
551
+ dniScriptUrl: dniScriptUrl ?? "",
552
+ customHeadCode: customHeadCode ?? "",
553
+ customFooterCode: customFooterCode ?? "",
554
+ debug: debug ?? false
555
+ };
556
+ } },
557
+ "settings/save": { handler: async (ctx) => {
558
+ const body = await ctx.request.json();
559
+ await Promise.all([
560
+ typeof body.licenseKey === "string" && ctx.kv.set("settings:licenseKey", body.licenseKey.trim()),
561
+ typeof body.dniSwapNumber === "string" && ctx.kv.set("settings:dniSwapNumber", body.dniSwapNumber),
562
+ typeof body.dniScriptUrl === "string" && ctx.kv.set("settings:dniScriptUrl", body.dniScriptUrl),
563
+ typeof body.customHeadCode === "string" && ctx.kv.set("settings:customHeadCode", body.customHeadCode),
564
+ typeof body.customFooterCode === "string" && ctx.kv.set("settings:customFooterCode", body.customFooterCode),
565
+ typeof body.debug === "boolean" && ctx.kv.set("settings:debug", body.debug)
566
+ ].filter(Boolean));
567
+ return { ok: true };
676
568
  } }
677
569
  }
678
570
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/crypto.ts","../src/lib/licensing.ts","../src/frontend/injector.ts","../src/lib/trackingSettingsDocument.ts","../src/index.ts"],"sourcesContent":["/**\n * Verifies an Ed25519 signature using the Web Crypto API.\n *\n * @param payload - Base64-encoded payload string (as returned by the API)\n * @param signature - Base64-encoded Ed25519 signature\n * @param publicKey - Base64-encoded 32-byte Ed25519 public key (hardcoded in plugin)\n */\nexport async function verifyEd25519Signature(\n payload: string,\n signature: string,\n publicKey: string\n): Promise<boolean> {\n try {\n const keyBytes = base64ToBytes(publicKey);\n const sigBytes = base64ToBytes(signature);\n const msgBytes = new TextEncoder().encode(payload);\n\n const cryptoKey = await crypto.subtle.importKey(\n 'raw',\n keyBytes,\n { name: 'Ed25519' },\n false,\n ['verify']\n );\n\n return await crypto.subtle.verify('Ed25519', cryptoKey, sigBytes, msgBytes);\n } catch {\n return false;\n }\n}\n\nfunction base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n","import type { PluginContext } from 'emdash';\nimport type { LicenseData, LicenseTokenPayload, ValidateApiResponse } from '../types';\nimport { verifyEd25519Signature } from './crypto';\n\nexport const CACHE_KEY = 'state:licenseCache';\n\nconst VALIDATE_URL = 'https://api.roiknowledge.com/api/roi/plugin/validate';\n\n// Ed25519 public key from the MosierData signing keypair.\nconst PUBLIC_KEY = 'COwQzXhDeQC9uxAdyNFdbFbIrwLAGgtRZlhfAxbR0Dk=';\n\nexport async function validateLicense(ctx: PluginContext): Promise<LicenseData> {\n // 1. Return from KV cache if still fresh\n const cached = await ctx.kv.get<LicenseData>(CACHE_KEY);\n if (cached && !isCacheExpired(cached)) {\n return cached;\n }\n\n // 2. Load license key — stored as an individual secret KV entry by settingsSchema\n const licenseKey = (await ctx.kv.get<string>('settings:licenseKey'))?.trim();\n if (!licenseKey) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'missing_key', capabilities: [] };\n }\n\n // 3. Call the MosierData validation endpoint via ctx.http (sandbox-safe)\n // Falls back to global fetch in trusted mode where ctx.http may not be wired.\n const httpFetch = ctx.http ? ctx.http.fetch.bind(ctx.http) : fetch;\n let data: ValidateApiResponse;\n try {\n const response = await httpFetch(VALIDATE_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ license_key: licenseKey })\n });\n\n if (!response.ok) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'api_error', capabilities: [] };\n }\n\n data = await response.json() as ValidateApiResponse;\n } catch {\n // Network failure: preserve any existing valid cache, fail open so GTM\n // keeps running, but flag as fallback so the dashboard stays gated.\n return { isValid: true, isFallback: true, capabilities: [] };\n }\n\n // 4. Verify Ed25519 signature\n const signatureValid = await verifyEd25519Signature(\n data.token.payload,\n data.token.signature,\n PUBLIC_KEY\n );\n if (!signatureValid) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'invalid_signature', capabilities: [] };\n }\n\n // 5. Decode and verify expiry\n const payload = JSON.parse(atob(data.token.payload)) as LicenseTokenPayload;\n if (payload.exp < Math.floor(Date.now() / 1000)) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'expired', capabilities: [] };\n }\n\n // 6. Write to KV cache (no TTL — expiry is checked via payload.exp on read)\n const licenseData: LicenseData = {\n isValid: true,\n tier: payload.tier,\n capabilities: payload.capabilities,\n sessionToken: data.sessionToken,\n expiresAt: payload.exp\n };\n\n await ctx.kv.set(CACHE_KEY, licenseData);\n return licenseData;\n}\n\nfunction isCacheExpired(cached: LicenseData): boolean {\n if (!cached.expiresAt) return false;\n return cached.expiresAt < Math.floor(Date.now() / 1000);\n}\n","import type { PageFragmentContribution } from 'emdash';\nimport type { LicenseCapability } from '../types';\n\nexport interface InjectorSettings {\n gtmEnabled: boolean;\n gtmId: string;\n ga4Enabled: boolean;\n ga4Id: string;\n metaPixelEnabled: boolean;\n metaPixelId: string;\n linkedInEnabled: boolean;\n linkedInPartnerId: string;\n tiktokEnabled: boolean;\n tiktokPixelId: string;\n bingEnabled: boolean;\n bingTagId: string;\n pinterestEnabled: boolean;\n pinterestTagId: string;\n nextdoorEnabled: boolean;\n nextdoorPixelId: string;\n dniSwapNumber: string;\n dniScriptUrl: string;\n customHeadCode: string;\n customFooterCode: string;\n debug: boolean;\n}\n\nconst MD_ROI_CDN = 'https://cdn.roiknowledge.com/assets/md-roi-emdash.js';\n\n/**\n * Builds the list of PageFragmentContributions for the page:fragments hook.\n * Returns structured contributions — never raw HTML interpolation at the\n * call site — so EmDash can validate, deduplicate, and render them safely.\n */\nexport function buildPageFragments(\n license: { capabilities: LicenseCapability[] },\n settings: InjectorSettings\n): PageFragmentContribution[] {\n const fragments: PageFragmentContribution[] = [];\n\n // 1. Google Tag Manager — official inline loader (initializes dataLayer and\n // pushes gtm.start before dynamically inserting gtm.js; required for consent\n // management and timing-dependent tags to work correctly)\n if (settings.gtmEnabled && settings.gtmId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${escapeJs(settings.gtmId)}');`,\n id: 'gtm-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=${escapeHtml(settings.gtmId)}\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>`,\n id: 'gtm-noscript'\n });\n }\n\n // 2. Google Analytics 4\n if (settings.ga4Enabled && settings.ga4Id) {\n fragments.push({\n kind: 'external-script',\n placement: 'head',\n src: `https://www.googletagmanager.com/gtag/js?id=${escapeJs(settings.ga4Id)}`,\n async: true,\n id: 'ga4-script'\n });\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '${escapeJs(settings.ga4Id)}');`,\n id: 'ga4-config'\n });\n }\n\n // 3. Meta (Facebook) Pixel\n if (settings.metaPixelEnabled && settings.metaPixelId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${escapeJs(settings.metaPixelId)}');fbq('track','PageView');`,\n id: 'meta-pixel-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none\" src=\"https://www.facebook.com/tr?id=${escapeHtml(settings.metaPixelId)}&amp;ev=PageView&amp;noscript=1\"/></noscript>`,\n id: 'meta-pixel-noscript'\n });\n }\n\n // 4. LinkedIn Insights Tag\n if (settings.linkedInEnabled && settings.linkedInPartnerId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window._linkedin_partner_id='${escapeJs(settings.linkedInPartnerId)}';window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];window._linkedin_data_partner_ids.push(window._linkedin_partner_id);(function(l){if(!l){window.lintrk=function(a,b){window.lintrk.q.push([a,b])};window.lintrk.q=[]}var s=document.getElementsByTagName('script')[0];var b=document.createElement('script');b.type='text/javascript';b.async=true;b.src='https://snap.licdn.com/li.lms-analytics/insight.min.js';s.parentNode.insertBefore(b,s)})(window.lintrk);`,\n id: 'linkedin-insight-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none;\" alt=\"\" src=\"https://px.ads.linkedin.com/collect/?pid=${escapeHtml(settings.linkedInPartnerId)}&amp;fmt=gif\"/></noscript>`,\n id: 'linkedin-insight-noscript'\n });\n }\n\n // 5. TikTok Pixel\n if (settings.tiktokEnabled && settings.tiktokPixelId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var s=document.createElement(\"script\");s.type=\"text/javascript\",s.async=!0,s.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var a=document.getElementsByTagName(\"script\")[0];a.parentNode.insertBefore(s,a)};ttq.load('${escapeJs(settings.tiktokPixelId)}');ttq.page();}(window,document,'ttq');`,\n id: 'tiktok-pixel-script'\n });\n }\n\n // 6. Microsoft (Bing) UET Tag\n if (settings.bingEnabled && settings.bingTagId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:'${escapeJs(settings.bingTagId)}',enableAutoSpaTracking:true};o.q=w[u],w[u]=new UET(o),w[u].push('pageLoad')},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=='loaded'&&s!=='complete'||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,'script','//bat.bing.com/bat.js','uetq');`,\n id: 'bing-uet-script'\n });\n }\n\n // 7. Pinterest Tag\n if (settings.pinterestEnabled && settings.pinterestTagId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${escapeJs(settings.pinterestTagId)}');pintrk('page');`,\n id: 'pinterest-tag-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none;\" alt=\"\" src=\"https://ct.pinterest.com/v3/?event=init&amp;tid=${escapeHtml(settings.pinterestTagId)}&amp;noscript=1\"/></noscript>`,\n id: 'pinterest-tag-noscript'\n });\n }\n\n // 8. Nextdoor Pixel\n if (settings.nextdoorEnabled && settings.nextdoorPixelId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(e,n){var t,p;e.ndp||((t=e.ndp=function(){t.handleRequest?t.handleRequest.apply(t,arguments):t.queue.push(arguments)}).queue=[],t.v=1,(p=n.createElement(e=\"script\")).async=!0,p.src=\"https://ads.nextdoor.com/public/pixel/ndp.js?id=${escapeJs(settings.nextdoorPixelId)}\",(n=n.getElementsByTagName(e)[0]).parentNode.insertBefore(p,n))}(window,document);ndp('init','${escapeJs(settings.nextdoorPixelId)}',{});ndp('track','PAGE_VIEW');`,\n id: 'nextdoor-pixel-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none\" src=\"https://flask.nextdoor.com/pixel?pid=${escapeHtml(settings.nextdoorPixelId)}&amp;ev=PAGE_VIEW&amp;noscript=1\"/></noscript>`,\n id: 'nextdoor-pixel-noscript'\n });\n }\n\n // 9. md-roi.js dataLayer engine (always injected for valid licenses)\n fragments.push({\n kind: 'external-script',\n placement: 'head',\n src: MD_ROI_CDN,\n defer: true,\n id: 'md-roi-script'\n });\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window.md_roii_settings = { gtm_id: '${escapeJs(settings.gtmEnabled ? settings.gtmId : '')}', debug: ${settings.debug} };`,\n id: 'md-roi-config'\n });\n\n // 10. AvidTrak DNI (Professional+ only)\n if (license.capabilities.includes('call_tracking') && settings.dniScriptUrl) {\n fragments.push({\n kind: 'external-script',\n placement: 'head',\n src: settings.dniScriptUrl,\n defer: true,\n id: 'avidtrak-script'\n });\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window.avidtrak_swap_number = '${escapeJs(settings.dniSwapNumber)}';`,\n id: 'avidtrak-config'\n });\n }\n\n // 11. Custom head / footer code (Free tier feature)\n if (settings.customHeadCode) {\n fragments.push({ kind: 'html', placement: 'head', content: settings.customHeadCode, id: 'custom-head' });\n }\n if (settings.customFooterCode) {\n fragments.push({ kind: 'html', placement: 'body:end', content: settings.customFooterCode, id: 'custom-footer' });\n }\n\n return fragments;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n}\n\nfunction escapeJs(value: string): string {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/<\\/script>/gi, '<\\\\/script>');\n}\n","import type { PluginContext } from 'emdash';\n\n/** Canonical tracking snapshot + revision (single options row via CAS). */\nexport const TRACKING_SETTINGS_DOC_KEY = 'state:trackingSettingsDoc';\n\nexport type TrackingSettingsDocument = {\n settingsRevision: number;\n gtmEnabled: boolean;\n gtmId: string;\n ga4Enabled: boolean;\n ga4Id: string;\n metaEnabled: boolean;\n metaId: string;\n linkedinEnabled: boolean;\n linkedinId: string;\n tiktokEnabled: boolean;\n tiktokId: string;\n bingEnabled: boolean;\n bingId: string;\n pinterestEnabled: boolean;\n pinterestId: string;\n nextdoorEnabled: boolean;\n nextdoorId: string;\n};\n\nexport type TrackingSaveBody = {\n settingsRevision?: number;\n} & Omit<TrackingSettingsDocument, 'settingsRevision'>;\n\nexport type KvWithCas = PluginContext['kv'] & {\n getRaw(key: string): Promise<string | null>;\n commitIfValueUnchanged(\n key: string,\n expectedRaw: string | null,\n newValue: unknown,\n ): Promise<boolean>;\n};\n\nexport function asKvWithCas(kv: PluginContext['kv']): KvWithCas {\n const k = kv as Partial<KvWithCas>;\n if (typeof k.getRaw !== 'function' || typeof k.commitIfValueUnchanged !== 'function') {\n throw new Error(\n 'KV must implement getRaw/commitIfValueUnchanged for atomic tracking saves. Install with patch-package (patches/emdash+0.1.0.patch).',\n );\n }\n return k as KvWithCas;\n}\n\nfunction isDocShape(v: unknown): v is TrackingSettingsDocument {\n if (typeof v !== 'object' || v === null) return false;\n const o = v as Record<string, unknown>;\n return typeof o.settingsRevision === 'number' && typeof o.gtmEnabled === 'boolean';\n}\n\nexport async function loadLegacyTrackingDocument(\n ctx: Pick<PluginContext, 'kv'>,\n): Promise<TrackingSettingsDocument> {\n const [\n gtmEnabled,\n gtmId,\n ga4Enabled,\n ga4Id,\n metaPixelEnabled,\n metaPixelId,\n linkedInEnabled,\n linkedInPartnerId,\n tiktokEnabled,\n tiktokPixelId,\n bingEnabled,\n bingTagId,\n pinterestEnabled,\n pinterestTagId,\n nextdoorEnabled,\n nextdoorPixelId,\n settingsRevision,\n ] = await Promise.all([\n ctx.kv.get<boolean>('settings:gtmEnabled'),\n ctx.kv.get<string>('settings:gtmId'),\n ctx.kv.get<boolean>('settings:ga4Enabled'),\n ctx.kv.get<string>('settings:ga4Id'),\n ctx.kv.get<boolean>('settings:metaPixelEnabled'),\n ctx.kv.get<string>('settings:metaPixelId'),\n ctx.kv.get<boolean>('settings:linkedInEnabled'),\n ctx.kv.get<string>('settings:linkedInPartnerId'),\n ctx.kv.get<boolean>('settings:tiktokEnabled'),\n ctx.kv.get<string>('settings:tiktokPixelId'),\n ctx.kv.get<boolean>('settings:bingEnabled'),\n ctx.kv.get<string>('settings:bingTagId'),\n ctx.kv.get<boolean>('settings:pinterestEnabled'),\n ctx.kv.get<string>('settings:pinterestTagId'),\n ctx.kv.get<boolean>('settings:nextdoorEnabled'),\n ctx.kv.get<string>('settings:nextdoorPixelId'),\n ctx.kv.get<number>('settings:trackingSettingsRevision'),\n ]);\n\n return {\n settingsRevision: settingsRevision ?? 0,\n // Backward compat: existing installs have settings:gtmId but not settings:gtmEnabled.\n // Infer enabled from presence of an ID so GTM keeps injecting after upgrade.\n gtmEnabled: gtmEnabled ?? (!!gtmId),\n gtmId: gtmId ?? '',\n ga4Enabled: ga4Enabled ?? false,\n ga4Id: ga4Id ?? '',\n metaEnabled: metaPixelEnabled ?? false,\n metaId: metaPixelId ?? '',\n linkedinEnabled: linkedInEnabled ?? false,\n linkedinId: linkedInPartnerId ?? '',\n tiktokEnabled: tiktokEnabled ?? false,\n tiktokId: tiktokPixelId ?? '',\n bingEnabled: bingEnabled ?? false,\n bingId: bingTagId ?? '',\n pinterestEnabled: pinterestEnabled ?? false,\n pinterestId: pinterestTagId ?? '',\n nextdoorEnabled: nextdoorEnabled ?? false,\n nextdoorId: nextdoorPixelId ?? '',\n };\n}\n\nexport async function loadTrackingSettingsDocument(\n ctx: Pick<PluginContext, 'kv'>,\n): Promise<TrackingSettingsDocument> {\n // Always read from settings:* so edits made through the auto-generated\n // settingsSchema form are reflected here. The canonical doc (state:trackingSettingsDoc)\n // is used only by saveTrackingSettings for CAS; it is not the runtime source of truth.\n return loadLegacyTrackingDocument(ctx);\n}\n\n/**\n * Ensure the canonical doc exists so saveTrackingSettings always has a snapshot\n * for conflict detection. Called when the /tracking UI loads settings — by save\n * time the doc exists and the field-by-field stale check runs.\n */\nexport async function ensureCanonicalDocExists(\n ctx: Pick<PluginContext, 'kv'>,\n doc: TrackingSettingsDocument,\n): Promise<void> {\n const kv = asKvWithCas(ctx.kv);\n const existing = await kv.getRaw(TRACKING_SETTINGS_DOC_KEY);\n if (existing === null) {\n await kv.commitIfValueUnchanged(TRACKING_SETTINGS_DOC_KEY, null, doc);\n }\n}\n\nexport async function mirrorTrackingDocumentToSettingsKeys(\n ctx: Pick<PluginContext, 'kv'>,\n doc: TrackingSettingsDocument,\n): Promise<void> {\n await Promise.all([\n ctx.kv.set('settings:gtmEnabled', doc.gtmEnabled),\n ctx.kv.set('settings:gtmId', doc.gtmId),\n ctx.kv.set('settings:ga4Enabled', doc.ga4Enabled),\n ctx.kv.set('settings:ga4Id', doc.ga4Id),\n ctx.kv.set('settings:metaPixelEnabled', doc.metaEnabled),\n ctx.kv.set('settings:metaPixelId', doc.metaId),\n ctx.kv.set('settings:linkedInEnabled', doc.linkedinEnabled),\n ctx.kv.set('settings:linkedInPartnerId', doc.linkedinId),\n ctx.kv.set('settings:tiktokEnabled', doc.tiktokEnabled),\n ctx.kv.set('settings:tiktokPixelId', doc.tiktokId),\n ctx.kv.set('settings:bingEnabled', doc.bingEnabled),\n ctx.kv.set('settings:bingTagId', doc.bingId),\n ctx.kv.set('settings:pinterestEnabled', doc.pinterestEnabled),\n ctx.kv.set('settings:pinterestTagId', doc.pinterestId),\n ctx.kv.set('settings:nextdoorEnabled', doc.nextdoorEnabled),\n ctx.kv.set('settings:nextdoorPixelId', doc.nextdoorId),\n ctx.kv.set('settings:trackingSettingsRevision', doc.settingsRevision),\n ]);\n}\n\nexport async function saveTrackingSettings(\n ctx: Pick<PluginContext, 'kv'>,\n body: TrackingSaveBody,\n): Promise<\n { ok: true; settingsRevision: number } | { ok: false; conflict: true; settingsRevision: number }\n> {\n const kv = asKvWithCas(ctx.kv);\n const maxAttempts = 32;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const expectedRaw = await kv.getRaw(TRACKING_SETTINGS_DOC_KEY);\n let current: TrackingSettingsDocument;\n if (expectedRaw === null) {\n current = await loadLegacyTrackingDocument(ctx);\n } else {\n // Merge live settings:* values (captures auto-form edits) with the canonical\n // doc's revision (authoritative for CAS). This prevents a stale /tracking\n // save from silently overwriting newer edits made through settingsSchema.\n const canonicalDoc = JSON.parse(expectedRaw) as TrackingSettingsDocument;\n const liveDoc = await loadLegacyTrackingDocument(ctx);\n current = { ...liveDoc, settingsRevision: canonicalDoc.settingsRevision };\n }\n\n if (\n body.settingsRevision !== undefined &&\n body.settingsRevision !== current.settingsRevision\n ) {\n return { ok: false, conflict: true, settingsRevision: current.settingsRevision };\n }\n\n // Detect settings-schema edits that the body would silently overwrite.\n // The settings-schema form writes settings:* without bumping settingsRevision,\n // so the revision check above cannot catch this race. Compare each body field\n // against the live settings:* value; if they diverge and body still carries the\n // stale canonical value, the /tracking snapshot is out of date — reject it.\n if (expectedRaw !== null) {\n const canonicalDoc = JSON.parse(expectedRaw) as TrackingSettingsDocument;\n const wouldClobber = (\n ['gtmEnabled','gtmId','ga4Enabled','ga4Id','metaEnabled','metaId',\n 'linkedinEnabled','linkedinId','tiktokEnabled','tiktokId','bingEnabled','bingId',\n 'pinterestEnabled','pinterestId','nextdoorEnabled','nextdoorId'] as const\n ).some(\n (field) =>\n current[field] !== canonicalDoc[field] &&\n (body as Record<string, unknown>)[field] === (canonicalDoc as Record<string, unknown>)[field],\n );\n if (wouldClobber) {\n return { ok: false, conflict: true, settingsRevision: current.settingsRevision };\n }\n }\n\n const nextDoc: TrackingSettingsDocument = {\n ...current,\n gtmEnabled: body.gtmEnabled,\n gtmId: body.gtmId,\n ga4Enabled: body.ga4Enabled,\n ga4Id: body.ga4Id,\n metaEnabled: body.metaEnabled,\n metaId: body.metaId,\n linkedinEnabled: body.linkedinEnabled,\n linkedinId: body.linkedinId,\n tiktokEnabled: body.tiktokEnabled,\n tiktokId: body.tiktokId,\n bingEnabled: body.bingEnabled,\n bingId: body.bingId,\n pinterestEnabled: body.pinterestEnabled,\n pinterestId: body.pinterestId,\n nextdoorEnabled: body.nextdoorEnabled,\n nextdoorId: body.nextdoorId,\n settingsRevision: current.settingsRevision + 1,\n };\n\n const committed = await kv.commitIfValueUnchanged(\n TRACKING_SETTINGS_DOC_KEY,\n expectedRaw,\n nextDoc,\n );\n if (committed) {\n await mirrorTrackingDocumentToSettingsKeys(ctx, nextDoc);\n return { ok: true, settingsRevision: nextDoc.settingsRevision };\n }\n }\n\n const cur = await loadTrackingSettingsDocument(ctx);\n return { ok: false, conflict: true, settingsRevision: cur.settingsRevision };\n}\n\nexport function documentToApiResponse(doc: TrackingSettingsDocument) {\n return {\n gtmEnabled: doc.gtmEnabled,\n gtmId: doc.gtmId,\n ga4Enabled: doc.ga4Enabled,\n ga4Id: doc.ga4Id,\n metaEnabled: doc.metaEnabled,\n metaId: doc.metaId,\n linkedinEnabled: doc.linkedinEnabled,\n linkedinId: doc.linkedinId,\n tiktokEnabled: doc.tiktokEnabled,\n tiktokId: doc.tiktokId,\n bingEnabled: doc.bingEnabled,\n bingId: doc.bingId,\n pinterestEnabled: doc.pinterestEnabled,\n pinterestId: doc.pinterestId,\n nextdoorEnabled: doc.nextdoorEnabled,\n nextdoorId: doc.nextdoorId,\n settingsRevision: doc.settingsRevision,\n };\n}\n","import { definePlugin } from 'emdash';\nimport type { LicenseData } from './types';\nimport { validateLicense, CACHE_KEY } from './lib/licensing';\nimport { buildPageFragments } from './frontend/injector';\nimport {\n documentToApiResponse,\n ensureCanonicalDocExists,\n loadTrackingSettingsDocument,\n saveTrackingSettings,\n type TrackingSaveBody,\n} from './lib/trackingSettingsDocument';\n\nexport function createPlugin() {\n return definePlugin({\n id: 'roi-insights',\n version: '1.0.0',\n capabilities: ['network:fetch'],\n allowedHosts: ['api.roiknowledge.com'],\n\n // Simple user-configurable settings — auto-generates a settings form.\n // Values are stored by EmDash at individual settings:<field> KV keys.\n admin: {\n entry: '@mosierdata/emdash-plugin-analytics/admin',\n settingsSchema: {\n licenseKey: {\n type: 'secret',\n label: 'License Key',\n description: \"Get this from your MosierData portal. Prefix: qdsh_\"\n },\n gtmEnabled: {\n type: 'boolean',\n label: 'Enable Google Tag Manager',\n default: false\n },\n gtmId: {\n type: 'string',\n label: 'Google Tag Manager ID',\n description: 'e.g. GTM-XXXXXXX',\n default: ''\n },\n ga4Enabled: {\n type: 'boolean',\n label: 'Enable Google Analytics 4',\n default: false\n },\n ga4Id: {\n type: 'string',\n label: 'Google Analytics 4 Measurement ID',\n description: 'e.g. G-XXXXXXXXXX',\n default: ''\n },\n metaPixelEnabled: {\n type: 'boolean',\n label: 'Enable Meta (Facebook) Pixel',\n default: false\n },\n metaPixelId: {\n type: 'string',\n label: 'Meta (Facebook) Pixel ID',\n description: 'Numeric ID from Meta Events Manager',\n default: ''\n },\n linkedInEnabled: {\n type: 'boolean',\n label: 'Enable LinkedIn Insights Tag',\n default: false\n },\n linkedInPartnerId: {\n type: 'string',\n label: 'LinkedIn Insights Tag Partner ID',\n description: 'Numeric Partner ID from LinkedIn Campaign Manager',\n default: ''\n },\n tiktokEnabled: {\n type: 'boolean',\n label: 'Enable TikTok Pixel',\n default: false\n },\n tiktokPixelId: {\n type: 'string',\n label: 'TikTok Pixel ID',\n description: 'Alphanumeric ID from TikTok Events Manager',\n default: ''\n },\n bingEnabled: {\n type: 'boolean',\n label: 'Enable Microsoft (Bing) UET Tag',\n default: false\n },\n bingTagId: {\n type: 'string',\n label: 'Microsoft UET Tag ID',\n description: 'Numeric Tag ID from Microsoft Advertising',\n default: ''\n },\n pinterestEnabled: {\n type: 'boolean',\n label: 'Enable Pinterest Tag',\n default: false\n },\n pinterestTagId: {\n type: 'string',\n label: 'Pinterest Tag ID',\n description: 'Numeric Tag ID from Pinterest Ads Manager',\n default: ''\n },\n nextdoorEnabled: {\n type: 'boolean',\n label: 'Enable Nextdoor Pixel',\n default: false\n },\n nextdoorPixelId: {\n type: 'string',\n label: 'Nextdoor Data Source ID',\n description: 'UUID from Nextdoor Business Ads dashboard',\n default: ''\n },\n dniSwapNumber: {\n type: 'string',\n label: 'Website Phone Number to Swap',\n description: 'Phone number on your site that AvidTrak will dynamically replace.',\n default: ''\n },\n dniScriptUrl: {\n type: 'string',\n label: 'AvidTrak Script URL',\n description: 'Provided by AvidTrak after provisioning a tracking number.',\n default: ''\n },\n customHeadCode: {\n type: 'string',\n label: 'Custom <head> Code',\n multiline: true,\n default: ''\n },\n customFooterCode: {\n type: 'string',\n label: 'Custom Footer Code',\n multiline: true,\n default: ''\n },\n debug: {\n type: 'boolean',\n label: 'Debug Mode',\n default: false\n }\n },\n pages: [\n { path: '/dashboard', label: 'Marketing ROI', icon: 'chart' },\n { path: '/tracking', label: 'Tracking Pixels', icon: 'tracking' },\n { path: '/settings', label: 'License & Google', icon: 'settings' }\n ]\n },\n\n hooks: {\n 'plugin:install': async (_event, ctx) => {\n // Seed schema defaults so page:fragments has values on first render\n // before the user visits the auto-generated settings form.\n await ctx.kv.set('settings:debug', false);\n ctx.log.info('ROI Insights installed');\n },\n\n // Trusted-only hook — injects GTM, md-roi.js, and AvidTrak DNI into\n // every page <head> and <body>. Uses validateLicense (not a raw KV read)\n // so the plugin auto-revalidates after a restart or cache loss without\n // requiring an admin to click Activate. Settings are read fresh from KV\n // on each render so form changes take effect on the next request.\n 'page:fragments': async (_event, ctx) => {\n const license = await validateLicense(ctx);\n if (!license.isValid) return null;\n\n const tracking = await loadTrackingSettingsDocument(ctx);\n const [dniSwapNumber, dniScriptUrl, customHeadCode, customFooterCode, debug] =\n await Promise.all([\n ctx.kv.get<string>('settings:dniSwapNumber'),\n ctx.kv.get<string>('settings:dniScriptUrl'),\n ctx.kv.get<string>('settings:customHeadCode'),\n ctx.kv.get<string>('settings:customFooterCode'),\n ctx.kv.get<boolean>('settings:debug'),\n ]);\n\n return buildPageFragments(license, {\n gtmEnabled: tracking.gtmEnabled,\n gtmId: tracking.gtmId,\n ga4Enabled: tracking.ga4Enabled,\n ga4Id: tracking.ga4Id,\n metaPixelEnabled: tracking.metaEnabled,\n metaPixelId: tracking.metaId,\n linkedInEnabled: tracking.linkedinEnabled,\n linkedInPartnerId: tracking.linkedinId,\n tiktokEnabled: tracking.tiktokEnabled,\n tiktokPixelId: tracking.tiktokId,\n bingEnabled: tracking.bingEnabled,\n bingTagId: tracking.bingId,\n pinterestEnabled: tracking.pinterestEnabled,\n pinterestTagId: tracking.pinterestId,\n nextdoorEnabled: tracking.nextdoorEnabled,\n nextdoorPixelId: tracking.nextdoorId,\n dniSwapNumber: dniSwapNumber ?? '',\n dniScriptUrl: dniScriptUrl ?? '',\n customHeadCode: customHeadCode ?? '',\n customFooterCode: customFooterCode ?? '',\n debug: debug ?? false,\n });\n }\n },\n\n routes: {\n // Returns the current license state. Calls validateLicense so the cache\n // is primed automatically on a cold start rather than returning not_validated.\n 'license/status': {\n handler: async (ctx) => {\n return validateLicense(ctx);\n }\n },\n\n // Forces a fresh API check. Snapshots the current cache first so a\n // transient network outage during revalidation does not disable tracking.\n 'license/validate': {\n handler: async (ctx) => {\n const existing = await ctx.kv.get<LicenseData>(CACHE_KEY);\n\n // Expire the cache so validateLicense skips it and hits the API\n if (existing) {\n await ctx.kv.set(CACHE_KEY, { ...existing, expiresAt: 0 });\n }\n\n const result = await validateLicense(ctx);\n\n // Network failure: restore the prior valid non-fallback cache so\n // page:fragments continues injecting scripts (fail-open preserved)\n if (result.isFallback && existing?.isValid && !existing.isFallback) {\n await ctx.kv.set(CACHE_KEY, existing);\n }\n\n return result;\n }\n },\n\n // Initiates the Google OAuth flow. Returns { authUrl } on success.\n 'google-oauth/initiate': {\n handler: async (ctx) => {\n const licenseKey = await ctx.kv.get<string>('settings:licenseKey');\n if (!licenseKey) {\n return { error: 'License key not saved. Configure it in Settings first.' };\n }\n const domain = new URL(ctx.request.url).origin;\n const httpFetch = ctx.http ? ctx.http.fetch.bind(ctx.http) : fetch;\n const response = await httpFetch(\n 'https://api.roiknowledge.com/api/roi/plugin/oauth/google/initiate',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ license_key: licenseKey, domain })\n }\n );\n if (!response.ok) return { error: 'Backend rejected OAuth initiation.' };\n return response.json();\n }\n },\n\n // Returns all tracking pixel settings mapped to TrackingValues field names.\n 'tracking/settings': {\n handler: async (ctx) => {\n const doc = await loadTrackingSettingsDocument(ctx);\n // Snapshot current state so saveTrackingSettings can detect races\n // with the settings-schema form even on first-ever /tracking load.\n await ensureCanonicalDocExists(ctx, doc);\n return documentToApiResponse(doc);\n }\n },\n\n // Saves tracking pixel settings from the custom UI back to the KV store.\n 'tracking/save': {\n handler: async (ctx) => {\n const body = await ctx.request.json() as TrackingSaveBody;\n return saveTrackingSettings(ctx, body);\n }\n },\n\n // Returns the current Google connection state.\n 'google-oauth/status': {\n handler: async (ctx) => {\n const connected = await ctx.kv.get<boolean>('state:googleConnected') ?? false;\n return { connected };\n }\n },\n\n // Called after a successful OAuth redirect to persist the connected flag.\n 'google-oauth/connected': {\n handler: async (ctx) => {\n await ctx.kv.set('state:googleConnected', true);\n return { connected: true };\n }\n }\n }\n });\n}\n\nexport default createPlugin;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/crypto.ts","../src/lib/licensing.ts","../src/frontend/injector.ts","../src/lib/trackingSettingsDocument.ts","../src/index.ts"],"sourcesContent":["/**\n * Verifies an Ed25519 signature using the Web Crypto API.\n *\n * @param payload - Base64-encoded payload string (as returned by the API)\n * @param signature - Base64-encoded Ed25519 signature\n * @param publicKey - Base64-encoded 32-byte Ed25519 public key (hardcoded in plugin)\n */\nexport async function verifyEd25519Signature(\n payload: string,\n signature: string,\n publicKey: string\n): Promise<boolean> {\n try {\n const keyBytes = base64ToBytes(publicKey);\n const sigBytes = base64ToBytes(signature);\n const msgBytes = new TextEncoder().encode(payload);\n\n const cryptoKey = await crypto.subtle.importKey(\n 'raw',\n keyBytes,\n { name: 'Ed25519' },\n false,\n ['verify']\n );\n\n return await crypto.subtle.verify('Ed25519', cryptoKey, sigBytes, msgBytes);\n } catch {\n return false;\n }\n}\n\nfunction base64ToBytes(base64: string): Uint8Array {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n","import type { PluginContext } from 'emdash';\nimport type { LicenseData, LicenseTokenPayload, ValidateApiResponse } from '../types';\nimport { verifyEd25519Signature } from './crypto';\n\nexport const CACHE_KEY = 'state:licenseCache';\n\nconst VALIDATE_URL = 'https://api.roiknowledge.com/api/roi/plugin/validate';\n\n// Ed25519 public key from the MosierData signing keypair.\nconst PUBLIC_KEY = 'COwQzXhDeQC9uxAdyNFdbFbIrwLAGgtRZlhfAxbR0Dk=';\n\nexport async function validateLicense(ctx: PluginContext): Promise<LicenseData> {\n // 1. Return from KV cache if still fresh\n const cached = await ctx.kv.get<LicenseData>(CACHE_KEY);\n if (cached && !isCacheExpired(cached)) {\n return cached;\n }\n\n // 2. Load license key — stored as an individual secret KV entry by settingsSchema\n const licenseKey = (await ctx.kv.get<string>('settings:licenseKey'))?.trim();\n if (!licenseKey) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'missing_key', capabilities: [] };\n }\n\n // 3. Call the MosierData validation endpoint via ctx.http (sandbox-safe)\n // Falls back to global fetch in trusted mode where ctx.http may not be wired.\n const httpFetch = ctx.http ? ctx.http.fetch.bind(ctx.http) : fetch;\n let data: ValidateApiResponse;\n try {\n const response = await httpFetch(VALIDATE_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ license_key: licenseKey })\n });\n\n if (!response.ok) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'api_error', capabilities: [] };\n }\n\n data = await response.json() as ValidateApiResponse;\n } catch {\n // Network failure: preserve any existing valid cache, fail open so GTM\n // keeps running, but flag as fallback so the dashboard stays gated.\n return { isValid: true, isFallback: true, capabilities: [] };\n }\n\n // 4. Verify Ed25519 signature\n const signatureValid = await verifyEd25519Signature(\n data.token.payload,\n data.token.signature,\n PUBLIC_KEY\n );\n if (!signatureValid) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'invalid_signature', capabilities: [] };\n }\n\n // 5. Decode and verify expiry\n const payload = JSON.parse(atob(data.token.payload)) as LicenseTokenPayload;\n if (payload.exp < Math.floor(Date.now() / 1000)) {\n await ctx.kv.delete(CACHE_KEY);\n return { isValid: false, reason: 'expired', capabilities: [] };\n }\n\n // 6. Write to KV cache (no TTL — expiry is checked via payload.exp on read)\n const licenseData: LicenseData = {\n isValid: true,\n tier: payload.tier,\n capabilities: payload.capabilities,\n sessionToken: data.sessionToken,\n expiresAt: payload.exp\n };\n\n await ctx.kv.set(CACHE_KEY, licenseData);\n return licenseData;\n}\n\nfunction isCacheExpired(cached: LicenseData): boolean {\n if (!cached.expiresAt) return false;\n return cached.expiresAt < Math.floor(Date.now() / 1000);\n}\n","import type { PageFragmentContribution } from 'emdash';\nimport type { LicenseCapability } from '../types';\n\nexport interface InjectorSettings {\n gtmEnabled: boolean;\n gtmId: string;\n ga4Enabled: boolean;\n ga4Id: string;\n metaPixelEnabled: boolean;\n metaPixelId: string;\n linkedInEnabled: boolean;\n linkedInPartnerId: string;\n tiktokEnabled: boolean;\n tiktokPixelId: string;\n bingEnabled: boolean;\n bingTagId: string;\n pinterestEnabled: boolean;\n pinterestTagId: string;\n nextdoorEnabled: boolean;\n nextdoorPixelId: string;\n dniSwapNumber: string;\n dniScriptUrl: string;\n customHeadCode: string;\n customFooterCode: string;\n debug: boolean;\n}\n\nconst MD_ROI_CDN = 'https://cdn.roiknowledge.com/assets/md-roi-emdash.js';\n\n/**\n * Builds the list of PageFragmentContributions for the page:fragments hook.\n * Returns structured contributions — never raw HTML interpolation at the\n * call site — so EmDash can validate, deduplicate, and render them safely.\n */\nexport function buildPageFragments(\n license: { capabilities: LicenseCapability[] },\n settings: InjectorSettings\n): PageFragmentContribution[] {\n const fragments: PageFragmentContribution[] = [];\n\n // 1. Google Tag Manager — official inline loader (initializes dataLayer and\n // pushes gtm.start before dynamically inserting gtm.js; required for consent\n // management and timing-dependent tags to work correctly)\n if (settings.gtmEnabled && settings.gtmId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${escapeJs(settings.gtmId)}');`,\n id: 'gtm-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><iframe src=\"https://www.googletagmanager.com/ns.html?id=${escapeHtml(settings.gtmId)}\" height=\"0\" width=\"0\" style=\"display:none;visibility:hidden\"></iframe></noscript>`,\n id: 'gtm-noscript'\n });\n }\n\n // 2. Google Analytics 4\n if (settings.ga4Enabled && settings.ga4Id) {\n fragments.push({\n kind: 'external-script',\n placement: 'head',\n src: `https://www.googletagmanager.com/gtag/js?id=${escapeJs(settings.ga4Id)}`,\n async: true,\n id: 'ga4-script'\n });\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '${escapeJs(settings.ga4Id)}');`,\n id: 'ga4-config'\n });\n }\n\n // 3. Meta (Facebook) Pixel\n if (settings.metaPixelEnabled && settings.metaPixelId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${escapeJs(settings.metaPixelId)}');fbq('track','PageView');`,\n id: 'meta-pixel-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none\" src=\"https://www.facebook.com/tr?id=${escapeHtml(settings.metaPixelId)}&amp;ev=PageView&amp;noscript=1\"/></noscript>`,\n id: 'meta-pixel-noscript'\n });\n }\n\n // 4. LinkedIn Insights Tag\n if (settings.linkedInEnabled && settings.linkedInPartnerId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window._linkedin_partner_id='${escapeJs(settings.linkedInPartnerId)}';window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];window._linkedin_data_partner_ids.push(window._linkedin_partner_id);(function(l){if(!l){window.lintrk=function(a,b){window.lintrk.q.push([a,b])};window.lintrk.q=[]}var s=document.getElementsByTagName('script')[0];var b=document.createElement('script');b.type='text/javascript';b.async=true;b.src='https://snap.licdn.com/li.lms-analytics/insight.min.js';s.parentNode.insertBefore(b,s)})(window.lintrk);`,\n id: 'linkedin-insight-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none;\" alt=\"\" src=\"https://px.ads.linkedin.com/collect/?pid=${escapeHtml(settings.linkedInPartnerId)}&amp;fmt=gif\"/></noscript>`,\n id: 'linkedin-insight-noscript'\n });\n }\n\n // 5. TikTok Pixel\n if (settings.tiktokEnabled && settings.tiktokPixelId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var s=document.createElement(\"script\");s.type=\"text/javascript\",s.async=!0,s.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var a=document.getElementsByTagName(\"script\")[0];a.parentNode.insertBefore(s,a)};ttq.load('${escapeJs(settings.tiktokPixelId)}');ttq.page();}(window,document,'ttq');`,\n id: 'tiktok-pixel-script'\n });\n }\n\n // 6. Microsoft (Bing) UET Tag\n if (settings.bingEnabled && settings.bingTagId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:'${escapeJs(settings.bingTagId)}',enableAutoSpaTracking:true};o.q=w[u],w[u]=new UET(o),w[u].push('pageLoad')},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=='loaded'&&s!=='complete'||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,'script','//bat.bing.com/bat.js','uetq');`,\n id: 'bing-uet-script'\n });\n }\n\n // 7. Pinterest Tag\n if (settings.pinterestEnabled && settings.pinterestTagId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${escapeJs(settings.pinterestTagId)}');pintrk('page');`,\n id: 'pinterest-tag-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none;\" alt=\"\" src=\"https://ct.pinterest.com/v3/?event=init&amp;tid=${escapeHtml(settings.pinterestTagId)}&amp;noscript=1\"/></noscript>`,\n id: 'pinterest-tag-noscript'\n });\n }\n\n // 8. Nextdoor Pixel\n if (settings.nextdoorEnabled && settings.nextdoorPixelId) {\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `!function(e,n){var t,p;e.ndp||((t=e.ndp=function(){t.handleRequest?t.handleRequest.apply(t,arguments):t.queue.push(arguments)}).queue=[],t.v=1,(p=n.createElement(e=\"script\")).async=!0,p.src=\"https://ads.nextdoor.com/public/pixel/ndp.js?id=${escapeJs(settings.nextdoorPixelId)}\",(n=n.getElementsByTagName(e)[0]).parentNode.insertBefore(p,n))}(window,document);ndp('init','${escapeJs(settings.nextdoorPixelId)}',{});ndp('track','PAGE_VIEW');`,\n id: 'nextdoor-pixel-script'\n });\n fragments.push({\n kind: 'html',\n placement: 'body:start',\n content: `<noscript><img height=\"1\" width=\"1\" style=\"display:none\" src=\"https://flask.nextdoor.com/pixel?pid=${escapeHtml(settings.nextdoorPixelId)}&amp;ev=PAGE_VIEW&amp;noscript=1\"/></noscript>`,\n id: 'nextdoor-pixel-noscript'\n });\n }\n\n // 9. md-roi.js dataLayer engine (always injected for valid licenses)\n fragments.push({\n kind: 'external-script',\n placement: 'head',\n src: MD_ROI_CDN,\n defer: true,\n id: 'md-roi-script'\n });\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window.md_roii_settings = { gtm_id: '${escapeJs(settings.gtmEnabled ? settings.gtmId : '')}', debug: ${settings.debug} };`,\n id: 'md-roi-config'\n });\n\n // 10. AvidTrak DNI (Professional+ only)\n if (license.capabilities.includes('call_tracking') && settings.dniScriptUrl) {\n fragments.push({\n kind: 'external-script',\n placement: 'head',\n src: settings.dniScriptUrl,\n defer: true,\n id: 'avidtrak-script'\n });\n fragments.push({\n kind: 'inline-script',\n placement: 'head',\n content: `window.avidtrak_swap_number = '${escapeJs(settings.dniSwapNumber)}';`,\n id: 'avidtrak-config'\n });\n }\n\n // 11. Custom head / footer code (Free tier feature)\n if (settings.customHeadCode) {\n fragments.push({ kind: 'html', placement: 'head', content: settings.customHeadCode, id: 'custom-head' });\n }\n if (settings.customFooterCode) {\n fragments.push({ kind: 'html', placement: 'body:end', content: settings.customFooterCode, id: 'custom-footer' });\n }\n\n return fragments;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n}\n\nfunction escapeJs(value: string): string {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/<\\/script>/gi, '<\\\\/script>');\n}\n","import type { PluginContext } from 'emdash';\n\n/** Canonical tracking snapshot + revision (single options row via CAS). */\nexport const TRACKING_SETTINGS_DOC_KEY = 'state:trackingSettingsDoc';\n\nexport type TrackingSettingsDocument = {\n settingsRevision: number;\n gtmEnabled: boolean;\n gtmId: string;\n ga4Enabled: boolean;\n ga4Id: string;\n metaEnabled: boolean;\n metaId: string;\n linkedinEnabled: boolean;\n linkedinId: string;\n tiktokEnabled: boolean;\n tiktokId: string;\n bingEnabled: boolean;\n bingId: string;\n pinterestEnabled: boolean;\n pinterestId: string;\n nextdoorEnabled: boolean;\n nextdoorId: string;\n};\n\nexport type TrackingSaveBody = {\n settingsRevision?: number;\n} & Omit<TrackingSettingsDocument, 'settingsRevision'>;\n\nexport type KvWithCas = PluginContext['kv'] & {\n getRaw(key: string): Promise<string | null>;\n commitIfValueUnchanged(\n key: string,\n expectedRaw: string | null,\n newValue: unknown,\n ): Promise<boolean>;\n};\n\nexport function asKvWithCas(kv: PluginContext['kv']): KvWithCas {\n const k = kv as Partial<KvWithCas>;\n if (typeof k.getRaw !== 'function' || typeof k.commitIfValueUnchanged !== 'function') {\n throw new Error(\n 'KV must implement getRaw/commitIfValueUnchanged for atomic tracking saves. Install with patch-package (patches/emdash+0.1.0.patch).',\n );\n }\n return k as KvWithCas;\n}\n\nfunction isDocShape(v: unknown): v is TrackingSettingsDocument {\n if (typeof v !== 'object' || v === null) return false;\n const o = v as Record<string, unknown>;\n return typeof o.settingsRevision === 'number' && typeof o.gtmEnabled === 'boolean';\n}\n\nexport async function loadLegacyTrackingDocument(\n ctx: Pick<PluginContext, 'kv'>,\n): Promise<TrackingSettingsDocument> {\n const [\n gtmEnabled,\n gtmId,\n ga4Enabled,\n ga4Id,\n metaPixelEnabled,\n metaPixelId,\n linkedInEnabled,\n linkedInPartnerId,\n tiktokEnabled,\n tiktokPixelId,\n bingEnabled,\n bingTagId,\n pinterestEnabled,\n pinterestTagId,\n nextdoorEnabled,\n nextdoorPixelId,\n settingsRevision,\n ] = await Promise.all([\n ctx.kv.get<boolean>('settings:gtmEnabled'),\n ctx.kv.get<string>('settings:gtmId'),\n ctx.kv.get<boolean>('settings:ga4Enabled'),\n ctx.kv.get<string>('settings:ga4Id'),\n ctx.kv.get<boolean>('settings:metaPixelEnabled'),\n ctx.kv.get<string>('settings:metaPixelId'),\n ctx.kv.get<boolean>('settings:linkedInEnabled'),\n ctx.kv.get<string>('settings:linkedInPartnerId'),\n ctx.kv.get<boolean>('settings:tiktokEnabled'),\n ctx.kv.get<string>('settings:tiktokPixelId'),\n ctx.kv.get<boolean>('settings:bingEnabled'),\n ctx.kv.get<string>('settings:bingTagId'),\n ctx.kv.get<boolean>('settings:pinterestEnabled'),\n ctx.kv.get<string>('settings:pinterestTagId'),\n ctx.kv.get<boolean>('settings:nextdoorEnabled'),\n ctx.kv.get<string>('settings:nextdoorPixelId'),\n ctx.kv.get<number>('settings:trackingSettingsRevision'),\n ]);\n\n return {\n settingsRevision: settingsRevision ?? 0,\n // Backward compat: existing installs have settings:gtmId but not settings:gtmEnabled.\n // Infer enabled from presence of an ID so GTM keeps injecting after upgrade.\n gtmEnabled: gtmEnabled ?? (!!gtmId),\n gtmId: gtmId ?? '',\n ga4Enabled: ga4Enabled ?? false,\n ga4Id: ga4Id ?? '',\n metaEnabled: metaPixelEnabled ?? false,\n metaId: metaPixelId ?? '',\n linkedinEnabled: linkedInEnabled ?? false,\n linkedinId: linkedInPartnerId ?? '',\n tiktokEnabled: tiktokEnabled ?? false,\n tiktokId: tiktokPixelId ?? '',\n bingEnabled: bingEnabled ?? false,\n bingId: bingTagId ?? '',\n pinterestEnabled: pinterestEnabled ?? false,\n pinterestId: pinterestTagId ?? '',\n nextdoorEnabled: nextdoorEnabled ?? false,\n nextdoorId: nextdoorPixelId ?? '',\n };\n}\n\nexport async function loadTrackingSettingsDocument(\n ctx: Pick<PluginContext, 'kv'>,\n): Promise<TrackingSettingsDocument> {\n // Always read from settings:* so edits made through the auto-generated\n // settingsSchema form are reflected here. The canonical doc (state:trackingSettingsDoc)\n // is used only by saveTrackingSettings for CAS; it is not the runtime source of truth.\n return loadLegacyTrackingDocument(ctx);\n}\n\n/**\n * Ensure the canonical doc exists so saveTrackingSettings always has a snapshot\n * for conflict detection. Called when the /tracking UI loads settings — by save\n * time the doc exists and the field-by-field stale check runs.\n */\nexport async function ensureCanonicalDocExists(\n ctx: Pick<PluginContext, 'kv'>,\n doc: TrackingSettingsDocument,\n): Promise<void> {\n const kv = asKvWithCas(ctx.kv);\n const existing = await kv.getRaw(TRACKING_SETTINGS_DOC_KEY);\n if (existing === null) {\n await kv.commitIfValueUnchanged(TRACKING_SETTINGS_DOC_KEY, null, doc);\n }\n}\n\nexport async function mirrorTrackingDocumentToSettingsKeys(\n ctx: Pick<PluginContext, 'kv'>,\n doc: TrackingSettingsDocument,\n): Promise<void> {\n await Promise.all([\n ctx.kv.set('settings:gtmEnabled', doc.gtmEnabled),\n ctx.kv.set('settings:gtmId', doc.gtmId),\n ctx.kv.set('settings:ga4Enabled', doc.ga4Enabled),\n ctx.kv.set('settings:ga4Id', doc.ga4Id),\n ctx.kv.set('settings:metaPixelEnabled', doc.metaEnabled),\n ctx.kv.set('settings:metaPixelId', doc.metaId),\n ctx.kv.set('settings:linkedInEnabled', doc.linkedinEnabled),\n ctx.kv.set('settings:linkedInPartnerId', doc.linkedinId),\n ctx.kv.set('settings:tiktokEnabled', doc.tiktokEnabled),\n ctx.kv.set('settings:tiktokPixelId', doc.tiktokId),\n ctx.kv.set('settings:bingEnabled', doc.bingEnabled),\n ctx.kv.set('settings:bingTagId', doc.bingId),\n ctx.kv.set('settings:pinterestEnabled', doc.pinterestEnabled),\n ctx.kv.set('settings:pinterestTagId', doc.pinterestId),\n ctx.kv.set('settings:nextdoorEnabled', doc.nextdoorEnabled),\n ctx.kv.set('settings:nextdoorPixelId', doc.nextdoorId),\n ctx.kv.set('settings:trackingSettingsRevision', doc.settingsRevision),\n ]);\n}\n\nexport async function saveTrackingSettings(\n ctx: Pick<PluginContext, 'kv'>,\n body: TrackingSaveBody,\n): Promise<\n { ok: true; settingsRevision: number } | { ok: false; conflict: true; settingsRevision: number }\n> {\n const kv = asKvWithCas(ctx.kv);\n const maxAttempts = 32;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const expectedRaw = await kv.getRaw(TRACKING_SETTINGS_DOC_KEY);\n let current: TrackingSettingsDocument;\n if (expectedRaw === null) {\n current = await loadLegacyTrackingDocument(ctx);\n } else {\n // Merge live settings:* values (captures auto-form edits) with the canonical\n // doc's revision (authoritative for CAS). This prevents a stale /tracking\n // save from silently overwriting newer edits made through settingsSchema.\n const canonicalDoc = JSON.parse(expectedRaw) as TrackingSettingsDocument;\n const liveDoc = await loadLegacyTrackingDocument(ctx);\n current = { ...liveDoc, settingsRevision: canonicalDoc.settingsRevision };\n }\n\n if (\n body.settingsRevision !== undefined &&\n body.settingsRevision !== current.settingsRevision\n ) {\n return { ok: false, conflict: true, settingsRevision: current.settingsRevision };\n }\n\n // Detect settings-schema edits that the body would silently overwrite.\n // The settings-schema form writes settings:* without bumping settingsRevision,\n // so the revision check above cannot catch this race. Compare each body field\n // against the live settings:* value; if they diverge and body still carries the\n // stale canonical value, the /tracking snapshot is out of date — reject it.\n if (expectedRaw !== null) {\n const canonicalDoc = JSON.parse(expectedRaw) as TrackingSettingsDocument;\n const wouldClobber = (\n ['gtmEnabled','gtmId','ga4Enabled','ga4Id','metaEnabled','metaId',\n 'linkedinEnabled','linkedinId','tiktokEnabled','tiktokId','bingEnabled','bingId',\n 'pinterestEnabled','pinterestId','nextdoorEnabled','nextdoorId'] as const\n ).some(\n (field) =>\n current[field] !== canonicalDoc[field] &&\n (body as Record<string, unknown>)[field] === (canonicalDoc as Record<string, unknown>)[field],\n );\n if (wouldClobber) {\n return { ok: false, conflict: true, settingsRevision: current.settingsRevision };\n }\n }\n\n const nextDoc: TrackingSettingsDocument = {\n ...current,\n gtmEnabled: body.gtmEnabled,\n gtmId: body.gtmId,\n ga4Enabled: body.ga4Enabled,\n ga4Id: body.ga4Id,\n metaEnabled: body.metaEnabled,\n metaId: body.metaId,\n linkedinEnabled: body.linkedinEnabled,\n linkedinId: body.linkedinId,\n tiktokEnabled: body.tiktokEnabled,\n tiktokId: body.tiktokId,\n bingEnabled: body.bingEnabled,\n bingId: body.bingId,\n pinterestEnabled: body.pinterestEnabled,\n pinterestId: body.pinterestId,\n nextdoorEnabled: body.nextdoorEnabled,\n nextdoorId: body.nextdoorId,\n settingsRevision: current.settingsRevision + 1,\n };\n\n const committed = await kv.commitIfValueUnchanged(\n TRACKING_SETTINGS_DOC_KEY,\n expectedRaw,\n nextDoc,\n );\n if (committed) {\n await mirrorTrackingDocumentToSettingsKeys(ctx, nextDoc);\n return { ok: true, settingsRevision: nextDoc.settingsRevision };\n }\n }\n\n const cur = await loadTrackingSettingsDocument(ctx);\n return { ok: false, conflict: true, settingsRevision: cur.settingsRevision };\n}\n\nexport function documentToApiResponse(doc: TrackingSettingsDocument) {\n return {\n gtmEnabled: doc.gtmEnabled,\n gtmId: doc.gtmId,\n ga4Enabled: doc.ga4Enabled,\n ga4Id: doc.ga4Id,\n metaEnabled: doc.metaEnabled,\n metaId: doc.metaId,\n linkedinEnabled: doc.linkedinEnabled,\n linkedinId: doc.linkedinId,\n tiktokEnabled: doc.tiktokEnabled,\n tiktokId: doc.tiktokId,\n bingEnabled: doc.bingEnabled,\n bingId: doc.bingId,\n pinterestEnabled: doc.pinterestEnabled,\n pinterestId: doc.pinterestId,\n nextdoorEnabled: doc.nextdoorEnabled,\n nextdoorId: doc.nextdoorId,\n settingsRevision: doc.settingsRevision,\n };\n}\n","import { definePlugin } from 'emdash';\nimport type { LicenseData } from './types';\nimport { validateLicense, CACHE_KEY } from './lib/licensing';\nimport { buildPageFragments } from './frontend/injector';\nimport {\n documentToApiResponse,\n ensureCanonicalDocExists,\n loadTrackingSettingsDocument,\n saveTrackingSettings,\n type TrackingSaveBody,\n} from './lib/trackingSettingsDocument';\n\nexport function createPlugin() {\n return definePlugin({\n id: 'roi-insights',\n version: '1.0.0',\n capabilities: ['network:fetch'],\n allowedHosts: ['api.roiknowledge.com'],\n\n admin: {\n entry: '@mosierdata/emdash-plugin-analytics/admin',\n pages: [\n { path: '/dashboard', label: 'Marketing ROI', icon: 'chart' },\n ]\n },\n\n hooks: {\n 'plugin:install': async (_event, ctx) => {\n // Seed schema defaults so page:fragments has values on first render\n // before the user visits the auto-generated settings form.\n await ctx.kv.set('settings:debug', false);\n ctx.log.info('ROI Insights installed');\n },\n\n // Trusted-only hook — injects GTM, md-roi.js, and AvidTrak DNI into\n // every page <head> and <body>. Uses validateLicense (not a raw KV read)\n // so the plugin auto-revalidates after a restart or cache loss without\n // requiring an admin to click Activate. Settings are read fresh from KV\n // on each render so form changes take effect on the next request.\n 'page:fragments': async (_event, ctx) => {\n const license = await validateLicense(ctx);\n if (!license.isValid) return null;\n\n const tracking = await loadTrackingSettingsDocument(ctx);\n const [dniSwapNumber, dniScriptUrl, customHeadCode, customFooterCode, debug] =\n await Promise.all([\n ctx.kv.get<string>('settings:dniSwapNumber'),\n ctx.kv.get<string>('settings:dniScriptUrl'),\n ctx.kv.get<string>('settings:customHeadCode'),\n ctx.kv.get<string>('settings:customFooterCode'),\n ctx.kv.get<boolean>('settings:debug'),\n ]);\n\n return buildPageFragments(license, {\n gtmEnabled: tracking.gtmEnabled,\n gtmId: tracking.gtmId,\n ga4Enabled: tracking.ga4Enabled,\n ga4Id: tracking.ga4Id,\n metaPixelEnabled: tracking.metaEnabled,\n metaPixelId: tracking.metaId,\n linkedInEnabled: tracking.linkedinEnabled,\n linkedInPartnerId: tracking.linkedinId,\n tiktokEnabled: tracking.tiktokEnabled,\n tiktokPixelId: tracking.tiktokId,\n bingEnabled: tracking.bingEnabled,\n bingTagId: tracking.bingId,\n pinterestEnabled: tracking.pinterestEnabled,\n pinterestTagId: tracking.pinterestId,\n nextdoorEnabled: tracking.nextdoorEnabled,\n nextdoorPixelId: tracking.nextdoorId,\n dniSwapNumber: dniSwapNumber ?? '',\n dniScriptUrl: dniScriptUrl ?? '',\n customHeadCode: customHeadCode ?? '',\n customFooterCode: customFooterCode ?? '',\n debug: debug ?? false,\n });\n }\n },\n\n routes: {\n // Returns the current license state. Calls validateLicense so the cache\n // is primed automatically on a cold start rather than returning not_validated.\n 'license/status': {\n handler: async (ctx) => {\n return validateLicense(ctx);\n }\n },\n\n // Forces a fresh API check. Snapshots the current cache first so a\n // transient network outage during revalidation does not disable tracking.\n 'license/validate': {\n handler: async (ctx) => {\n const existing = await ctx.kv.get<LicenseData>(CACHE_KEY);\n\n // Expire the cache so validateLicense skips it and hits the API\n if (existing) {\n await ctx.kv.set(CACHE_KEY, { ...existing, expiresAt: 0 });\n }\n\n const result = await validateLicense(ctx);\n\n // Network failure: restore the prior valid non-fallback cache so\n // page:fragments continues injecting scripts (fail-open preserved)\n if (result.isFallback && existing?.isValid && !existing.isFallback) {\n await ctx.kv.set(CACHE_KEY, existing);\n }\n\n return result;\n }\n },\n\n // Initiates the Google OAuth flow. Returns { authUrl } on success.\n 'google-oauth/initiate': {\n handler: async (ctx) => {\n const licenseKey = await ctx.kv.get<string>('settings:licenseKey');\n if (!licenseKey) {\n return { error: 'License key not saved. Configure it in Settings first.' };\n }\n const domain = new URL(ctx.request.url).origin;\n const httpFetch = ctx.http ? ctx.http.fetch.bind(ctx.http) : fetch;\n const response = await httpFetch(\n 'https://api.roiknowledge.com/api/roi/plugin/oauth/google/initiate',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ license_key: licenseKey, domain })\n }\n );\n if (!response.ok) return { error: 'Backend rejected OAuth initiation.' };\n return response.json();\n }\n },\n\n // Returns all tracking pixel settings mapped to TrackingValues field names.\n 'tracking/settings': {\n handler: async (ctx) => {\n const doc = await loadTrackingSettingsDocument(ctx);\n // Snapshot current state so saveTrackingSettings can detect races\n // with the settings-schema form even on first-ever /tracking load.\n await ensureCanonicalDocExists(ctx, doc);\n return documentToApiResponse(doc);\n }\n },\n\n // Saves tracking pixel settings from the custom UI back to the KV store.\n 'tracking/save': {\n handler: async (ctx) => {\n const body = await ctx.request.json() as TrackingSaveBody;\n return saveTrackingSettings(ctx, body);\n }\n },\n\n // Returns the current Google connection state.\n 'google-oauth/status': {\n handler: async (ctx) => {\n const connected = await ctx.kv.get<boolean>('state:googleConnected') ?? false;\n return { connected };\n }\n },\n\n // Called after a successful OAuth redirect to persist the connected flag.\n 'google-oauth/connected': {\n handler: async (ctx) => {\n await ctx.kv.set('state:googleConnected', true);\n return { connected: true };\n }\n },\n\n // Returns non-sensitive runtime settings for the admin UI.\n // The license key is intentionally excluded — it is write-only from the UI.\n 'settings/load': {\n handler: async (ctx) => {\n const [dniSwapNumber, dniScriptUrl, customHeadCode, customFooterCode, debug] =\n await Promise.all([\n ctx.kv.get<string>('settings:dniSwapNumber'),\n ctx.kv.get<string>('settings:dniScriptUrl'),\n ctx.kv.get<string>('settings:customHeadCode'),\n ctx.kv.get<string>('settings:customFooterCode'),\n ctx.kv.get<boolean>('settings:debug'),\n ]);\n return {\n dniSwapNumber: dniSwapNumber ?? '',\n dniScriptUrl: dniScriptUrl ?? '',\n customHeadCode: customHeadCode ?? '',\n customFooterCode: customFooterCode ?? '',\n debug: debug ?? false,\n };\n }\n },\n\n // Saves admin-configurable runtime settings. Each field is optional so\n // callers can update only what they own (license key vs. advanced settings).\n 'settings/save': {\n handler: async (ctx) => {\n const body = await ctx.request.json() as {\n licenseKey?: string;\n dniSwapNumber?: string;\n dniScriptUrl?: string;\n customHeadCode?: string;\n customFooterCode?: string;\n debug?: boolean;\n };\n await Promise.all([\n typeof body.licenseKey === 'string' && ctx.kv.set('settings:licenseKey', body.licenseKey.trim()),\n typeof body.dniSwapNumber === 'string' && ctx.kv.set('settings:dniSwapNumber', body.dniSwapNumber),\n typeof body.dniScriptUrl === 'string' && ctx.kv.set('settings:dniScriptUrl', body.dniScriptUrl),\n typeof body.customHeadCode === 'string' && ctx.kv.set('settings:customHeadCode', body.customHeadCode),\n typeof body.customFooterCode === 'string'&& ctx.kv.set('settings:customFooterCode',body.customFooterCode),\n typeof body.debug === 'boolean' && ctx.kv.set('settings:debug', body.debug),\n ].filter(Boolean) as Promise<void>[]);\n return { ok: true };\n }\n }\n }\n });\n}\n\nexport default createPlugin;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mosierdata/emdash-plugin-analytics",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Tag Manager, Call Tracking, and Marketing Analytics for EmDash",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,11 +19,9 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
- "patches",
23
- "scripts"
22
+ "patches"
24
23
  ],
25
24
  "scripts": {
26
- "postinstall": "node scripts/postinstall.mjs",
27
25
  "prepack": "npm run build",
28
26
  "build": "tsdown",
29
27
  "dev": "tsdown --watch",
@@ -47,7 +45,7 @@
47
45
  "patch-package": "^8.0.1",
48
46
  "tsdown": "0.20.3",
49
47
  "typescript": "^5.4.0",
50
- "vitest": "^1.6.0"
48
+ "vitest": "^4.1.2"
51
49
  },
52
50
  "peerDependencies": {
53
51
  "@emdash-cms/admin": "*",
@@ -55,5 +53,8 @@
55
53
  "react": "^18.0.0 || ^19.0.0",
56
54
  "react-dom": "^18.0.0 || ^19.0.0"
57
55
  },
58
- "dependencies": {}
56
+ "dependencies": {},
57
+ "overrides": {
58
+ "kysely": "^0.28.14"
59
+ }
59
60
  }
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Conditionally applies the emdash compatibility patch.
4
- *
5
- * The patch adds ctx.kv.getRaw() and ctx.kv.commitIfValueUnchanged() to emdash.
6
- * Once a future emdash release ships these methods natively this script becomes
7
- * a no-op automatically — no consumer action required.
8
- */
9
-
10
- import { readFileSync, readdirSync } from 'fs';
11
- import { resolve, join } from 'path';
12
- import { execSync } from 'child_process';
13
-
14
- const PATCH_METHODS = ['getRaw', 'commitIfValueUnchanged'];
15
- const tag = '[emdash-plugin-analytics]';
16
-
17
- function emdashAlreadyHasMethods() {
18
- try {
19
- const distDir = resolve('node_modules/emdash/dist');
20
- const files = readdirSync(distDir).filter(f => f.endsWith('.mjs'));
21
- for (const file of files) {
22
- const content = readFileSync(join(distDir, file), 'utf8');
23
- if (PATCH_METHODS.every(m => content.includes(m))) return true;
24
- }
25
- } catch {
26
- // emdash not installed or dist missing — proceed to patch attempt
27
- }
28
- return false;
29
- }
30
-
31
- if (emdashAlreadyHasMethods()) {
32
- console.log(`${tag} emdash already has required KV APIs — skipping patch.`);
33
- } else {
34
- console.log(`${tag} Applying emdash compatibility patch…`);
35
- try {
36
- execSync('npx --yes patch-package', { stdio: 'inherit' });
37
- console.log(`${tag} Patch applied successfully.`);
38
- } catch (err) {
39
- console.error(`${tag} Failed to apply patch: ${err.message}`);
40
- console.error(`${tag} Run "npx patch-package" manually in your project root.`);
41
- process.exit(1);
42
- }
43
- }