@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
|
@@ -0,0 +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)}&ev=PageView&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)}&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&tid=${escapeHtml(settings.pinterestTagId)}&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)}&ev=PAGE_VIEW&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, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>');\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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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mosierdata/emdash-plugin-analytics",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tag Manager, Call Tracking, and Marketing Analytics for EmDash",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"import": "./dist/index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./descriptor": {
|
|
12
|
+
"types": "./dist/descriptor.d.mts",
|
|
13
|
+
"import": "./dist/descriptor.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./admin": {
|
|
16
|
+
"types": "./dist/admin.d.mts",
|
|
17
|
+
"import": "./dist/admin.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"patches"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"prepack": "npm run build",
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"dev": "tsdown --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"bundle": "emdash plugin bundle",
|
|
32
|
+
"publish": "emdash plugin publish --build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@testing-library/dom": "^10.4.1",
|
|
36
|
+
"@testing-library/jest-dom": "^6.4.0",
|
|
37
|
+
"@testing-library/react": "^16.0.0",
|
|
38
|
+
"@types/react": "^18.3.0",
|
|
39
|
+
"@types/react-dom": "^18.3.0",
|
|
40
|
+
"emdash": "^0.1.0",
|
|
41
|
+
"jsdom": "^24.0.0",
|
|
42
|
+
"patch-package": "^8.0.1",
|
|
43
|
+
"tsdown": "0.20.3",
|
|
44
|
+
"typescript": "^5.4.0",
|
|
45
|
+
"vitest": "^1.6.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@emdash-cms/admin": "*",
|
|
49
|
+
"emdash": "^0.1.0",
|
|
50
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
51
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {}
|
|
54
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
diff --git a/node_modules/emdash/dist/apply-Bjfq_b4-.mjs b/node_modules/emdash/dist/apply-Bjfq_b4-.mjs
|
|
2
|
+
index 7d6ed8f..225baff 100644
|
|
3
|
+
--- a/node_modules/emdash/dist/apply-Bjfq_b4-.mjs
|
|
4
|
+
+++ b/node_modules/emdash/dist/apply-Bjfq_b4-.mjs
|
|
5
|
+
@@ -178,6 +178,26 @@ var OptionsRepository = class {
|
|
6
|
+
if (!row) return null;
|
|
7
|
+
return JSON.parse(row.value);
|
|
8
|
+
}
|
|
9
|
+
+ async getRaw(name) {
|
|
10
|
+
+ const row = await this.db.selectFrom("options").select("value").where("name", "=", name).executeTakeFirst();
|
|
11
|
+
+ if (!row) return null;
|
|
12
|
+
+ return row.value;
|
|
13
|
+
+ }
|
|
14
|
+
+ async commitIfValueUnchanged(name, expectedRaw, newValue) {
|
|
15
|
+
+ const newSer = JSON.stringify(newValue);
|
|
16
|
+
+ if (expectedRaw === null) {
|
|
17
|
+
+ const cur = await this.getRaw(name);
|
|
18
|
+
+ if (cur !== null) return false;
|
|
19
|
+
+ try {
|
|
20
|
+
+ await this.db.insertInto("options").values({ name, value: newSer }).execute();
|
|
21
|
+
+ return true;
|
|
22
|
+
+ } catch {
|
|
23
|
+
+ return false;
|
|
24
|
+
+ }
|
|
25
|
+
+ }
|
|
26
|
+
+ const result = await this.db.updateTable("options").set({ value: newSer }).where("name", "=", name).where("value", "=", expectedRaw).executeTakeFirst();
|
|
27
|
+
+ return Number(result.numUpdatedRows ?? 0) > 0;
|
|
28
|
+
+ }
|
|
29
|
+
/**
|
|
30
|
+
* Get an option value with a default
|
|
31
|
+
*/
|
|
32
|
+
diff --git a/node_modules/emdash/dist/search-DG603UrT.mjs b/node_modules/emdash/dist/search-DG603UrT.mjs
|
|
33
|
+
index a7d99e7..d682e8b 100644
|
|
34
|
+
--- a/node_modules/emdash/dist/search-DG603UrT.mjs
|
|
35
|
+
+++ b/node_modules/emdash/dist/search-DG603UrT.mjs
|
|
36
|
+
@@ -14,7 +14,7 @@ import { i as pluginManifestSchema } from "./manifest-schema-Dcl0R6nM.mjs";
|
|
37
|
+
import { t as generatePreviewToken } from "./tokens-DpgrkrXK.mjs";
|
|
38
|
+
import { sql } from "kysely";
|
|
39
|
+
import { ulid } from "ulidx";
|
|
40
|
+
-import { z } from "astro/zod";
|
|
41
|
+
+import { z } from "zod";
|
|
42
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
43
|
+
import { z as z$1 } from "zod";
|
|
44
|
+
import { createGzipDecoder, unpackTar } from "modern-tar";
|
|
45
|
+
@@ -5262,6 +5262,12 @@ function createKVAccess(optionsRepo, pluginId) {
|
|
46
|
+
value
|
|
47
|
+
});
|
|
48
|
+
return result;
|
|
49
|
+
+ },
|
|
50
|
+
+ async getRaw(key) {
|
|
51
|
+
+ return optionsRepo.getRaw(`${prefix}${key}`);
|
|
52
|
+
+ },
|
|
53
|
+
+ async commitIfValueUnchanged(key, expectedRaw, newValue) {
|
|
54
|
+
+ return optionsRepo.commitIfValueUnchanged(`${prefix}${key}`, expectedRaw, newValue);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
diff --git a/node_modules/emdash/src/database/repositories/options.ts b/node_modules/emdash/src/database/repositories/options.ts
|
|
59
|
+
index cb7ced2..c62ebe4 100644
|
|
60
|
+
--- a/node_modules/emdash/src/database/repositories/options.ts
|
|
61
|
+
+++ b/node_modules/emdash/src/database/repositories/options.ts
|
|
62
|
+
@@ -26,6 +26,49 @@ export class OptionsRepository {
|
|
63
|
+
return JSON.parse(row.value) as T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
+ /**
|
|
67
|
+
+ * Raw JSON text as stored in the options row (for compare-and-swap).
|
|
68
|
+
+ */
|
|
69
|
+
+ async getRaw(name: string): Promise<string | null> {
|
|
70
|
+
+ const row = await this.db
|
|
71
|
+
+ .selectFrom("options")
|
|
72
|
+
+ .select("value")
|
|
73
|
+
+ .where("name", "=", name)
|
|
74
|
+
+ .executeTakeFirst();
|
|
75
|
+
+
|
|
76
|
+
+ if (!row) return null;
|
|
77
|
+
+ return row.value;
|
|
78
|
+
+ }
|
|
79
|
+
+
|
|
80
|
+
+ /**
|
|
81
|
+
+ * Writes a value only if the row is absent (expectedRaw === null) or the
|
|
82
|
+
+ * stored JSON text still equals expectedRaw. Returns whether the write ran.
|
|
83
|
+
+ */
|
|
84
|
+
+ async commitIfValueUnchanged(
|
|
85
|
+
+ name: string,
|
|
86
|
+
+ expectedRaw: string | null,
|
|
87
|
+
+ newValue: unknown,
|
|
88
|
+
+ ): Promise<boolean> {
|
|
89
|
+
+ const newSer = JSON.stringify(newValue);
|
|
90
|
+
+ if (expectedRaw === null) {
|
|
91
|
+
+ const cur = await this.getRaw(name);
|
|
92
|
+
+ if (cur !== null) return false;
|
|
93
|
+
+ try {
|
|
94
|
+
+ await this.db.insertInto("options").values({ name, value: newSer }).execute();
|
|
95
|
+
+ return true;
|
|
96
|
+
+ } catch {
|
|
97
|
+
+ return false;
|
|
98
|
+
+ }
|
|
99
|
+
+ }
|
|
100
|
+
+ const result = await this.db
|
|
101
|
+
+ .updateTable("options")
|
|
102
|
+
+ .set({ value: newSer })
|
|
103
|
+
+ .where("name", "=", name)
|
|
104
|
+
+ .where("value", "=", expectedRaw)
|
|
105
|
+
+ .executeTakeFirst();
|
|
106
|
+
+ return Number(result.numUpdatedRows ?? 0) > 0;
|
|
107
|
+
+ }
|
|
108
|
+
+
|
|
109
|
+
/**
|
|
110
|
+
* Get an option value with a default
|
|
111
|
+
*/
|
|
112
|
+
diff --git a/node_modules/emdash/src/plugins/context.ts b/node_modules/emdash/src/plugins/context.ts
|
|
113
|
+
index 80927e7..8bb9865 100644
|
|
114
|
+
--- a/node_modules/emdash/src/plugins/context.ts
|
|
115
|
+
+++ b/node_modules/emdash/src/plugins/context.ts
|
|
116
|
+
@@ -79,6 +79,18 @@ export function createKVAccess(optionsRepo: OptionsRepository, pluginId: string)
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
},
|
|
120
|
+
+
|
|
121
|
+
+ async getRaw(key: string): Promise<string | null> {
|
|
122
|
+
+ return optionsRepo.getRaw(`${prefix}${key}`);
|
|
123
|
+
+ },
|
|
124
|
+
+
|
|
125
|
+
+ async commitIfValueUnchanged(
|
|
126
|
+
+ key: string,
|
|
127
|
+
+ expectedRaw: string | null,
|
|
128
|
+
+ newValue: unknown,
|
|
129
|
+
+ ): Promise<boolean> {
|
|
130
|
+
+ return optionsRepo.commitIfValueUnchanged(`${prefix}${key}`, expectedRaw, newValue);
|
|
131
|
+
+ },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
diff --git a/node_modules/emdash/src/plugins/types.ts b/node_modules/emdash/src/plugins/types.ts
|
|
136
|
+
index 76a262d..1a0d888 100644
|
|
137
|
+
--- a/node_modules/emdash/src/plugins/types.ts
|
|
138
|
+
+++ b/node_modules/emdash/src/plugins/types.ts
|
|
139
|
+
@@ -159,6 +159,14 @@ export interface KVAccess {
|
|
140
|
+
set(key: string, value: unknown): Promise<void>;
|
|
141
|
+
delete(key: string): Promise<boolean>;
|
|
142
|
+
list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
|
|
143
|
+
+ /** Raw JSON text for the option row (compare-and-swap). */
|
|
144
|
+
+ getRaw(key: string): Promise<string | null>;
|
|
145
|
+
+ /** Set only if absent (expectedRaw null) or stored JSON text matches. */
|
|
146
|
+
+ commitIfValueUnchanged(
|
|
147
|
+
+ key: string,
|
|
148
|
+
+ expectedRaw: string | null,
|
|
149
|
+
+ newValue: unknown,
|
|
150
|
+
+ ): Promise<boolean>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|