@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/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)}&amp;ev=PageView&amp;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)}&amp;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&amp;tid=${escapeHtml(settings.pinterestTagId)}&amp;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)}&amp;ev=PAGE_VIEW&amp;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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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