@mosierdata/emdash-plugin-analytics 1.0.0
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/LICENSE +21 -0
- package/README.md +298 -0
- package/dist/admin.d.mts +21 -0
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +1545 -0
- package/dist/admin.mjs.map +1 -0
- package/dist/descriptor.d.mts +11 -0
- package/dist/descriptor.d.mts.map +1 -0
- package/dist/descriptor.mjs +35 -0
- package/dist/descriptor.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +683 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/patches/emdash+0.1.0.patch +153 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { definePlugin } from "emdash";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/crypto.ts
|
|
4
|
+
/**
|
|
5
|
+
* Verifies an Ed25519 signature using the Web Crypto API.
|
|
6
|
+
*
|
|
7
|
+
* @param payload - Base64-encoded payload string (as returned by the API)
|
|
8
|
+
* @param signature - Base64-encoded Ed25519 signature
|
|
9
|
+
* @param publicKey - Base64-encoded 32-byte Ed25519 public key (hardcoded in plugin)
|
|
10
|
+
*/
|
|
11
|
+
async function verifyEd25519Signature(payload, signature, publicKey) {
|
|
12
|
+
try {
|
|
13
|
+
const keyBytes = base64ToBytes(publicKey);
|
|
14
|
+
const sigBytes = base64ToBytes(signature);
|
|
15
|
+
const msgBytes = new TextEncoder().encode(payload);
|
|
16
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBytes, { name: "Ed25519" }, false, ["verify"]);
|
|
17
|
+
return await crypto.subtle.verify("Ed25519", cryptoKey, sigBytes, msgBytes);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function base64ToBytes(base64) {
|
|
23
|
+
const binary = atob(base64);
|
|
24
|
+
const bytes = new Uint8Array(binary.length);
|
|
25
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/lib/licensing.ts
|
|
31
|
+
const CACHE_KEY = "state:licenseCache";
|
|
32
|
+
const VALIDATE_URL = "https://api.roiknowledge.com/api/roi/plugin/validate";
|
|
33
|
+
const PUBLIC_KEY = "COwQzXhDeQC9uxAdyNFdbFbIrwLAGgtRZlhfAxbR0Dk=";
|
|
34
|
+
async function validateLicense(ctx) {
|
|
35
|
+
const cached = await ctx.kv.get(CACHE_KEY);
|
|
36
|
+
if (cached && !isCacheExpired(cached)) return cached;
|
|
37
|
+
const licenseKey = (await ctx.kv.get("settings:licenseKey"))?.trim();
|
|
38
|
+
if (!licenseKey) {
|
|
39
|
+
await ctx.kv.delete(CACHE_KEY);
|
|
40
|
+
return {
|
|
41
|
+
isValid: false,
|
|
42
|
+
reason: "missing_key",
|
|
43
|
+
capabilities: []
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const httpFetch = ctx.http ? ctx.http.fetch.bind(ctx.http) : fetch;
|
|
47
|
+
let data;
|
|
48
|
+
try {
|
|
49
|
+
const response = await httpFetch(VALIDATE_URL, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ license_key: licenseKey })
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
await ctx.kv.delete(CACHE_KEY);
|
|
56
|
+
return {
|
|
57
|
+
isValid: false,
|
|
58
|
+
reason: "api_error",
|
|
59
|
+
capabilities: []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
data = await response.json();
|
|
63
|
+
} catch {
|
|
64
|
+
return {
|
|
65
|
+
isValid: true,
|
|
66
|
+
isFallback: true,
|
|
67
|
+
capabilities: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (!await verifyEd25519Signature(data.token.payload, data.token.signature, PUBLIC_KEY)) {
|
|
71
|
+
await ctx.kv.delete(CACHE_KEY);
|
|
72
|
+
return {
|
|
73
|
+
isValid: false,
|
|
74
|
+
reason: "invalid_signature",
|
|
75
|
+
capabilities: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const payload = JSON.parse(atob(data.token.payload));
|
|
79
|
+
if (payload.exp < Math.floor(Date.now() / 1e3)) {
|
|
80
|
+
await ctx.kv.delete(CACHE_KEY);
|
|
81
|
+
return {
|
|
82
|
+
isValid: false,
|
|
83
|
+
reason: "expired",
|
|
84
|
+
capabilities: []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const licenseData = {
|
|
88
|
+
isValid: true,
|
|
89
|
+
tier: payload.tier,
|
|
90
|
+
capabilities: payload.capabilities,
|
|
91
|
+
sessionToken: data.sessionToken,
|
|
92
|
+
expiresAt: payload.exp
|
|
93
|
+
};
|
|
94
|
+
await ctx.kv.set(CACHE_KEY, licenseData);
|
|
95
|
+
return licenseData;
|
|
96
|
+
}
|
|
97
|
+
function isCacheExpired(cached) {
|
|
98
|
+
if (!cached.expiresAt) return false;
|
|
99
|
+
return cached.expiresAt < Math.floor(Date.now() / 1e3);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/frontend/injector.ts
|
|
104
|
+
const MD_ROI_CDN = "https://cdn.roiknowledge.com/assets/md-roi-emdash.js";
|
|
105
|
+
/**
|
|
106
|
+
* Builds the list of PageFragmentContributions for the page:fragments hook.
|
|
107
|
+
* Returns structured contributions — never raw HTML interpolation at the
|
|
108
|
+
* call site — so EmDash can validate, deduplicate, and render them safely.
|
|
109
|
+
*/
|
|
110
|
+
function buildPageFragments(license, settings) {
|
|
111
|
+
const fragments = [];
|
|
112
|
+
if (settings.gtmEnabled && settings.gtmId) {
|
|
113
|
+
fragments.push({
|
|
114
|
+
kind: "inline-script",
|
|
115
|
+
placement: "head",
|
|
116
|
+
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)}');`,
|
|
117
|
+
id: "gtm-script"
|
|
118
|
+
});
|
|
119
|
+
fragments.push({
|
|
120
|
+
kind: "html",
|
|
121
|
+
placement: "body:start",
|
|
122
|
+
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>`,
|
|
123
|
+
id: "gtm-noscript"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (settings.ga4Enabled && settings.ga4Id) {
|
|
127
|
+
fragments.push({
|
|
128
|
+
kind: "external-script",
|
|
129
|
+
placement: "head",
|
|
130
|
+
src: `https://www.googletagmanager.com/gtag/js?id=${escapeJs(settings.ga4Id)}`,
|
|
131
|
+
async: true,
|
|
132
|
+
id: "ga4-script"
|
|
133
|
+
});
|
|
134
|
+
fragments.push({
|
|
135
|
+
kind: "inline-script",
|
|
136
|
+
placement: "head",
|
|
137
|
+
content: `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '${escapeJs(settings.ga4Id)}');`,
|
|
138
|
+
id: "ga4-config"
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (settings.metaPixelEnabled && settings.metaPixelId) {
|
|
142
|
+
fragments.push({
|
|
143
|
+
kind: "inline-script",
|
|
144
|
+
placement: "head",
|
|
145
|
+
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');`,
|
|
146
|
+
id: "meta-pixel-script"
|
|
147
|
+
});
|
|
148
|
+
fragments.push({
|
|
149
|
+
kind: "html",
|
|
150
|
+
placement: "body:start",
|
|
151
|
+
content: `<noscript><img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=${escapeHtml(settings.metaPixelId)}&ev=PageView&noscript=1"/></noscript>`,
|
|
152
|
+
id: "meta-pixel-noscript"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (settings.linkedInEnabled && settings.linkedInPartnerId) {
|
|
156
|
+
fragments.push({
|
|
157
|
+
kind: "inline-script",
|
|
158
|
+
placement: "head",
|
|
159
|
+
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);`,
|
|
160
|
+
id: "linkedin-insight-script"
|
|
161
|
+
});
|
|
162
|
+
fragments.push({
|
|
163
|
+
kind: "html",
|
|
164
|
+
placement: "body:start",
|
|
165
|
+
content: `<noscript><img height="1" width="1" style="display:none;" alt="" src="https://px.ads.linkedin.com/collect/?pid=${escapeHtml(settings.linkedInPartnerId)}&fmt=gif"/></noscript>`,
|
|
166
|
+
id: "linkedin-insight-noscript"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (settings.tiktokEnabled && settings.tiktokPixelId) fragments.push({
|
|
170
|
+
kind: "inline-script",
|
|
171
|
+
placement: "head",
|
|
172
|
+
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');`,
|
|
173
|
+
id: "tiktok-pixel-script"
|
|
174
|
+
});
|
|
175
|
+
if (settings.bingEnabled && settings.bingTagId) fragments.push({
|
|
176
|
+
kind: "inline-script",
|
|
177
|
+
placement: "head",
|
|
178
|
+
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');`,
|
|
179
|
+
id: "bing-uet-script"
|
|
180
|
+
});
|
|
181
|
+
if (settings.pinterestEnabled && settings.pinterestTagId) {
|
|
182
|
+
fragments.push({
|
|
183
|
+
kind: "inline-script",
|
|
184
|
+
placement: "head",
|
|
185
|
+
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');`,
|
|
186
|
+
id: "pinterest-tag-script"
|
|
187
|
+
});
|
|
188
|
+
fragments.push({
|
|
189
|
+
kind: "html",
|
|
190
|
+
placement: "body:start",
|
|
191
|
+
content: `<noscript><img height="1" width="1" style="display:none;" alt="" src="https://ct.pinterest.com/v3/?event=init&tid=${escapeHtml(settings.pinterestTagId)}&noscript=1"/></noscript>`,
|
|
192
|
+
id: "pinterest-tag-noscript"
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (settings.nextdoorEnabled && settings.nextdoorPixelId) {
|
|
196
|
+
fragments.push({
|
|
197
|
+
kind: "inline-script",
|
|
198
|
+
placement: "head",
|
|
199
|
+
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');`,
|
|
200
|
+
id: "nextdoor-pixel-script"
|
|
201
|
+
});
|
|
202
|
+
fragments.push({
|
|
203
|
+
kind: "html",
|
|
204
|
+
placement: "body:start",
|
|
205
|
+
content: `<noscript><img height="1" width="1" style="display:none" src="https://flask.nextdoor.com/pixel?pid=${escapeHtml(settings.nextdoorPixelId)}&ev=PAGE_VIEW&noscript=1"/></noscript>`,
|
|
206
|
+
id: "nextdoor-pixel-noscript"
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
fragments.push({
|
|
210
|
+
kind: "external-script",
|
|
211
|
+
placement: "head",
|
|
212
|
+
src: MD_ROI_CDN,
|
|
213
|
+
defer: true,
|
|
214
|
+
id: "md-roi-script"
|
|
215
|
+
});
|
|
216
|
+
fragments.push({
|
|
217
|
+
kind: "inline-script",
|
|
218
|
+
placement: "head",
|
|
219
|
+
content: `window.md_roii_settings = { gtm_id: '${escapeJs(settings.gtmEnabled ? settings.gtmId : "")}', debug: ${settings.debug} };`,
|
|
220
|
+
id: "md-roi-config"
|
|
221
|
+
});
|
|
222
|
+
if (license.capabilities.includes("call_tracking") && settings.dniScriptUrl) {
|
|
223
|
+
fragments.push({
|
|
224
|
+
kind: "external-script",
|
|
225
|
+
placement: "head",
|
|
226
|
+
src: settings.dniScriptUrl,
|
|
227
|
+
defer: true,
|
|
228
|
+
id: "avidtrak-script"
|
|
229
|
+
});
|
|
230
|
+
fragments.push({
|
|
231
|
+
kind: "inline-script",
|
|
232
|
+
placement: "head",
|
|
233
|
+
content: `window.avidtrak_swap_number = '${escapeJs(settings.dniSwapNumber)}';`,
|
|
234
|
+
id: "avidtrak-config"
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (settings.customHeadCode) fragments.push({
|
|
238
|
+
kind: "html",
|
|
239
|
+
placement: "head",
|
|
240
|
+
content: settings.customHeadCode,
|
|
241
|
+
id: "custom-head"
|
|
242
|
+
});
|
|
243
|
+
if (settings.customFooterCode) fragments.push({
|
|
244
|
+
kind: "html",
|
|
245
|
+
placement: "body:end",
|
|
246
|
+
content: settings.customFooterCode,
|
|
247
|
+
id: "custom-footer"
|
|
248
|
+
});
|
|
249
|
+
return fragments;
|
|
250
|
+
}
|
|
251
|
+
function escapeHtml(value) {
|
|
252
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
253
|
+
}
|
|
254
|
+
function escapeJs(value) {
|
|
255
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/<\/script>/gi, "<\\/script>");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/lib/trackingSettingsDocument.ts
|
|
260
|
+
/** Canonical tracking snapshot + revision (single options row via CAS). */
|
|
261
|
+
const TRACKING_SETTINGS_DOC_KEY = "state:trackingSettingsDoc";
|
|
262
|
+
function asKvWithCas(kv) {
|
|
263
|
+
const k = kv;
|
|
264
|
+
if (typeof k.getRaw !== "function" || typeof k.commitIfValueUnchanged !== "function") throw new Error("KV must implement getRaw/commitIfValueUnchanged for atomic tracking saves. Install with patch-package (patches/emdash+0.1.0.patch).");
|
|
265
|
+
return k;
|
|
266
|
+
}
|
|
267
|
+
async function loadLegacyTrackingDocument(ctx) {
|
|
268
|
+
const [gtmEnabled, gtmId, ga4Enabled, ga4Id, metaPixelEnabled, metaPixelId, linkedInEnabled, linkedInPartnerId, tiktokEnabled, tiktokPixelId, bingEnabled, bingTagId, pinterestEnabled, pinterestTagId, nextdoorEnabled, nextdoorPixelId, settingsRevision] = await Promise.all([
|
|
269
|
+
ctx.kv.get("settings:gtmEnabled"),
|
|
270
|
+
ctx.kv.get("settings:gtmId"),
|
|
271
|
+
ctx.kv.get("settings:ga4Enabled"),
|
|
272
|
+
ctx.kv.get("settings:ga4Id"),
|
|
273
|
+
ctx.kv.get("settings:metaPixelEnabled"),
|
|
274
|
+
ctx.kv.get("settings:metaPixelId"),
|
|
275
|
+
ctx.kv.get("settings:linkedInEnabled"),
|
|
276
|
+
ctx.kv.get("settings:linkedInPartnerId"),
|
|
277
|
+
ctx.kv.get("settings:tiktokEnabled"),
|
|
278
|
+
ctx.kv.get("settings:tiktokPixelId"),
|
|
279
|
+
ctx.kv.get("settings:bingEnabled"),
|
|
280
|
+
ctx.kv.get("settings:bingTagId"),
|
|
281
|
+
ctx.kv.get("settings:pinterestEnabled"),
|
|
282
|
+
ctx.kv.get("settings:pinterestTagId"),
|
|
283
|
+
ctx.kv.get("settings:nextdoorEnabled"),
|
|
284
|
+
ctx.kv.get("settings:nextdoorPixelId"),
|
|
285
|
+
ctx.kv.get("settings:trackingSettingsRevision")
|
|
286
|
+
]);
|
|
287
|
+
return {
|
|
288
|
+
settingsRevision: settingsRevision ?? 0,
|
|
289
|
+
gtmEnabled: gtmEnabled ?? !!gtmId,
|
|
290
|
+
gtmId: gtmId ?? "",
|
|
291
|
+
ga4Enabled: ga4Enabled ?? false,
|
|
292
|
+
ga4Id: ga4Id ?? "",
|
|
293
|
+
metaEnabled: metaPixelEnabled ?? false,
|
|
294
|
+
metaId: metaPixelId ?? "",
|
|
295
|
+
linkedinEnabled: linkedInEnabled ?? false,
|
|
296
|
+
linkedinId: linkedInPartnerId ?? "",
|
|
297
|
+
tiktokEnabled: tiktokEnabled ?? false,
|
|
298
|
+
tiktokId: tiktokPixelId ?? "",
|
|
299
|
+
bingEnabled: bingEnabled ?? false,
|
|
300
|
+
bingId: bingTagId ?? "",
|
|
301
|
+
pinterestEnabled: pinterestEnabled ?? false,
|
|
302
|
+
pinterestId: pinterestTagId ?? "",
|
|
303
|
+
nextdoorEnabled: nextdoorEnabled ?? false,
|
|
304
|
+
nextdoorId: nextdoorPixelId ?? ""
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function loadTrackingSettingsDocument(ctx) {
|
|
308
|
+
return loadLegacyTrackingDocument(ctx);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Ensure the canonical doc exists so saveTrackingSettings always has a snapshot
|
|
312
|
+
* for conflict detection. Called when the /tracking UI loads settings — by save
|
|
313
|
+
* time the doc exists and the field-by-field stale check runs.
|
|
314
|
+
*/
|
|
315
|
+
async function ensureCanonicalDocExists(ctx, doc) {
|
|
316
|
+
const kv = asKvWithCas(ctx.kv);
|
|
317
|
+
if (await kv.getRaw(TRACKING_SETTINGS_DOC_KEY) === null) await kv.commitIfValueUnchanged(TRACKING_SETTINGS_DOC_KEY, null, doc);
|
|
318
|
+
}
|
|
319
|
+
async function mirrorTrackingDocumentToSettingsKeys(ctx, doc) {
|
|
320
|
+
await Promise.all([
|
|
321
|
+
ctx.kv.set("settings:gtmEnabled", doc.gtmEnabled),
|
|
322
|
+
ctx.kv.set("settings:gtmId", doc.gtmId),
|
|
323
|
+
ctx.kv.set("settings:ga4Enabled", doc.ga4Enabled),
|
|
324
|
+
ctx.kv.set("settings:ga4Id", doc.ga4Id),
|
|
325
|
+
ctx.kv.set("settings:metaPixelEnabled", doc.metaEnabled),
|
|
326
|
+
ctx.kv.set("settings:metaPixelId", doc.metaId),
|
|
327
|
+
ctx.kv.set("settings:linkedInEnabled", doc.linkedinEnabled),
|
|
328
|
+
ctx.kv.set("settings:linkedInPartnerId", doc.linkedinId),
|
|
329
|
+
ctx.kv.set("settings:tiktokEnabled", doc.tiktokEnabled),
|
|
330
|
+
ctx.kv.set("settings:tiktokPixelId", doc.tiktokId),
|
|
331
|
+
ctx.kv.set("settings:bingEnabled", doc.bingEnabled),
|
|
332
|
+
ctx.kv.set("settings:bingTagId", doc.bingId),
|
|
333
|
+
ctx.kv.set("settings:pinterestEnabled", doc.pinterestEnabled),
|
|
334
|
+
ctx.kv.set("settings:pinterestTagId", doc.pinterestId),
|
|
335
|
+
ctx.kv.set("settings:nextdoorEnabled", doc.nextdoorEnabled),
|
|
336
|
+
ctx.kv.set("settings:nextdoorPixelId", doc.nextdoorId),
|
|
337
|
+
ctx.kv.set("settings:trackingSettingsRevision", doc.settingsRevision)
|
|
338
|
+
]);
|
|
339
|
+
}
|
|
340
|
+
async function saveTrackingSettings(ctx, body) {
|
|
341
|
+
const kv = asKvWithCas(ctx.kv);
|
|
342
|
+
const maxAttempts = 32;
|
|
343
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
344
|
+
const expectedRaw = await kv.getRaw(TRACKING_SETTINGS_DOC_KEY);
|
|
345
|
+
let current;
|
|
346
|
+
if (expectedRaw === null) current = await loadLegacyTrackingDocument(ctx);
|
|
347
|
+
else {
|
|
348
|
+
const canonicalDoc = JSON.parse(expectedRaw);
|
|
349
|
+
current = {
|
|
350
|
+
...await loadLegacyTrackingDocument(ctx),
|
|
351
|
+
settingsRevision: canonicalDoc.settingsRevision
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (body.settingsRevision !== void 0 && body.settingsRevision !== current.settingsRevision) return {
|
|
355
|
+
ok: false,
|
|
356
|
+
conflict: true,
|
|
357
|
+
settingsRevision: current.settingsRevision
|
|
358
|
+
};
|
|
359
|
+
if (expectedRaw !== null) {
|
|
360
|
+
const canonicalDoc = JSON.parse(expectedRaw);
|
|
361
|
+
if ([
|
|
362
|
+
"gtmEnabled",
|
|
363
|
+
"gtmId",
|
|
364
|
+
"ga4Enabled",
|
|
365
|
+
"ga4Id",
|
|
366
|
+
"metaEnabled",
|
|
367
|
+
"metaId",
|
|
368
|
+
"linkedinEnabled",
|
|
369
|
+
"linkedinId",
|
|
370
|
+
"tiktokEnabled",
|
|
371
|
+
"tiktokId",
|
|
372
|
+
"bingEnabled",
|
|
373
|
+
"bingId",
|
|
374
|
+
"pinterestEnabled",
|
|
375
|
+
"pinterestId",
|
|
376
|
+
"nextdoorEnabled",
|
|
377
|
+
"nextdoorId"
|
|
378
|
+
].some((field) => current[field] !== canonicalDoc[field] && body[field] === canonicalDoc[field])) return {
|
|
379
|
+
ok: false,
|
|
380
|
+
conflict: true,
|
|
381
|
+
settingsRevision: current.settingsRevision
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const nextDoc = {
|
|
385
|
+
...current,
|
|
386
|
+
gtmEnabled: body.gtmEnabled,
|
|
387
|
+
gtmId: body.gtmId,
|
|
388
|
+
ga4Enabled: body.ga4Enabled,
|
|
389
|
+
ga4Id: body.ga4Id,
|
|
390
|
+
metaEnabled: body.metaEnabled,
|
|
391
|
+
metaId: body.metaId,
|
|
392
|
+
linkedinEnabled: body.linkedinEnabled,
|
|
393
|
+
linkedinId: body.linkedinId,
|
|
394
|
+
tiktokEnabled: body.tiktokEnabled,
|
|
395
|
+
tiktokId: body.tiktokId,
|
|
396
|
+
bingEnabled: body.bingEnabled,
|
|
397
|
+
bingId: body.bingId,
|
|
398
|
+
pinterestEnabled: body.pinterestEnabled,
|
|
399
|
+
pinterestId: body.pinterestId,
|
|
400
|
+
nextdoorEnabled: body.nextdoorEnabled,
|
|
401
|
+
nextdoorId: body.nextdoorId,
|
|
402
|
+
settingsRevision: current.settingsRevision + 1
|
|
403
|
+
};
|
|
404
|
+
if (await kv.commitIfValueUnchanged(TRACKING_SETTINGS_DOC_KEY, expectedRaw, nextDoc)) {
|
|
405
|
+
await mirrorTrackingDocumentToSettingsKeys(ctx, nextDoc);
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
settingsRevision: nextDoc.settingsRevision
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
ok: false,
|
|
414
|
+
conflict: true,
|
|
415
|
+
settingsRevision: (await loadTrackingSettingsDocument(ctx)).settingsRevision
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function documentToApiResponse(doc) {
|
|
419
|
+
return {
|
|
420
|
+
gtmEnabled: doc.gtmEnabled,
|
|
421
|
+
gtmId: doc.gtmId,
|
|
422
|
+
ga4Enabled: doc.ga4Enabled,
|
|
423
|
+
ga4Id: doc.ga4Id,
|
|
424
|
+
metaEnabled: doc.metaEnabled,
|
|
425
|
+
metaId: doc.metaId,
|
|
426
|
+
linkedinEnabled: doc.linkedinEnabled,
|
|
427
|
+
linkedinId: doc.linkedinId,
|
|
428
|
+
tiktokEnabled: doc.tiktokEnabled,
|
|
429
|
+
tiktokId: doc.tiktokId,
|
|
430
|
+
bingEnabled: doc.bingEnabled,
|
|
431
|
+
bingId: doc.bingId,
|
|
432
|
+
pinterestEnabled: doc.pinterestEnabled,
|
|
433
|
+
pinterestId: doc.pinterestId,
|
|
434
|
+
nextdoorEnabled: doc.nextdoorEnabled,
|
|
435
|
+
nextdoorId: doc.nextdoorId,
|
|
436
|
+
settingsRevision: doc.settingsRevision
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/index.ts
|
|
442
|
+
function createPlugin() {
|
|
443
|
+
return definePlugin({
|
|
444
|
+
id: "roi-insights",
|
|
445
|
+
version: "1.0.0",
|
|
446
|
+
capabilities: ["network:fetch"],
|
|
447
|
+
allowedHosts: ["api.roiknowledge.com"],
|
|
448
|
+
admin: {
|
|
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
|
+
]
|
|
591
|
+
},
|
|
592
|
+
hooks: {
|
|
593
|
+
"plugin:install": async (_event, ctx) => {
|
|
594
|
+
await ctx.kv.set("settings:debug", false);
|
|
595
|
+
ctx.log.info("ROI Insights installed");
|
|
596
|
+
},
|
|
597
|
+
"page:fragments": async (_event, ctx) => {
|
|
598
|
+
const license = await validateLicense(ctx);
|
|
599
|
+
if (!license.isValid) return null;
|
|
600
|
+
const tracking = await loadTrackingSettingsDocument(ctx);
|
|
601
|
+
const [dniSwapNumber, dniScriptUrl, customHeadCode, customFooterCode, debug] = await Promise.all([
|
|
602
|
+
ctx.kv.get("settings:dniSwapNumber"),
|
|
603
|
+
ctx.kv.get("settings:dniScriptUrl"),
|
|
604
|
+
ctx.kv.get("settings:customHeadCode"),
|
|
605
|
+
ctx.kv.get("settings:customFooterCode"),
|
|
606
|
+
ctx.kv.get("settings:debug")
|
|
607
|
+
]);
|
|
608
|
+
return buildPageFragments(license, {
|
|
609
|
+
gtmEnabled: tracking.gtmEnabled,
|
|
610
|
+
gtmId: tracking.gtmId,
|
|
611
|
+
ga4Enabled: tracking.ga4Enabled,
|
|
612
|
+
ga4Id: tracking.ga4Id,
|
|
613
|
+
metaPixelEnabled: tracking.metaEnabled,
|
|
614
|
+
metaPixelId: tracking.metaId,
|
|
615
|
+
linkedInEnabled: tracking.linkedinEnabled,
|
|
616
|
+
linkedInPartnerId: tracking.linkedinId,
|
|
617
|
+
tiktokEnabled: tracking.tiktokEnabled,
|
|
618
|
+
tiktokPixelId: tracking.tiktokId,
|
|
619
|
+
bingEnabled: tracking.bingEnabled,
|
|
620
|
+
bingTagId: tracking.bingId,
|
|
621
|
+
pinterestEnabled: tracking.pinterestEnabled,
|
|
622
|
+
pinterestTagId: tracking.pinterestId,
|
|
623
|
+
nextdoorEnabled: tracking.nextdoorEnabled,
|
|
624
|
+
nextdoorPixelId: tracking.nextdoorId,
|
|
625
|
+
dniSwapNumber: dniSwapNumber ?? "",
|
|
626
|
+
dniScriptUrl: dniScriptUrl ?? "",
|
|
627
|
+
customHeadCode: customHeadCode ?? "",
|
|
628
|
+
customFooterCode: customFooterCode ?? "",
|
|
629
|
+
debug: debug ?? false
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
routes: {
|
|
634
|
+
"license/status": { handler: async (ctx) => {
|
|
635
|
+
return validateLicense(ctx);
|
|
636
|
+
} },
|
|
637
|
+
"license/validate": { handler: async (ctx) => {
|
|
638
|
+
const existing = await ctx.kv.get(CACHE_KEY);
|
|
639
|
+
if (existing) await ctx.kv.set(CACHE_KEY, {
|
|
640
|
+
...existing,
|
|
641
|
+
expiresAt: 0
|
|
642
|
+
});
|
|
643
|
+
const result = await validateLicense(ctx);
|
|
644
|
+
if (result.isFallback && existing?.isValid && !existing.isFallback) await ctx.kv.set(CACHE_KEY, existing);
|
|
645
|
+
return result;
|
|
646
|
+
} },
|
|
647
|
+
"google-oauth/initiate": { handler: async (ctx) => {
|
|
648
|
+
const licenseKey = await ctx.kv.get("settings:licenseKey");
|
|
649
|
+
if (!licenseKey) return { error: "License key not saved. Configure it in Settings first." };
|
|
650
|
+
const domain = new URL(ctx.request.url).origin;
|
|
651
|
+
const response = await (ctx.http ? ctx.http.fetch.bind(ctx.http) : fetch)("https://api.roiknowledge.com/api/roi/plugin/oauth/google/initiate", {
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: { "Content-Type": "application/json" },
|
|
654
|
+
body: JSON.stringify({
|
|
655
|
+
license_key: licenseKey,
|
|
656
|
+
domain
|
|
657
|
+
})
|
|
658
|
+
});
|
|
659
|
+
if (!response.ok) return { error: "Backend rejected OAuth initiation." };
|
|
660
|
+
return response.json();
|
|
661
|
+
} },
|
|
662
|
+
"tracking/settings": { handler: async (ctx) => {
|
|
663
|
+
const doc = await loadTrackingSettingsDocument(ctx);
|
|
664
|
+
await ensureCanonicalDocExists(ctx, doc);
|
|
665
|
+
return documentToApiResponse(doc);
|
|
666
|
+
} },
|
|
667
|
+
"tracking/save": { handler: async (ctx) => {
|
|
668
|
+
return saveTrackingSettings(ctx, await ctx.request.json());
|
|
669
|
+
} },
|
|
670
|
+
"google-oauth/status": { handler: async (ctx) => {
|
|
671
|
+
return { connected: await ctx.kv.get("state:googleConnected") ?? false };
|
|
672
|
+
} },
|
|
673
|
+
"google-oauth/connected": { handler: async (ctx) => {
|
|
674
|
+
await ctx.kv.set("state:googleConnected", true);
|
|
675
|
+
return { connected: true };
|
|
676
|
+
} }
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
//#endregion
|
|
682
|
+
export { createPlugin, createPlugin as default };
|
|
683
|
+
//# sourceMappingURL=index.mjs.map
|