mcp-scraper 0.89.5 → 0.89.8
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/CHANGELOG.md +65 -15
- package/README.md +5 -5
- package/dist/{analytics-repository-RG7CFK6W.js → analytics-repository-UX6VU22H.js} +1 -1
- package/dist/bin/api-server.js +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-core.js +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.js +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-DBDMD7V7.js → chunk-2BS7MMOS.js} +2 -2
- package/dist/{chunk-NGQYSQQA.js → chunk-2GRE4BAO.js} +1 -1
- package/dist/chunk-2TZBO52D.js +1 -0
- package/dist/chunk-4FROKQJN.js +1 -1
- package/dist/chunk-52XUWR3X.js +820 -0
- package/dist/chunk-5B42QVDC.js +19 -0
- package/dist/chunk-7AC66V3T.js +1 -0
- package/dist/{chunk-7JAMFXNL.js → chunk-7BEWBRMT.js} +153 -86
- package/dist/{chunk-4A5F7IUA.js → chunk-AGK756UE.js} +210 -210
- package/dist/chunk-D6BTAT5Y.js +1 -0
- package/dist/{chunk-53HIX3X7.js → chunk-DKQZNSYM.js} +15 -15
- package/dist/{chunk-MZN4U5BL.js → chunk-E4ME7J6G.js} +1 -1
- package/dist/{chunk-A2CZ7WRV.js → chunk-HR2A2WTL.js} +1 -1
- package/dist/{chunk-XLWNEVUZ.js → chunk-JHMI6HEO.js} +3 -3
- package/dist/{chunk-UB5N2ICD.js → chunk-JSNFZPPB.js} +1 -1
- package/dist/{chunk-WYCXAFID.js → chunk-JVU2THSY.js} +1 -1
- package/dist/{chunk-SEHBXTMX.js → chunk-KX72IORC.js} +6 -6
- package/dist/{chunk-RZSJC7FX.js → chunk-LZTDANNZ.js} +1 -1
- package/dist/{chunk-QPWPR5XG.js → chunk-M5QHXNFZ.js} +3 -3
- package/dist/chunk-MM6QBDPC.js +1 -0
- package/dist/{chunk-3UPA3XHF.js → chunk-PHBU6X5B.js} +1 -1
- package/dist/chunk-Q4A2KIPZ.js +41 -0
- package/dist/{chunk-4SQ4RKDX.js → chunk-TI3KH3YW.js} +1 -1
- package/dist/{chunk-JJDYKM5I.js → chunk-VT6SLBOU.js} +1 -1
- package/dist/{chunk-YQZGZBB4.js → chunk-XG6GCEUE.js} +1 -1
- package/dist/chunk-Z3WXA4ZU.js +1 -0
- package/dist/db-UKQ74OVT.js +1 -0
- package/dist/{extract-bundle-RVOBMGU2.js → extract-bundle-DI7OVLT5.js} +5 -5
- package/dist/gmail-service-ASX7YBC3.js +1 -0
- package/dist/index.cjs +47 -47
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +1 -1
- package/dist/{lead-list-enrichment-repository-GKVCX7BM.js → lead-list-enrichment-repository-GPDHJVM3.js} +1 -1
- package/dist/{location-data-repository-WK27TSHZ.js → location-data-repository-MXUNVDHW.js} +1 -1
- package/dist/{server-7TSXB24M.js → server-PQB2EITA.js} +732 -1551
- package/dist/{site-extract-repository-FAHOTQGX.js → site-extract-repository-WADCAIYB.js} +1 -1
- package/dist/stripe-event-worker-ZPNWXX2T.js +1 -0
- package/dist/worker-GTIKQJIN.js +1 -0
- package/package.json +137 -17
- package/THIRD_PARTY_NOTICES.html +0 -203
- package/dist/chunk-2ICI4TIX.js +0 -59
- package/dist/chunk-3GP5CYZX.js +0 -1
- package/dist/chunk-O2QKRYLS.js +0 -1
- package/dist/chunk-SCHTLEMZ.js +0 -1
- package/dist/db-DV4HHCYP.js +0 -1
- package/dist/gmail-service-FJTBCZPH.js +0 -1
- package/dist/worker-ROG7SO3R.js +0 -1
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import{a as Jt,s as Yt}from"./chunk-5B42QVDC.js";import{T as tn,a as de,b as he,c as lt,d as Qt,e as en,q as f,r as v,sa as G,ta as Ne,v as U,w as J}from"./chunk-HR2A2WTL.js";import{a as Me,e as zt}from"./chunk-2GRE4BAO.js";import{d as Xt,g as Ue,i as Fe,k as ot,m as ct,o as Gt,r as je,t as Zt,u as dt}from"./chunk-D6BTAT5Y.js";import{B as at,F as D,G as Wt,P as Bt,Q as Vt,S as X,U as ce,W as Kt,r as ke,s as Ht,x as qt}from"./chunk-E4ME7J6G.js";import{Ab as Ut,Eb as oe,G as kt,Hb as Ft,Ib as st,Q as $e,Rb as jt,S as Mt,t as ae,ub as M,vb as Pe,xb as nt,yb as it,zb as rt}from"./chunk-7BEWBRMT.js";import{createCipheriv as Ai,createDecipheriv as Ii,randomBytes as Si,scryptSync as hi}from"crypto";import{setScheduleEntitlementTool as Ni}from"mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement";import{createHash as pi,randomUUID as yi}from"crypto";import{exportJWK as gi,importPKCS8 as fi,SignJWT as vi}from"jose";function Ei(e=process.env.OAUTH_ISSUER){let t=e?.trim()||"https://mcpscraper.dev",n=new URL(t);if(n.protocol!=="https:"||n.username||n.password||n.search||n.hash||n.pathname!=="/"&&n.pathname!=="")throw new Error("OAUTH_ISSUER must be an HTTPS origin without credentials, path, query, or fragment");return n.origin}var bi=Ei(),He=null;async function nn(){if(He)return He;let e=process.env.OAUTH_SIGNING_KEY;if(!e?.trim())throw new Error("OAUTH_SIGNING_KEY is not set");let t=e.includes("BEGIN")?e:Buffer.from(e,"base64").toString("utf8"),n=await fi(t,"RS256",{extractable:!0}),i=await gi(n),r={kty:i.kty,n:i.n,e:i.e},s=pi("sha256").update(JSON.stringify({e:r.e,kty:r.kty,n:r.n})).digest("base64url").slice(0,16);return r.kid=s,r.alg="RS256",r.use="sig",He={privateKey:n,publicJwk:r,kid:s},He}async function Is(){return(await nn()).publicJwk}async function rn(e){let{privateKey:t,kid:n}=await nn();return new vi({scope:e.scope,plan:e.plan}).setProtectedHeader({alg:"RS256",kid:n}).setIssuer(bi).setSubject(e.identity).setAudience(e.audience).setIssuedAt().setJti(yi()).setExpirationTime(`${e.ttlSeconds}s`).sign(t)}var Ci=()=>(process.env.MCP_MEMORY_BASE_URL??"https://memory.mcpscraper.dev").replace(/\/$/,""),Ri=()=>(process.env.MCP_MEMORY_RESOURCE_URL??"https://memory.mcpscraper.dev/mcp").replace(/\/$/,""),wi=()=>process.env.MCP_MEMORY_ADMIN_KEY?.trim()??"",Oi=300;function Di(e){let t=e.split(`
|
|
2
|
+
`).find(i=>i.startsWith("data:")),n=t?t.slice(5).trim():e;return JSON.parse(n)}function ut(e){return e?(process.env.MEMORY_OPERATOR_IDENTITIES??"").split(",").map(n=>n.trim().toLowerCase()).filter(Boolean).includes(e.trim().toLowerCase()):!1}function sn(){return hi(Me(),"mcp-memory-key-v1",32)}function Ds(e){let t=Si(12),n=Ai("aes-256-gcm",sn(),t),i=Buffer.concat([n.update(e,"utf8"),n.final()]),r=n.getAuthTag();return`${t.toString("base64")}:${r.toString("base64")}:${i.toString("base64")}`}function Ts(e){try{let[t,n,i]=e.split(":"),r=Ii("aes-256-gcm",sn(),Buffer.from(t,"base64"));return r.setAuthTag(Buffer.from(n,"base64")),Buffer.concat([r.update(Buffer.from(i,"base64")),r.final()]).toString("utf8")}catch{return null}}async function an(e,t,n){try{let i=await fetch(`${Ci()}/mcp`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json, text/event-stream",authorization:`Bearer ${n}`},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"tools/call",params:{name:e,arguments:t}})}),r=await i.text();if(!i.ok)return{ok:!1,error:`memory ${e} failed (${i.status})`};let s=Di(r);if(s.error)return{ok:!1,error:s.error.message??`memory ${e} failed`};if(s.result?.structuredContent)return s.result.structuredContent;let a=s.result?.content?.find(o=>o.type==="text")?.text;if(a)try{return JSON.parse(a)}catch{return{ok:!1,error:a}}return{ok:!1,error:`memory ${e} returned no result`}}catch(i){return{ok:!1,error:i?.message??"memory call failed"}}}function on(e){return e.email}function Ti(e){return on(e)}function xs(e){return ut(e.email)?"unlimited":e.memory_plan==="pro"||e.memory_plan==="team"?e.memory_plan:"free"}async function Ls(e){try{let{key:t}=await cn(e);if(!t)return;(await an("listVaultsTool",{},t)).ok&&await Ft(e.id,!0)}catch{}}function xi(e){return e?!!Wt[e]?.includes_memory:!1}function F(e){return e.memory_plan==="team"?"team":xi(e.subscription_tier)||e.memory_plan==="pro"?"pro":"free"}async function cn(e,t){try{let n=ut(e.email),i=n?"unlimited":t??F(e),r=n?"memory.read memory.write memory.admin":"memory.read memory.write";return{key:await rn({identity:on(e),scope:r,plan:i,audience:Ri(),ttlSeconds:Oi})}}catch(n){return{key:null,error:n instanceof Error?n.message:"memory service authorization unavailable"}}}async function z(e,t){let n=ut(e.email)?"unlimited":t,{key:i,error:r}=await cn(e,n);if(!i)return{ok:!1,error:r??"memory unavailable"};let s=await an("listVaultsTool",{},i);return{ok:s.ok,error:s.error}}async function qe(e){let t=wi();if(!t)return{ok:!1,error:"memory not configured (MCP_MEMORY_ADMIN_KEY missing)"};let n=Ti(e),i=await Ni.execute({apiKey:t,granteeIdentity:n,mcpScraperApiKey:e.api_key},{});return{ok:i.ok,error:i.error}}var dn=["active","trialing","past_due"];function Ce(e){return dn.includes(e)}function ln(e,t){for(let n of dn){let i=e.find(r=>r.status===n&&t(r));if(i)return i}}var Li="price_1TtYPAS8aAcsk3TGHOgrDZiQ";function $i(e=new Date){return`${e.getUTCFullYear()}-${String(e.getUTCMonth()+1).padStart(2,"0")}`}var Pi=new Set(["connected","needs_reauth"]);function ki(e){return Pi.has(String(e.lifecycleStatus??""))}async function Mi(e,t){let n=await ae().execute({sql:"SELECT connection_id FROM connected_account_charges WHERE user_id = ? AND period_start = ?",args:[Number(e),t]});return new Set(n.rows.map(i=>String(i.connection_id)))}async function Ui(e,t,n){await zt();let i=Number(e),r=ae();return await r.batch([{sql:`
|
|
3
|
+
INSERT OR IGNORE INTO connected_account_charge_transfers (
|
|
4
|
+
user_id, period_start, from_connection_id, to_connection_id, provider_config_key
|
|
5
|
+
)
|
|
6
|
+
SELECT ?, ?, previous.id, replacement.id, replacement.provider_config_key
|
|
7
|
+
FROM service_connections AS replacement
|
|
8
|
+
JOIN service_connections AS previous
|
|
9
|
+
ON previous.user_id = replacement.user_id
|
|
10
|
+
AND previous.transport = 'nango'
|
|
11
|
+
AND previous.provider_config_key = replacement.provider_config_key
|
|
12
|
+
AND previous.lifecycle_status = 'disconnected'
|
|
13
|
+
JOIN connected_account_charges AS charge
|
|
14
|
+
ON charge.user_id = previous.user_id
|
|
15
|
+
AND charge.connection_id = previous.id
|
|
16
|
+
AND charge.period_start = ?
|
|
17
|
+
WHERE replacement.id = ?
|
|
18
|
+
AND replacement.user_id = ?
|
|
19
|
+
AND replacement.transport = 'nango'
|
|
20
|
+
AND replacement.lifecycle_status IN ('connected', 'needs_reauth')
|
|
21
|
+
AND NOT EXISTS (
|
|
22
|
+
SELECT 1 FROM connected_account_charges AS existing
|
|
23
|
+
WHERE existing.user_id = replacement.user_id
|
|
24
|
+
AND existing.connection_id = replacement.id
|
|
25
|
+
AND existing.period_start = ?
|
|
26
|
+
)
|
|
27
|
+
ORDER BY previous.updated_at DESC, previous.id ASC
|
|
28
|
+
LIMIT 1
|
|
29
|
+
`,args:[i,n,n,t,i,n]},{sql:`
|
|
30
|
+
UPDATE connected_account_charges
|
|
31
|
+
SET connection_id = ?
|
|
32
|
+
WHERE user_id = ?
|
|
33
|
+
AND period_start = ?
|
|
34
|
+
AND connection_id = (
|
|
35
|
+
SELECT transfer.from_connection_id
|
|
36
|
+
FROM connected_account_charge_transfers AS transfer
|
|
37
|
+
JOIN connected_account_charges AS current_charge
|
|
38
|
+
ON current_charge.user_id = transfer.user_id
|
|
39
|
+
AND current_charge.period_start = transfer.period_start
|
|
40
|
+
AND current_charge.connection_id = transfer.from_connection_id
|
|
41
|
+
WHERE transfer.user_id = ?
|
|
42
|
+
AND transfer.period_start = ?
|
|
43
|
+
AND transfer.to_connection_id = ?
|
|
44
|
+
ORDER BY transfer.transferred_at DESC, transfer.id DESC
|
|
45
|
+
LIMIT 1
|
|
46
|
+
)
|
|
47
|
+
AND NOT EXISTS (
|
|
48
|
+
SELECT 1 FROM connected_account_charges AS existing
|
|
49
|
+
WHERE existing.user_id = ?
|
|
50
|
+
AND existing.connection_id = ?
|
|
51
|
+
AND existing.period_start = ?
|
|
52
|
+
)
|
|
53
|
+
`,args:[t,i,n,i,n,t,i,t,n]}],"write"),(await r.execute({sql:"SELECT 1 FROM connected_account_charges WHERE user_id = ? AND connection_id = ? AND period_start = ? LIMIT 1",args:[i,t,n]})).rows.length>0}async function pt(e){let t=await ae().execute({sql:"SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",args:[Number(e)]});return t.rows[0]?t.rows[0]:null}async function _t(e){await ae().execute({sql:`INSERT INTO connected_account_billing
|
|
54
|
+
(user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, checked_at, synced_at, updated_at)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
|
|
56
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
57
|
+
stripe_subscription_id = excluded.stripe_subscription_id,
|
|
58
|
+
stripe_subscription_item_id = excluded.stripe_subscription_item_id,
|
|
59
|
+
price_id = excluded.price_id,
|
|
60
|
+
quantity = excluded.quantity,
|
|
61
|
+
status = excluded.status,
|
|
62
|
+
last_error_code = excluded.last_error_code,
|
|
63
|
+
checked_at = datetime('now'),
|
|
64
|
+
synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
|
|
65
|
+
updated_at = datetime('now')`,args:[Number(e.userId),e.stripeSubscriptionId,e.stripeSubscriptionItemId,e.priceId,e.quantity,e.status,e.lastErrorCode,e.synced?1:0,e.synced?1:0]});let t=await pt(e.userId);if(!t)throw new Error("connected account billing state was not persisted");return t}var j=class extends Error{constructor(n,i,r=409,s){super(n);this.code=i;this.status=r;this.details=s;this.name="ConnectedAccountBillingError"}code;status;details};function yt(){let e=process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();return e||Li||null}function mt(e,t){let n=Math.max(0,Math.round(e));return{provider:"nango",billingMode:"flat_recurring_credits",priceId:yt(),unitAmountCredits:ke,interval:"month",quantity:n,projectedMonthlyCredits:n*ke,status:t?.status??"pending",lastErrorCode:t?.last_error_code??null,syncedAt:t?.synced_at??null,usage:qt.usage}}function le(e){return e.items.data.find(t=>!!t.price?.id&&t.price.id in D)}function Hs(e){return ln(e,t=>!!le(t))}function Fi(e){return e instanceof j?e.code:"stripe_connection_billing_failed"}async function We(e,t,n){await _t({userId:e.id,stripeSubscriptionId:e.subscription_id,stripeSubscriptionItemId:null,priceId:yt(),quantity:t,status:n instanceof j&&n.code!=="stripe_connection_billing_failed"?"blocked":"error",lastErrorCode:Fi(n),synced:!1})}async function qs(e,t){let n=[...new Set(t.filter(ki).map(c=>c.connectionId))],i=n.length,r=yt();if(!r){let c=new j("Connected-account billing is not configured.","connected_account_price_not_configured",503);throw await We(e,i,c),c}if(!e.subscription_id||!(e.subscription_tier??"")||!Object.values(D).some(c=>c.tier===e.subscription_tier)){if(i===0){let d=await _t({userId:e.id,stripeSubscriptionId:null,stripeSubscriptionItemId:null,priceId:r,quantity:0,status:"synced",lastErrorCode:null,synced:!0});return mt(0,d)}let c=new j("An active paid plan is required before connected accounts can be billed.","paid_plan_required",403);throw await We(e,i,c),c}let s=await pt(e.id),a=Math.max(0,s?.quantity??0),o=$i();try{let c=await Mi(e.id,o),d=n.filter(u=>!c.has(u));for(let u of d){if(await Ui(e.id,u,o))continue;let y=Ht,E=await jt(e.id,y,Kt.CONNECTED_ACCOUNT_FEE,`1 connected account at ${ke} credits/month (${o})`,`connection-fee:${u}:${o}`);if(!E.ok){let b=new j(`Insufficient credits to connect. Balance: ${X(E.balance_mc)} credits. Connecting requires ${X(y)} credits (${X(y-E.balance_mc)} short). Upgrade your plan or wait for your credits to refresh.`,"insufficient_credits",402,{balanceCredits:X(E.balance_mc),requiredCredits:X(y),shortByCredits:X(Math.max(0,y-E.balance_mc))});throw await We(e,a,b),b}await ae().execute({sql:`INSERT INTO connected_account_charges (user_id, connection_id, period_start, amount_mc)
|
|
66
|
+
VALUES (?, ?, ?, ?) ON CONFLICT(user_id, connection_id, period_start) DO NOTHING`,args:[Number(e.id),u,o,y]}),E.duplicate||await Jt(e.id,1)}let _=await _t({userId:e.id,stripeSubscriptionId:e.subscription_id,stripeSubscriptionItemId:null,priceId:r,quantity:i,status:"synced",lastErrorCode:null,synced:!0});return mt(i,_)}catch(c){throw c instanceof j?c:(await We(e,a,c),new j("Unable to update connected-account billing.","stripe_connection_billing_failed",503))}}async function Ws(e,t){return mt(t,await pt(e))}import{createHash as Yi}from"crypto";import{z as q}from"zod";import{createHash as ji,randomUUID as Z}from"crypto";function I(e){return e??v()}function Be(e){return ji("sha256").update(JSON.stringify(e)).digest("hex")}async function Q(e){let t=await I(e.db).query("SELECT id::text AS id FROM analytics_connections WHERE id::text=$1 AND site_id=$2",[e.connectionId,e.siteId]);if(!t.rowCount)throw new f("analytics_crm_resource_not_found","CRM resource was not found.",404);return t.rows[0].id}async function zs(e){let t=Xt.parse(e.capability),n=I(e.db),i=await Q({siteId:e.siteId,connectionId:e.connectionId,db:n});await n.query(`INSERT INTO analytics_crm_connection_capabilities(
|
|
67
|
+
site_id,connection_id,provider,contract_version,mode,schema_fingerprint,allowed_operations,evidence,verified_at,updated_at
|
|
68
|
+
) VALUES($1,$2,$3,1,$4,$5,$6::jsonb,$7::jsonb,$8,now())
|
|
69
|
+
ON CONFLICT(connection_id,contract_version) DO UPDATE SET
|
|
70
|
+
mode=EXCLUDED.mode,schema_fingerprint=EXCLUDED.schema_fingerprint,
|
|
71
|
+
allowed_operations=EXCLUDED.allowed_operations,evidence=EXCLUDED.evidence,
|
|
72
|
+
verified_at=EXCLUDED.verified_at,updated_at=now()`,[e.siteId,i,t.provider,t.mode,t.evidence.tenantSchemaFingerprint,JSON.stringify([...new Set(e.allowedOperations)].sort()),JSON.stringify(t.evidence),t.evidence.evaluatedAt])}async function Ys(e){let t=Ue.parse(e.plan),n=I(e.db),i=await Q({siteId:e.siteId,connectionId:t.connectionId,db:n}),r=await n.query("SELECT plan,request_fingerprint FROM analytics_crm_provisioning_plans WHERE site_id=$1 AND public_plan_id=$2",[e.siteId,t.planId]);if(r.rowCount){if(r.rows[0].request_fingerprint!==t.requestFingerprint)throw new f("idempotency_conflict","This plan identifier was already used for different arguments.",409);return{plan:Ue.parse(r.rows[0].plan),reused:!0}}return await n.query(`INSERT INTO analytics_crm_provisioning_plans(
|
|
73
|
+
id,site_id,connection_id,provider,contract_version,tenant_schema_version,plan,status,
|
|
74
|
+
created_by_user_id,public_plan_id,schema_fingerprint,request_fingerprint,created_at,updated_at
|
|
75
|
+
) VALUES($1,$2,$3,$4,1,$5,$6::jsonb,'preview',$7,$8,$9,$10,$11,$11)`,[Z(),e.siteId,i,t.provider,t.schemaFingerprint,JSON.stringify(t),e.createdByUserId,t.planId,t.schemaFingerprint,t.requestFingerprint,t.createdAt]),{plan:t,reused:!1}}async function Xs(e){let t=await I(e.db).query("SELECT plan FROM analytics_crm_provisioning_plans WHERE site_id=$1 AND public_plan_id=$2",[e.siteId,e.planId]);if(!t.rowCount)throw new f("analytics_crm_resource_not_found","CRM resource was not found.",404);return Ue.parse(t.rows[0].plan)}async function Gs(e){let t=Fe.parse(e.receipt),n=I(e.db),i=await n.query("SELECT id::text AS id,request_fingerprint FROM analytics_crm_provisioning_plans WHERE site_id=$1 AND public_plan_id=$2",[e.siteId,t.planId]);if(!i.rowCount)throw new f("analytics_crm_resource_not_found","CRM resource was not found.",404);if(i.rows[0].request_fingerprint!==t.planFingerprint)throw new f("idempotency_conflict","The approved plan no longer matches the persisted plan.",409);let r=await n.query(`SELECT r.receipt FROM analytics_crm_provisioning_receipts r
|
|
76
|
+
JOIN analytics_crm_provisioning_plans p ON p.id=r.plan_id
|
|
77
|
+
WHERE p.site_id=$1 AND r.public_receipt_id=$2`,[e.siteId,t.receiptId]);if(r.rowCount&&Fe.parse(r.rows[0].receipt).planFingerprint!==t.planFingerprint)throw new f("idempotency_conflict","This receipt identifier was already used for different arguments.",409);let s=t.status==="tested"?"test_write":"create_field",a=t.status==="failed"?"failed":"accepted";return await n.query(`INSERT INTO analytics_crm_provisioning_receipts(
|
|
78
|
+
id,plan_id,operation,status,safe_error_code,warnings,created_at,public_receipt_id,receipt
|
|
79
|
+
) VALUES($1,$2,$3,$4,$5,'[]'::jsonb,$6,$7,$8::jsonb)
|
|
80
|
+
ON CONFLICT (public_receipt_id) WHERE public_receipt_id IS NOT NULL
|
|
81
|
+
DO UPDATE SET operation=EXCLUDED.operation,status=EXCLUDED.status,safe_error_code=EXCLUDED.safe_error_code,receipt=EXCLUDED.receipt`,[Z(),i.rows[0].id,s,a,t.errorCode??null,t.occurredAt,t.receiptId,JSON.stringify(t)]),await n.query(`UPDATE analytics_crm_provisioning_plans
|
|
82
|
+
SET status=$3,approved_by_user_id=$4,approved_at=COALESCE(approved_at,$5),
|
|
83
|
+
approved_field_ids=$6::jsonb,updated_at=$5
|
|
84
|
+
WHERE site_id=$1 AND public_plan_id=$2`,[e.siteId,t.planId,t.status==="tested"?"verified":t.status==="failed"?"failed":"approved",e.approvedByUserId,t.occurredAt,JSON.stringify([...new Set(e.approvedFieldInternalNames)].sort())]),t}async function Zs(e){let t=await I(e.db).query(`SELECT r.receipt FROM analytics_crm_provisioning_receipts r
|
|
85
|
+
JOIN analytics_crm_provisioning_plans p ON p.id=r.plan_id
|
|
86
|
+
WHERE p.site_id=$1 AND r.public_receipt_id=$2`,[e.siteId,e.receiptId]);if(!t.rowCount)throw new f("analytics_crm_resource_not_found","CRM resource was not found.",404);return Fe.parse(t.rows[0].receipt)}var gt=e=>e instanceof Date?e.toISOString():e;function ft(e){return Gt.parse({syncId:e.sync_id,siteId:e.site_id,connectionId:e.connection_id,provider:e.provider,state:e.state,mode:e.mode,cursor:typeof e.cursor=="string"?e.cursor:e.cursor==null?null:JSON.stringify(e.cursor),lastStartedAt:gt(e.last_started_at),lastSuccessAt:gt(e.last_success_at),nextRunAt:gt(e.next_run_at),imported:Number(e.imported),deduplicated:Number(e.deduplicated),rejected:Number(e.rejected),lastErrorCode:e.last_error_code,requestFingerprint:e.request_fingerprint})}async function Qs(e){let t=I(e.db),n=await Q({siteId:e.siteId,connectionId:e.connectionId,db:t}),i=Be({siteId:e.siteId,connectionId:n,provider:e.provider,mode:e.mode,mapping:e.mapping,mappingVersion:e.mappingVersion??1,capabilityEvidence:e.capabilityEvidence??{},backfillFrom:e.backfillFrom??null}),r=await t.query("SELECT * FROM analytics_crm_sync_runs WHERE site_id=$1 AND idempotency_key=$2",[e.siteId,e.idempotencyKey]);if(r.rowCount){if(r.rows[0].request_fingerprint!==i)throw new f("idempotency_conflict","This Idempotency-Key was already used for different CRM sync arguments.",409);return{reused:!0,status:ft(r.rows[0])}}let s=await t.query(`INSERT INTO analytics_crm_sync_runs(
|
|
87
|
+
sync_id,site_id,connection_id,provider,mode,state,mapping,mapping_version,capability_evidence,idempotency_key,request_fingerprint,last_started_at
|
|
88
|
+
) VALUES($1,$2,$3,$4,$5,'queued',$6::jsonb,$7,$8::jsonb,$9,$10,now()) RETURNING *`,[e.syncId,e.siteId,n,e.provider,e.mode,JSON.stringify(e.mapping),e.mappingVersion??1,JSON.stringify(e.capabilityEvidence??{}),e.idempotencyKey,i]);return await t.query(`INSERT INTO analytics_crm_sync_state(connection_id,status,last_started_at,updated_at)
|
|
89
|
+
VALUES($1,'pending',now(),now()) ON CONFLICT(connection_id)
|
|
90
|
+
DO UPDATE SET status='pending',last_started_at=now(),updated_at=now()`,[n]),{reused:!1,status:ft(s.rows[0])}}async function ea(e){let t=await I(e.db).query("SELECT * FROM analytics_crm_sync_runs WHERE site_id=$1 AND sync_id=$2",[e.siteId,e.syncId]);return t.rowCount?ft(t.rows[0]):null}async function ta(e){let t=I(e.db);return(await t.query(`UPDATE analytics_crm_sync_runs SET next_run_at=$4,interval_seconds=$5,updated_at=now()
|
|
91
|
+
WHERE site_id=$1 AND sync_id=$2 AND connection_id::text=$3 AND state <> 'cancelled'`,[e.siteId,e.syncId,e.connectionId,e.nextRunAt,e.intervalSeconds])).rowCount?(e.intervalSeconds===null?await t.query(`DELETE FROM analytics_reconciliation_jobs
|
|
92
|
+
WHERE connection_id::text=$1 AND status='pending'`,[e.connectionId]):await t.query(`INSERT INTO analytics_reconciliation_jobs(id,connection_id,status,next_attempt_at)
|
|
93
|
+
VALUES($1,$2,'pending',$3)
|
|
94
|
+
ON CONFLICT(connection_id) DO UPDATE SET
|
|
95
|
+
status=CASE WHEN analytics_reconciliation_jobs.status='leased' THEN 'leased' ELSE 'pending' END,
|
|
96
|
+
next_attempt_at=CASE WHEN analytics_reconciliation_jobs.status='leased'
|
|
97
|
+
THEN analytics_reconciliation_jobs.next_attempt_at ELSE EXCLUDED.next_attempt_at END,
|
|
98
|
+
updated_at=now()`,[Z(),e.connectionId,e.nextRunAt]),!0):!1}async function na(e){let t=I(e.db),n=await t.query(`UPDATE analytics_crm_sync_runs SET state='cancelled',next_run_at=NULL,updated_at=now()
|
|
99
|
+
WHERE site_id=$1 AND sync_id=$2 AND state <> 'cancelled' RETURNING connection_id::text AS connection_id`,[e.siteId,e.syncId]);return n.rowCount?(await t.query("UPDATE analytics_crm_sync_state SET status='disabled',next_sync_at=NULL,updated_at=now() WHERE connection_id::text=$1",[n.rows[0].connection_id]),!0):!1}async function ia(e){let t=I(e.db);await Q({siteId:e.siteId,connectionId:e.connectionId,db:t}),await t.query(`INSERT INTO analytics_crm_sync_state(connection_id,direction,status,last_started_at,updated_at)
|
|
100
|
+
VALUES($1,'inbound','running',now(),now())
|
|
101
|
+
ON CONFLICT(connection_id) DO UPDATE SET
|
|
102
|
+
direction=CASE WHEN analytics_crm_sync_state.direction='outbound' THEN 'bidirectional' ELSE analytics_crm_sync_state.direction END,
|
|
103
|
+
status='running',last_started_at=now(),last_error_code=NULL,updated_at=now()`,[e.connectionId]),await t.query(`UPDATE analytics_crm_sync_runs SET state='running',last_started_at=now(),last_error_code=NULL,updated_at=now()
|
|
104
|
+
WHERE sync_id=(SELECT sync_id FROM analytics_crm_sync_runs
|
|
105
|
+
WHERE site_id=$1 AND connection_id::text=$2 AND state NOT IN ('cancelled','webhook_only')
|
|
106
|
+
ORDER BY created_at DESC LIMIT 1)`,[e.siteId,e.connectionId])}async function ra(e){let t=I(e.db);await Q({siteId:e.siteId,connectionId:e.connectionId,db:t}),await t.query(`INSERT INTO analytics_crm_sync_state(
|
|
107
|
+
connection_id,cursor,direction,status,last_started_at,last_completed_at,last_success_at,next_sync_at,updated_at
|
|
108
|
+
) VALUES($1,$2::jsonb,'inbound','healthy',now(),now(),now(),$3,now())
|
|
109
|
+
ON CONFLICT(connection_id) DO UPDATE SET
|
|
110
|
+
cursor=EXCLUDED.cursor,
|
|
111
|
+
direction=CASE WHEN analytics_crm_sync_state.direction='outbound' THEN 'bidirectional' ELSE analytics_crm_sync_state.direction END,
|
|
112
|
+
status='healthy',last_completed_at=now(),last_success_at=now(),last_error_code=NULL,
|
|
113
|
+
next_sync_at=EXCLUDED.next_sync_at,updated_at=now()`,[e.connectionId,JSON.stringify(e.cursor??null),e.nextRunAt]),await t.query(`UPDATE analytics_crm_sync_runs SET
|
|
114
|
+
state=$3,cursor=$4::jsonb,last_success_at=now(),next_run_at=$5,
|
|
115
|
+
imported=imported+$6,deduplicated=deduplicated+$7,rejected=rejected+$8,
|
|
116
|
+
last_error_code=NULL,updated_at=now()
|
|
117
|
+
WHERE sync_id=(SELECT sync_id FROM analytics_crm_sync_runs
|
|
118
|
+
WHERE site_id=$1 AND connection_id::text=$2 AND state <> 'cancelled'
|
|
119
|
+
ORDER BY created_at DESC LIMIT 1)`,[e.siteId,e.connectionId,e.hasMore?"queued":"completed",JSON.stringify(e.cursor??null),e.nextRunAt,e.imported,e.deduplicated,e.rejected]),await t.query(`INSERT INTO analytics_crm_sync_receipts(
|
|
120
|
+
id,site_id,connection_id,direction,operation,subject_ref,external_receipt_id,status,occurred_at
|
|
121
|
+
) VALUES($1,$2,$3,'inbound','historical_pull',NULL,$4,'accepted',now())`,[Z(),e.siteId,e.connectionId,e.receiptFingerprint])}async function sa(e){let t=I(e.db);await Q({siteId:e.siteId,connectionId:e.connectionId,db:t});let n=e.terminal?"failed":"retrying";await t.query(`INSERT INTO analytics_crm_sync_state(connection_id,direction,status,last_completed_at,last_error_code,next_sync_at,updated_at)
|
|
122
|
+
VALUES($1,'inbound','degraded',now(),$2,$3,now())
|
|
123
|
+
ON CONFLICT(connection_id) DO UPDATE SET
|
|
124
|
+
direction=CASE WHEN analytics_crm_sync_state.direction='outbound' THEN 'bidirectional' ELSE analytics_crm_sync_state.direction END,
|
|
125
|
+
status='degraded',last_completed_at=now(),last_error_code=EXCLUDED.last_error_code,
|
|
126
|
+
next_sync_at=EXCLUDED.next_sync_at,updated_at=now()`,[e.connectionId,e.safeErrorCode,e.nextRunAt]),await t.query(`UPDATE analytics_crm_sync_runs SET state='degraded',last_error_code=$3,next_run_at=$4,
|
|
127
|
+
rejected=rejected+1,updated_at=now()
|
|
128
|
+
WHERE sync_id=(SELECT sync_id FROM analytics_crm_sync_runs
|
|
129
|
+
WHERE site_id=$1 AND connection_id::text=$2 AND state <> 'cancelled'
|
|
130
|
+
ORDER BY created_at DESC LIMIT 1)`,[e.siteId,e.connectionId,e.safeErrorCode,e.nextRunAt]),await t.query(`INSERT INTO analytics_crm_sync_receipts(
|
|
131
|
+
id,site_id,connection_id,direction,operation,subject_ref,external_receipt_id,status,safe_error_code,occurred_at
|
|
132
|
+
) VALUES($1,$2,$3,'inbound','historical_pull',NULL,$4,$5,$6,now())`,[Z(),e.siteId,e.connectionId,e.receiptFingerprint,n,e.safeErrorCode])}function un(e){return je.parse({policyId:e.policy_id,siteId:e.site_id,provider:e.provider,connectionId:e.connection_id,kind:e.kind,enabled:e.enabled,version:Number(e.version),provisioningReceiptId:e.provisioning_receipt_id,schemaFingerprint:e.schema_fingerprint,mappingVersion:e.mapping_version==null?null:Number(e.mapping_version),mapping:e.mapping??null,allowedFields:e.allowed_fields,selectedEvents:e.selected_events,purpose:e.purpose,updatedAt:e.updated_at instanceof Date?e.updated_at.toISOString():new Date(e.updated_at).toISOString()})}async function aa(e){let t=je.parse(e.policy),n=I(e.db),i=await Q({siteId:t.siteId,connectionId:t.connectionId,db:n});if(t.enabled){let a=(await n.query(`SELECT r.receipt->>'status' receipt_status,p.status plan_status,p.schema_fingerprint plan_schema_fingerprint,
|
|
133
|
+
c.schema_fingerprint current_schema_fingerprint
|
|
134
|
+
FROM analytics_crm_provisioning_receipts r
|
|
135
|
+
JOIN analytics_crm_provisioning_plans p ON p.id=r.plan_id
|
|
136
|
+
LEFT JOIN analytics_crm_connection_capabilities c ON c.connection_id=p.connection_id AND c.contract_version=1
|
|
137
|
+
WHERE p.site_id=$1 AND p.connection_id=$2 AND r.public_receipt_id=$3`,[t.siteId,i,t.provisioningReceiptId])).rows[0];if(!a||a.receipt_status!=="tested"||a.plan_status!=="verified")throw new f("analytics_crm_outbound_test_required","A successful provisioning test is required before outbound automation can be enabled.",409);if(a.plan_schema_fingerprint!==t.schemaFingerprint||a.current_schema_fingerprint!==t.schemaFingerprint)throw new f("analytics_crm_schema_changed","The CRM schema changed after testing. Test the exact schema again before enabling outbound automation.",409)}if(t.kind==="pipeline_event"&&(!t.mapping||t.mapping.version!==t.mappingVersion))throw new f("analytics_crm_mapping_version_mismatch","The pipeline mapping and mapping version must match exactly.",409);let r=await n.query(`INSERT INTO analytics_crm_outbound_policies(
|
|
138
|
+
policy_id,site_id,connection_id,provider,kind,enabled,version,provisioning_receipt_id,
|
|
139
|
+
schema_fingerprint,mapping_version,mapping,allowed_fields,selected_events,purpose,created_by_user_id,updated_at
|
|
140
|
+
) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12::jsonb,$13::jsonb,$14,$15,now())
|
|
141
|
+
ON CONFLICT(policy_id) DO UPDATE SET
|
|
142
|
+
enabled=EXCLUDED.enabled,provisioning_receipt_id=EXCLUDED.provisioning_receipt_id,
|
|
143
|
+
schema_fingerprint=EXCLUDED.schema_fingerprint,mapping_version=EXCLUDED.mapping_version,
|
|
144
|
+
mapping=EXCLUDED.mapping,allowed_fields=EXCLUDED.allowed_fields,selected_events=EXCLUDED.selected_events,
|
|
145
|
+
purpose=EXCLUDED.purpose,updated_at=now()
|
|
146
|
+
WHERE analytics_crm_outbound_policies.site_id=EXCLUDED.site_id
|
|
147
|
+
AND analytics_crm_outbound_policies.connection_id=EXCLUDED.connection_id
|
|
148
|
+
AND analytics_crm_outbound_policies.kind=EXCLUDED.kind
|
|
149
|
+
AND analytics_crm_outbound_policies.version=EXCLUDED.version
|
|
150
|
+
RETURNING *`,[t.policyId,t.siteId,i,t.provider,t.kind,t.enabled,t.version,t.provisioningReceiptId,t.schemaFingerprint,t.mappingVersion,JSON.stringify(t.mapping),JSON.stringify([...new Set(t.allowedFields)].sort()),JSON.stringify([...new Set(t.selectedEvents)].sort()),t.purpose,e.createdByUserId]);if(!r.rowCount)throw new f("idempotency_conflict","This policy identifier is already bound to another immutable policy version.",409);return un(r.rows[0])}async function _n(e){return(await I(e.db).query("SELECT * FROM analytics_crm_outbound_policies WHERE site_id=$1 ORDER BY updated_at DESC,policy_id",[e.siteId])).rows.map(un)}function vt(e){return Zt.parse({jobId:e.job_id,policyId:e.policy_id,siteId:e.site_id,personId:e.person_id,kind:e.kind,state:e.state,policyVersion:Number(e.policy_version),mappingVersion:e.mapping_version==null?null:Number(e.mapping_version),attempts:Number(e.attempts),nextAttemptAt:e.next_attempt_at==null?null:e.next_attempt_at instanceof Date?e.next_attempt_at.toISOString():e.next_attempt_at,safeErrorCode:e.safe_error_code,requestFingerprint:e.request_fingerprint})}async function mn(e){let t=je.parse(e.policy);if(!t.enabled)throw new f("analytics_crm_outbound_policy_disabled","The CRM outbound policy is disabled.",409);let n=I(e.db);if(!(await n.query(`SELECT p.id::text id FROM analytics_people p
|
|
151
|
+
WHERE p.id=$1 AND p.site_id=$2 AND
|
|
152
|
+
(p.crm_person_ref IS NOT NULL OR EXISTS (
|
|
153
|
+
SELECT 1 FROM analytics_identity_edges e JOIN analytics_identity_nodes n ON n.id=e.identity_node_id
|
|
154
|
+
WHERE e.person_id=p.id AND n.kind IN ('email','phone','customer_id') AND e.confidence >= 0.99))`,[e.personId,t.siteId])).rowCount)throw new f("analytics_crm_outbound_confirmed_person_required","CRM outbound delivery requires a confirmed person.",409);let r=await n.query("SELECT * FROM analytics_crm_outbound_jobs WHERE site_id=$1 AND idempotency_key=$2",[t.siteId,e.idempotencyKey]);if(r.rowCount){if(r.rows[0].request_fingerprint!==e.requestFingerprint)throw new f("idempotency_conflict","This idempotency key was already used for another CRM outbound request.",409);return{job:vt(r.rows[0]),reused:!0}}let s=await n.query(`INSERT INTO analytics_crm_outbound_jobs(
|
|
155
|
+
job_id,policy_id,site_id,person_id,kind,policy_version,mapping_version,source_event_id,
|
|
156
|
+
payload_ciphertext,idempotency_key,request_fingerprint,next_attempt_at
|
|
157
|
+
) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,now()) RETURNING *`,[e.jobId,t.policyId,t.siteId,e.personId,t.kind,t.version,t.mappingVersion,e.sourceEventId??null,e.payloadCiphertext,e.idempotencyKey,e.requestFingerprint]);return{job:vt(s.rows[0]),reused:!1}}async function pn(e){let t=await I(e.db).query(`WITH due AS (
|
|
158
|
+
SELECT j.job_id FROM analytics_crm_outbound_jobs j
|
|
159
|
+
JOIN analytics_crm_outbound_policies p ON p.policy_id=j.policy_id
|
|
160
|
+
WHERE p.enabled=true AND p.provider NOT IN ('salesforce','pipedrive')
|
|
161
|
+
AND ((j.state IN ('pending','retrying') AND j.next_attempt_at <= now()) OR
|
|
162
|
+
(j.state='running' AND j.lease_expires_at < now()))
|
|
163
|
+
ORDER BY coalesce(j.next_attempt_at,j.lease_expires_at),j.created_at FOR UPDATE OF j SKIP LOCKED LIMIT $1
|
|
164
|
+
)
|
|
165
|
+
UPDATE analytics_crm_outbound_jobs j SET state='running',attempts=j.attempts+1,
|
|
166
|
+
lease_expires_at=now()+($2::text || ' seconds')::interval,updated_at=now()
|
|
167
|
+
FROM due,analytics_crm_outbound_policies p,analytics_sites s
|
|
168
|
+
WHERE j.job_id=due.job_id AND p.policy_id=j.policy_id AND s.id=j.site_id
|
|
169
|
+
RETURNING j.*,p.provider,p.connection_id::text,p.mapping,p.allowed_fields,p.purpose,s.owner_user_id,
|
|
170
|
+
EXISTS(SELECT 1 FROM analytics_crm_connection_capabilities c
|
|
171
|
+
WHERE c.connection_id=p.connection_id AND c.contract_version=1 AND c.schema_fingerprint=p.schema_fingerprint) schema_current`,[Math.max(1,Math.min(50,e.limit)),e.leaseSeconds??90]);for(let n of t.rows)await I(e.db).query(`INSERT INTO analytics_crm_outbound_attempts(id,job_id,attempt,status,started_at)
|
|
172
|
+
VALUES($1,$2,$3,'started',now()) ON CONFLICT(job_id,attempt) DO NOTHING`,[Z(),n.job_id,Number(n.attempts)]);return t.rows.map(n=>({job:vt(n),provider:n.provider,connectionId:n.connection_id,ownerUserId:Number(n.owner_user_id),payloadCiphertext:n.payload_ciphertext,mapping:n.mapping,allowedFields:n.allowed_fields,purpose:n.purpose,schemaCurrent:n.schema_current}))}async function yn(e){return(await I(e.db).query(`SELECT EXISTS(
|
|
173
|
+
SELECT 1
|
|
174
|
+
FROM analytics_crm_outbound_jobs j
|
|
175
|
+
JOIN analytics_crm_outbound_policies p ON p.policy_id=j.policy_id
|
|
176
|
+
JOIN analytics_people person ON person.id=j.person_id AND person.site_id=j.site_id
|
|
177
|
+
WHERE j.job_id=$1 AND j.state='running' AND j.attempts=$2
|
|
178
|
+
AND j.lease_expires_at>now() AND j.payload_ciphertext<>''
|
|
179
|
+
AND p.enabled=true AND p.version=j.policy_version AND p.purpose=$3
|
|
180
|
+
AND EXISTS (
|
|
181
|
+
SELECT 1 FROM analytics_purpose_policies purpose_policy
|
|
182
|
+
WHERE purpose_policy.site_id=j.site_id
|
|
183
|
+
AND purpose_policy.purpose=CASE $3
|
|
184
|
+
WHEN 'necessary_form_fulfillment' THEN 'necessary'
|
|
185
|
+
WHEN 'site_analytics' THEN 'site_analytics'
|
|
186
|
+
ELSE 'advertising_measurement' END
|
|
187
|
+
AND purpose_policy.enabled=true AND purpose_policy.retired_at IS NULL
|
|
188
|
+
)
|
|
189
|
+
AND NOT EXISTS (
|
|
190
|
+
SELECT 1 FROM analytics_identity_tombstones tombstone
|
|
191
|
+
WHERE tombstone.site_id=j.site_id AND tombstone.person_id=j.person_id
|
|
192
|
+
)
|
|
193
|
+
AND ($3='necessary_form_fulfillment' OR EXISTS (
|
|
194
|
+
SELECT 1
|
|
195
|
+
FROM analytics_consent_current_state consent
|
|
196
|
+
JOIN analytics_consent_receipts receipt
|
|
197
|
+
ON receipt.id=consent.latest_receipt_id AND receipt.site_id=consent.site_id
|
|
198
|
+
WHERE consent.site_id=j.site_id
|
|
199
|
+
AND consent.latest_receipt_id::text=$4
|
|
200
|
+
AND consent.gpc=false
|
|
201
|
+
AND receipt.action NOT IN ('withdraw','reject_all','gpc')
|
|
202
|
+
AND (consent.expires_at IS NULL OR consent.expires_at>now())
|
|
203
|
+
AND EXISTS (
|
|
204
|
+
SELECT 1 FROM analytics_purpose_policies current_policy
|
|
205
|
+
WHERE current_policy.site_id=consent.site_id
|
|
206
|
+
AND current_policy.purpose=CASE $3
|
|
207
|
+
WHEN 'site_analytics' THEN 'site_analytics'
|
|
208
|
+
ELSE 'advertising_measurement' END
|
|
209
|
+
AND current_policy.jurisdiction_profile=consent.jurisdiction_profile
|
|
210
|
+
AND current_policy.enabled=true AND current_policy.retired_at IS NULL
|
|
211
|
+
)
|
|
212
|
+
AND (($3='site_analytics' AND consent.site_analytics=true) OR
|
|
213
|
+
($3='advertising_measurement' AND consent.advertising_measurement=true))
|
|
214
|
+
))
|
|
215
|
+
) authorized`,[e.jobId,e.attempt,e.purpose,e.consentReceiptRef])).rows[0]?.authorized===!0}async function gn(e){let t=I(e.db);return(await t.query(`UPDATE analytics_crm_outbound_jobs
|
|
216
|
+
SET state='cancelled',payload_ciphertext='',next_attempt_at=NULL,
|
|
217
|
+
lease_expires_at=NULL,cancelled_at=now(),safe_error_code=$3,updated_at=now()
|
|
218
|
+
WHERE job_id=$1 AND state='running' AND attempts=$2`,[e.jobId,e.attempt,e.safeErrorCode.slice(0,80)])).rowCount?(await t.query(`UPDATE analytics_crm_outbound_attempts
|
|
219
|
+
SET status='cancelled',safe_error_code=$3,completed_at=now()
|
|
220
|
+
WHERE job_id=$1 AND attempt=$2 AND status='started'`,[e.jobId,e.attempt,e.safeErrorCode.slice(0,80)]),!0):!1}async function Ve(e){let t=I(e.db),n=await t.query("SELECT attempts,max_attempts FROM analytics_crm_outbound_jobs WHERE job_id=$1",[e.jobId]);if(!n.rowCount)throw new f("analytics_crm_resource_not_found","CRM outbound job was not found.",404);let i=Number(n.rows[0].attempts),r=e.status!=="retrying"||i>=Number(n.rows[0].max_attempts),s=e.status==="accepted"?"delivered":e.status==="retrying"&&!r?"retrying":"dead_letter",a=`crmout_${Be({jobId:e.jobId,attempt:i,status:e.status}).slice(0,24)}`;return await t.query(`UPDATE analytics_crm_outbound_jobs SET state=$2,safe_error_code=$3,next_attempt_at=$4,
|
|
221
|
+
lease_expires_at=NULL,delivered_at=CASE WHEN $2='delivered' THEN now() ELSE delivered_at END,updated_at=now()
|
|
222
|
+
WHERE job_id=$1 AND state='running'`,[e.jobId,s,e.safeErrorCode??null,s==="retrying"?e.retryAt??null:null]),await t.query(`INSERT INTO analytics_crm_outbound_attempts(id,job_id,attempt,status,safe_error_code,completed_at)
|
|
223
|
+
VALUES($1,$2,$3,$4,$5,now()) ON CONFLICT(job_id,attempt) DO UPDATE SET status=EXCLUDED.status,safe_error_code=EXCLUDED.safe_error_code,completed_at=now()`,[Z(),e.jobId,i,e.status==="accepted"?"accepted":s==="retrying"?"retrying":"failed",e.safeErrorCode??null]),await t.query(`INSERT INTO analytics_crm_outbound_receipts(receipt_id,job_id,status,external_receipt_id,safe_error_code)
|
|
224
|
+
VALUES($1,$2,$3,$4,$5) ON CONFLICT(receipt_id) DO NOTHING`,[a,e.jobId,s==="retrying"?"retrying":e.status,e.externalReceiptId??null,e.safeErrorCode??null]),dt.parse({receiptId:a,jobId:e.jobId,status:s==="retrying"?"retrying":e.status,...e.externalReceiptId?{externalReceiptId:e.externalReceiptId}:{},...e.safeErrorCode?{safeErrorCode:e.safeErrorCode}:{},occurredAt:new Date().toISOString()})}async function oa(e){return(await I(e.db).query(`SELECT r.* FROM analytics_crm_outbound_receipts r
|
|
225
|
+
JOIN analytics_crm_outbound_jobs j ON j.job_id=r.job_id
|
|
226
|
+
JOIN analytics_crm_outbound_policies p ON p.policy_id=j.policy_id
|
|
227
|
+
WHERE j.site_id=$1 AND p.provider NOT IN ('salesforce','pipedrive')
|
|
228
|
+
ORDER BY r.occurred_at DESC LIMIT $2`,[e.siteId,Math.max(1,Math.min(100,e.limit??25))])).rows.map(n=>dt.parse({receiptId:n.receipt_id,jobId:n.job_id,status:n.status,...n.external_receipt_id?{externalReceiptId:n.external_receipt_id}:{},...n.safe_error_code?{safeErrorCode:n.safe_error_code}:{},occurredAt:n.occurred_at instanceof Date?n.occurred_at.toISOString():n.occurred_at}))}import{createHash as vn}from"crypto";var qi=["hubspot","salesforce","highlevel","zoho","pipedrive","keap"],ca=["hubspot","highlevel","zoho","keap"];var H=e=>{let t={describe:!1,discover_schema:!1,incremental_read:!1,webhook:!1,create_fields:!1,upsert_person:!1,append_activity:!1,test_write:!1,list_outcomes:!1,pipeline_write:!1};for(let n of e)t[n]=!0;return Object.freeze(t)},ue=["describe","discover_schema","incremental_read","webhook","upsert_person","append_activity","test_write","list_outcomes","pipeline_write"],fn=Object.freeze({hubspot:{provider:"hubspot",displayName:"HubSpot",providerConfigKeys:["hubspot"],auth:"oauth2",operations:H([...ue,"create_fields"]),personObject:"contacts",pipelineObject:"deals",activityObject:"notes",supportedObjects:["contacts","companies","deals","notes"],metadataEndpoint:"/crm/v3/properties/{objectType}",customFieldEndpoint:"/crm/v3/properties/{objectType}",externalIdStrategy:"email or an approved unique contact property",writablePersonFields:["email","firstname","lastname","phone"],requiredPersonFields:[],stageField:"dealstage",valueField:"amount",associations:["contact_to_company","contact_to_deal","note_to_contact"],pagination:"cursor",batchLimit:100,rateLimitHeaders:["x-hubspot-ratelimit-remaining","retry-after"],caveats:["Effective properties and writable scopes are tenant-specific."],safeFallback:"note"},salesforce:{provider:"salesforce",displayName:"Salesforce",providerConfigKeys:["salesforce"],auth:"oauth2",operations:H([...ue,"create_fields"]),personObject:"Contact",pipelineObject:"Opportunity",activityObject:"Task",supportedObjects:["Lead","Contact","Account","Opportunity","Task"],metadataEndpoint:"/services/data/{version}/sobjects/{object}/describe",customFieldEndpoint:"Metadata API",externalIdStrategy:"approved External ID field",writablePersonFields:["Email","FirstName","LastName","Phone"],requiredPersonFields:["LastName"],stageField:"StageName",valueField:"Amount",associations:["Contact.AccountId","Opportunity.AccountId","OpportunityContactRole"],pagination:"cursor",batchLimit:200,rateLimitHeaders:["sforce-limit-info","retry-after"],caveats:["Required fields, validation rules, record types and field-level security are tenant-specific."],safeFallback:"activity"},highlevel:{provider:"highlevel",displayName:"HighLevel",providerConfigKeys:["highlevel","gohighlevel"],auth:"oauth2",operations:H([...ue,"create_fields"]),personObject:"contacts",pipelineObject:"opportunities",activityObject:"notes",supportedObjects:["contacts","companies","opportunities","notes"],metadataEndpoint:"/locations/{locationId}/customFields",customFieldEndpoint:"/locations/{locationId}/customFields",externalIdStrategy:"location-scoped email/phone or approved custom field",writablePersonFields:["email","firstName","lastName","phone"],requiredPersonFields:[],stageField:"pipelineStageId",valueField:"monetaryValue",associations:["contact.location","opportunity.contact"],pagination:"cursor",batchLimit:100,rateLimitHeaders:["x-ratelimit-remaining","retry-after"],caveats:["Location context and installed OAuth scopes determine effective access."],safeFallback:"note"},zoho:{provider:"zoho",displayName:"Zoho CRM",providerConfigKeys:["zoho","zoho-crm"],auth:"oauth2",operations:H([...ue,"create_fields"]),personObject:"Contacts",pipelineObject:"Deals",activityObject:"Notes",supportedObjects:["Leads","Contacts","Accounts","Deals","Notes"],metadataEndpoint:"/crm/v7/settings/fields",customFieldEndpoint:"/crm/v7/settings/fields",externalIdStrategy:"approved unique field and duplicate-check preference",writablePersonFields:["Email","First_Name","Last_Name","Phone"],requiredPersonFields:["Last_Name"],stageField:"Stage",valueField:"Amount",associations:["Contact_Name","Account_Name","Parent_Id"],pagination:"page",batchLimit:100,rateLimitHeaders:["x-api-credits-remaining","retry-after"],caveats:["Custom field APIs, editions and API credits vary by tenant."],safeFallback:"note"},pipedrive:{provider:"pipedrive",displayName:"Pipedrive",providerConfigKeys:["pipedrive"],auth:"oauth2",operations:H([...ue,"create_fields"]),personObject:"persons",pipelineObject:"deals",activityObject:"notes",supportedObjects:["persons","organizations","deals","activities","notes"],metadataEndpoint:"/v1/personFields",customFieldEndpoint:"/v1/personFields",externalIdStrategy:"approved namespaced custom field; custom-field keys are tenant-specific",writablePersonFields:["name","email","phone"],requiredPersonFields:["name"],stageField:"stage_id",valueField:"value",associations:["person_id","org_id","deal_id"],pagination:"cursor",batchLimit:100,rateLimitHeaders:["x-ratelimit-remaining","retry-after"],caveats:["Custom-field hashes must be discovered from the connected tenant."],safeFallback:"note"},keap:{provider:"keap",displayName:"Keap",providerConfigKeys:["keap","infusionsoft"],auth:"oauth2",operations:H([...ue,"create_fields"]),personObject:"contacts",pipelineObject:"opportunities",activityObject:"notes",supportedObjects:["contacts","companies","opportunities","notes","tags"],metadataEndpoint:"/v1/contactCustomFields",customFieldEndpoint:"/v1/contactCustomFields",externalIdStrategy:"email or approved custom field",writablePersonFields:["email_addresses","given_name","family_name","phone_numbers"],requiredPersonFields:[],stageField:"stage_id",valueField:"projected_revenue",associations:["contact.company","opportunity.contact"],pagination:"offset",batchLimit:100,rateLimitHeaders:["x-ratelimit-remaining","retry-after"],caveats:["Custom fields, opportunity access and throttling depend on account entitlements."],safeFallback:"note"},callrail:{provider:"callrail",displayName:"CallRail",providerConfigKeys:["callrail"],auth:"api_key",operations:H(["describe","incremental_read","webhook","list_outcomes"]),personObject:null,pipelineObject:null,activityObject:"calls",supportedObjects:["calls"],metadataEndpoint:null,customFieldEndpoint:null,externalIdStrategy:null,writablePersonFields:[],requiredPersonFields:[],stageField:null,valueField:null,associations:[],pagination:"cursor",batchLimit:100,rateLimitHeaders:["x-ratelimit-remaining","retry-after"],caveats:["Read and webhook call outcomes only; no CRM person or pipeline writes."],safeFallback:"webhook_only"},twilio:{provider:"twilio",displayName:"Twilio",providerConfigKeys:["twilio"],auth:"account_token",operations:H(["describe","incremental_read","webhook","list_outcomes"]),personObject:null,pipelineObject:null,activityObject:"calls",supportedObjects:["calls"],metadataEndpoint:null,customFieldEndpoint:null,externalIdStrategy:null,writablePersonFields:[],requiredPersonFields:[],stageField:null,valueField:null,associations:[],pagination:"page",batchLimit:100,rateLimitHeaders:["twilio-concurrent-requests","retry-after"],caveats:["Read and webhook call outcomes only; no CRM person or pipeline writes."],safeFallback:"webhook_only"},ctm:{provider:"ctm",displayName:"CallTrackingMetrics",providerConfigKeys:["ctm","calltrackingmetrics"],auth:"api_key",operations:H(["describe","webhook"]),personObject:null,pipelineObject:null,activityObject:"calls",supportedObjects:["calls"],metadataEndpoint:null,customFieldEndpoint:null,externalIdStrategy:null,writablePersonFields:[],requiredPersonFields:[],stageField:null,valueField:null,associations:[],pagination:"webhook_only",batchLimit:null,rateLimitHeaders:["retry-after"],caveats:["Webhook-only until incremental cursor semantics are documented and proven."],safeFallback:"webhook_only"}}),Wi=Object.freeze({gohighlevel:"highlevel",ghl:"highlevel","high-level":"highlevel",infusionsoft:"keap","zoho-crm":"zoho",calltrackingmetrics:"ctm","call-tracking-metrics":"ctm"});function ee(e){let t=e.trim().toLowerCase().replace(/[ _]+/g,"-"),n=Wi[t]??t;if(!(n in fn))throw new Error("analytics_provider_unsupported");return n}function Ke(e){return fn[ee(e)]}function _e(e,t){let n=Ke(e);if(!n.operations[t])throw new Error(`analytics_provider_operation_unsupported:${n.provider}:${t}`);return n}var x=Object.freeze({describe:"describe-xray-crm-capabilities",discoverSchema:"discover-xray-crm-schema",upsertPerson:"upsert-xray-person",appendActivity:"append-xray-journey-activity",testWrite:"test-xray-write",listOutcomes:"list-xray-outcomes",createFields:"create-xray-custom-fields",pipelineEvent:"apply-xray-pipeline-event"}),Bi=Object.freeze({hubspot:["crm.objects.deals.read"],salesforce:["api"],highlevel:["opportunities.readonly"],zoho:["ZohoCRM.modules.deals.READ"],pipedrive:["deals:read"],keap:["full"]});function da(e){let t=ee(e.provider);if(!qi.includes(t))throw new Error("analytics_crm_provider_required");let n=t,i=Ke(n),r=new Set(e.compiledActions??[]),s=new Set(e.advertisedTools??[]),a=new Set(e.grantedScopes??[]),o=e.documented===!0,c=e.normalizedAdapter===!0,d=x.listOutcomes,_=c&&r.has(d),u=s.has(d),y=Bi[n],E=y.every(w=>a.has(w)),b=new Set(e.tenantObjects??[]),V=!!(i.pipelineObject&&b.has(i.pipelineObject)),P=o&&_&&u&&E&&V,h=i.operations.webhook&&e.webhookAdvertised===!0,T=[...o?[]:["official_documentation"],...c?[]:["normalized_adapter"],...r.has(d)?[]:[`compiled_action:${d}`],...u?[]:[`connection_tool:${d}`],...y.filter(w=>!a.has(w)).map(w=>`scope:${w}`),...!V&&i.pipelineObject?[`tenant_object:${i.pipelineObject}`]:[]],m=P&&h?"incremental_and_webhook":P?"incremental_only":h?"webhook_only":e.manualBackfillAvailable===!0?"manual_backfill":"unavailable",K=(w,A)=>i.operations[w]&&r.has(A)&&s.has(A);return{provider:n,mode:m,canSyncNow:P,canSchedule:P,canProvisionFields:K("create_fields",x.createFields),canUpsertPerson:K("upsert_person",x.upsertPerson),canApplyPipelineEvent:K("pipeline_write",x.pipelineEvent),supportedObjects:e.tenantObjects?i.supportedObjects.filter(w=>b.has(w)):[],evidence:{documented:o,normalizedAdapter:c,advertisedTools:[...s].sort(),grantedScopes:[...a].sort(),missingRequirements:T,tenantSchemaFingerprint:e.tenantSchemaFingerprint??null,evaluatedAt:e.evaluatedAt??new Date().toISOString()},caveats:i.caveats}}var En=[{internalName:"xray_person_id",label:"X-Ray Person ID",type:"string",writable:!0},{internalName:"xray_journey_url",label:"X-Ray Detailed Journey",type:"url",writable:!0},{internalName:"xray_first_touch",label:"X-Ray First Touch",type:"string",writable:!0},{internalName:"xray_last_non_direct_touch",label:"X-Ray Last Non-Direct Touch",type:"string",writable:!0},{internalName:"xray_converting_touch",label:"X-Ray Converting Touch",type:"string",writable:!0},{internalName:"xray_landing_page",label:"X-Ray Landing Page",type:"url",writable:!0},{internalName:"xray_conversion_page",label:"X-Ray Conversion Page",type:"url",writable:!0},{internalName:"xray_session_count",label:"X-Ray Session Count",type:"number",writable:!0},{internalName:"xray_event_count",label:"X-Ray Event Count",type:"number",writable:!0},{internalName:"xray_consent_receipt_ref",label:"X-Ray Consent Receipt",type:"string",writable:!0}];function te(e,t){return`${e}_${vn("sha256").update(JSON.stringify(t)).digest("hex").slice(0,24)}`}function bt(e){return vn("sha256").update(JSON.stringify(e)).digest("hex")}function Vi(e){return bt([...e].sort((t,n)=>`${t.object}:${t.internalName}`.localeCompare(`${n.object}:${n.internalName}`)))}function N(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function C(e,t=300){return typeof e=="string"&&e.trim()?e.trim().slice(0,t):void 0}function Ki(e){let t=C(e,80)?.toLowerCase()??"";return["number","integer","double","currency","decimal","percent","monetary"].some(n=>t.includes(n))?"number":["datetime","date","timestamp"].some(n=>t.includes(n))?"datetime":["bool","checkbox"].some(n=>t.includes(n))?"boolean":["url","uri"].some(n=>t.includes(n))?"url":"string"}function me(e){let t=C(e.object,100),n=C(e.internalName,160),i=C(e.label,160);return!t||!n||!i?null:{object:t,internalName:n,label:i,type:Ki(e.type),writable:e.writable,...e.required?{required:!0}:{}}}function Et(e){return Array.isArray(e)?e.flatMap(t=>{if(!N(t))return[];let n=C(t.id,240),i=C(t.name??t.label,240);if(!n||!i)return[];let r=Array.isArray(t.stages)?t.stages.flatMap(s=>{if(!N(s))return[];let a=C(s.id,240),o=C(s.name??s.label,240);return a&&o?[{id:a,name:o}]:[]}):[];return[{id:n,name:i,stages:r}]}):[]}function Ji(e,t){let n=N(t)?t:{},i=[],r=[],s=Et(n.pipelines),a=o=>{o&&i.push(o)};if(e==="hubspot"){for(let[o,c]of[["contacts","contacts"],["deals","deals"]])if(Array.isArray(n[o])){r.push(c);for(let d of n[o])N(d)&&a(me({object:c,internalName:d.name,label:d.label,type:d.type,writable:d.archived!==!0&&d.providerDefined!==!0}))}}else if(e==="salesforce"){if(Array.isArray(n.objects))for(let o of n.objects){if(!N(o))continue;let c=C(o.object,100);if(!c)continue;r.push(c);let d=N(o.permissions)?o.permissions:{};if(Array.isArray(o.fields))for(let _ of o.fields)N(_)&&a(me({object:c,internalName:_.name,label:_.label,type:_.type,writable:d.update===!0&&_.updateable===!0,required:_.required===!0}))}}else if(e==="highlevel"){if(Array.isArray(n.customFields)){for(let o of n.customFields)if(N(o)){let c=o.model==="opportunity"?"opportunities":"contacts";r.push(c),a(me({object:c,internalName:o.fieldKey??o.id,label:o.name,type:o.dataType,writable:!0}))}}}else if(e==="zoho"){let o=Array.isArray(n.moduleResults)?n.moduleResults:[n];for(let c of o){if(!N(c))continue;let d=C(c.module,100);if(d&&(r.push(d),Array.isArray(c.fields)))for(let _ of c.fields)N(_)&&a(me({object:d,internalName:_.apiName,label:_.label,type:_.dataType,writable:_.writable===!0,required:_.required===!0}))}}else if(e==="pipedrive"){if(Array.isArray(n.fields)){for(let c of n.fields)if(N(c)){let d=c.objectType==="deal"?"deals":"persons";r.push(d),a(me({object:d,internalName:c.code,label:c.name,type:c.fieldType,writable:c.writable===!0}))}}let o=Array.isArray(n.stages)?n.stages:[];s=Et(Array.isArray(n.pipelines)?n.pipelines.map(c=>N(c)?{...c,id:String(c.id??""),stages:o.filter(d=>N(d)&&String(d.pipelineId)===String(c.id)).map(d=>({...d,id:String(d.id??"")}))}:c):[])}else if(e==="keap"){if(r.push("contacts"),Array.isArray(n.contactFields))for(let c of n.contactFields)N(c)&&a(me({object:"contacts",internalName:c.fieldName??c.id,label:c.label,type:c.fieldType,writable:!0}));let o=Array.isArray(n.opportunityStages)?n.opportunityStages:[];o.length&&(s=[{id:"opportunities",name:"Opportunities",stages:Et([{id:"opportunities",name:"Opportunities",stages:o}])[0]?.stages??[]}])}return{fields:i,objects:[...new Set(r)],pipelines:s,...Array.isArray(n.allowedOperations)?{allowedOperations:n.allowedOperations.filter(o=>typeof o=="string")}:{}}}function zi(e){return!N(e)||!Array.isArray(e.fields)?[]:e.fields.flatMap(t=>{if(!N(t))return[];let n=C(t.object,100),i=C(t.internalName??t.internal_name,160);if(!n||!i)return[];let r=C(t.id??t.providerFieldId,240);return[{object:n,internalName:i,...r?{providerFieldId:r}:{}}]})}function Je(e){let t=ee(e);if(t==="callrail"||t==="twilio"||t==="ctm")throw new Error("analytics_crm_provider_required");return t}function ma(e){let t=Je(e.provider),n=_e(t,"discover_schema");if(!n.personObject||!e.connectionId.trim())throw new Error("analytics_crm_provisioning_invalid_scope");let i={},r=[];for(let d of En){let _=e.fields.find(u=>u.object===n.personObject&&(u.internalName.toLowerCase()===d.internalName||u.label.toLowerCase()===d.label.toLowerCase()));_?_.writable&&(i[d.internalName]=_.internalName):r.push({...d,object:n.personObject})}let s=e.now??new Date().toISOString(),a=Vi(e.fields),o={provider:t,connectionId:e.connectionId,existingMappings:i,proposedFields:r,schemaFingerprint:a},c=bt(o);return{planId:te("crmplan",o),provider:t,connectionId:e.connectionId,personObject:n.personObject,existingMappings:i,proposedFields:r,fallback:n.safeFallback==="activity"?"activity":"note",permissionRequirements:["schema:read",...r.length?["schema:write"]:[],"records:write"],schemaFingerprint:a,requestFingerprint:c,createdAt:s}}async function pa(e){let t=_e(e.plan.provider,"create_fields"),n=te("crmplan",{provider:e.plan.provider,connectionId:e.plan.connectionId,existingMappings:e.plan.existingMappings,proposedFields:e.plan.proposedFields,schemaFingerprint:e.plan.schemaFingerprint}),i=bt({provider:e.plan.provider,connectionId:e.plan.connectionId,existingMappings:e.plan.existingMappings,proposedFields:e.plan.proposedFields,schemaFingerprint:e.plan.schemaFingerprint});if(e.plan.planId!==n||e.plan.personObject!==t.personObject||e.plan.requestFingerprint!==i)throw new Error("analytics_crm_provisioning_plan_tampered");let r=new Set(En.map(c=>c.internalName));if(e.plan.proposedFields.some(c=>c.object!==t.personObject||!r.has(c.internalName)))throw new Error("analytics_crm_provisioning_plan_tampered");if(!Number.isInteger(e.approvedByUserId)||e.approvedByUserId<=0)throw new Error("analytics_crm_provisioning_approval_required");let s=new Set(e.approvedFieldInternalNames);if([...s].some(c=>!e.plan.proposedFields.some(d=>d.internalName===c)))throw new Error("analytics_crm_provisioning_field_not_in_plan");let a=e.plan.proposedFields.filter(c=>s.has(c.internalName)),o=te("crmreceipt",{planId:e.plan.planId,fields:[...s].sort()});try{let c=a.length?await e.dependencies.callAction({connectionId:e.plan.connectionId,action:x.createFields,payload:{provider:e.plan.provider,fields:a},idempotencyKey:o}):{fields:[]};return{receiptId:o,planId:e.plan.planId,provider:e.plan.provider,connectionId:e.plan.connectionId,status:"approved",createdFields:zi(c),existingMappings:e.plan.existingMappings,cleanup:{supported:!1,instructions:"Created empty X-Ray fields may be removed manually after confirming they are not populated."},planFingerprint:e.plan.requestFingerprint,occurredAt:e.now??new Date().toISOString()}}catch(c){return{receiptId:o,planId:e.plan.planId,provider:e.plan.provider,connectionId:e.plan.connectionId,status:"failed",createdFields:[],existingMappings:e.plan.existingMappings,cleanup:{supported:!1,instructions:"No cleanup was attempted."},planFingerprint:e.plan.requestFingerprint,occurredAt:e.now??new Date().toISOString(),errorCode:C(c instanceof Error?c.message:c,80)?.replace(/[^a-zA-Z0-9_:-]/g,"_")||"analytics_crm_provisioning_failed"}}}async function ya(e){if(e.receipt.status!=="approved")throw new Error("analytics_crm_provisioning_not_approved");_e(e.receipt.provider,"test_write");let t=te("crmtest",e.receipt.receiptId),n=await e.dependencies.callAction({connectionId:e.receipt.connectionId,action:x.testWrite,payload:{provider:e.receipt.provider,marker:"X-Ray reversible connection test",cleanupRequested:!0},idempotencyKey:t});if(!N(n)||n.ok!==!0)throw new Error("analytics_crm_test_write_not_verified");return{...e.receipt,status:"tested",occurredAt:e.now??new Date().toISOString()}}async function bn(e){let t=Je(e.provider);if(_e(t,"upsert_person"),e.person.identityTier!=="confirmed"||!e.person.deterministicIdentity.value.trim())throw new Error("analytics_crm_person_requires_confirmed_identity");let n=te("crmperson",{provider:t,connectionId:e.connectionId,personId:e.person.personId,identity:e.person.deterministicIdentity,consent:e.person.consentReceiptRef}),i={provider:t,identity:e.person.deterministicIdentity,contact:e.person.contact,journey:{personId:e.person.personId,firstTouch:e.person.firstTouch,lastNonDirectTouch:e.person.lastNonDirectTouch,convertingTouch:e.person.convertingTouch,landingPage:e.person.landingPage,conversionPage:e.person.conversionPage,sessionCount:e.person.sessionCount,eventCount:e.person.eventCount,campaignClickCoverage:e.person.campaignClickCoverage,consentReceiptRef:e.person.consentReceiptRef,detailedJourneyUrl:e.person.detailedJourneyUrl},mapping:e.mapping??{},createDeal:!1},r=await e.dependencies.callAction({connectionId:e.connectionId,action:x.upsertPerson,payload:i,idempotencyKey:n}),s=N(r)?C(r.recordRef??r.id,240):void 0;return{receiptId:n,provider:t,operation:"person_upsert",dealCreated:!1,...s?{providerRecordRef:s}:{}}}async function An(e){let t=Je(e.provider);if(_e(t,"pipeline_write"),e.person.identityTier!=="confirmed")throw new Error("analytics_crm_pipeline_requires_confirmed_identity");if(e.event.eventName!==e.mapping.eventName)throw new Error("analytics_crm_pipeline_event_not_mapped");if(e.mapping.valueSource==="verified_revenue"&&(!Number.isSafeInteger(e.event.verifiedRevenueMinor)||e.event.verifiedRevenueMinor<0))throw new Error("analytics_crm_pipeline_verified_revenue_required");let n=te("crmpipeline",{provider:t,connectionId:e.connectionId,eventId:e.event.eventId,mappingVersion:e.mapping.version});return await e.dependencies.callAction({connectionId:e.connectionId,action:x.pipelineEvent,payload:{provider:t,command:"apply_pipeline_event",person:{personId:e.person.personId,identity:e.person.deterministicIdentity},event:e.event,mapping:e.mapping},idempotencyKey:n}),{receiptId:n,provider:t,operation:"pipeline_event",dealCreatedOrUpdated:!0}}async function ga(e){let t=Je(e.provider),n=Ke(t),i=t==="highlevel"?{locationId:e.discoveryContext?.locationId}:t==="zoho"?{module:e.discoveryContext?.zohoModules?.[0]??"Contacts"}:{};if(t==="highlevel"&&!C(i.locationId,128))throw new Error("analytics_crm_highlevel_location_required");let r=(y,E)=>e.dependencies.callAction({connectionId:e.connectionId,action:x.discoverSchema,payload:y,idempotencyKey:te("crmdiscover",{provider:t,connectionId:e.connectionId,actionPayload:y,suffix:E})}),s=await r(i);if(t==="zoho"){let y=[...new Set(e.discoveryContext?.zohoModules??["Contacts","Deals"])],E=[s];for(let b of y.slice(1))E.push(await r({module:b},b));s={moduleResults:E}}let a=Ji(t,s),o=a.fields,c=Object.entries(n.operations).filter(([,y])=>y).map(([y])=>y),d=a.allowedOperations?new Set(a.allowedOperations.map(y=>C(y,80)).filter(y=>!!y)):null,_=d?c.filter(y=>d.has(y)):c,u=new Set(n.supportedObjects);return{provider:t,publicProfile:n,connectionId:e.connectionId,fields:o,objects:a.objects.length?a.objects.filter(y=>u.has(y)):n.supportedObjects,pipelines:a.pipelines,allowedOperations:_,discoveredAt:e.now??new Date().toISOString()}}var Xi=25,Gi=25e3,In=q.object({person:ot,event:ct.nullable(),mapping:q.object({version:q.number().int().positive(),eventName:q.string(),pipelineId:q.string(),stageId:q.string(),operation:q.enum(["create_or_update_deal","update_stage"]),valueSource:q.enum(["none","verified_revenue"]),activationEligible:q.boolean()}).strict().nullable()}).strict();function Zi(e,t){return`${e}_${Yi("sha256").update(JSON.stringify(t)).digest("hex").slice(0,24)}`}function ze(e){return tn(e)??void 0}function Qi(e,t,n){return e.purpose==="necessary_form_fulfillment"?t.eventKind==="form":e.purpose==="site_analytics"?n.siteAnalytics:n.advertisingMeasurement}function L(e,t){return e.has(t)}function er(e){let t=new Set(e.policy.allowedFields),n=e.contact?{...L(t,"contact.email")&&e.contact.email?{email:e.contact.email}:{},...L(t,"contact.phone")&&e.contact.phone?{phone:e.contact.phone}:{},...L(t,"contact.firstName")&&e.contact.firstName?{firstName:e.contact.firstName}:{},...L(t,"contact.lastName")&&e.contact.lastName?{lastName:e.contact.lastName}:{}}:void 0,i=[e.event.sourceName??e.event.source,e.event.medium,e.event.campaign].filter(Boolean).map(a=>String(a).slice(0,240)).join(" / "),r=e.policy.purpose==="advertising_measurement"&&e.consent.advertisingMeasurement?G(e.event.clickIds):{},s=Object.entries(r).filter(([a])=>L(t,`clickIds.${a}`)).map(([a])=>a).sort().join(",");return ot.parse({identityTier:"confirmed",personId:e.personId,deterministicIdentity:{kind:"crm_id",value:e.crmPersonRef},...n&&Object.keys(n).length?{contact:n}:{},...L(t,"attribution.firstTouch")&&i?{firstTouch:i}:{},...L(t,"attribution.lastNonDirectTouch")&&i?{lastNonDirectTouch:i}:{},...L(t,"attribution.convertingTouch")&&i?{convertingTouch:i}:{},...L(t,"attribution.landingPage")&&ze(e.event.canonicalUrl)?{landingPage:ze(e.event.canonicalUrl)}:{},...L(t,"attribution.conversionPage")&&ze(e.event.canonicalUrl)?{conversionPage:ze(e.event.canonicalUrl)}:{},sessionCount:e.sessionCount,eventCount:e.eventCount,...s?{campaignClickCoverage:s}:{},consentReceiptRef:e.consent.receiptId??`purpose:${e.policy.purpose}`,detailedJourneyUrl:`https://mcpscraper.dev/dashboard/analytics/people/${encodeURIComponent(e.personId)}`})}async function Sn(e){let t=(await _n({siteId:e.siteId,db:e.db})).filter(s=>s.enabled&&Qi(s,e.event,e.consent)).filter(s=>s.selectedEvents.length===0||s.selectedEvents.includes(e.event.eventName));if(!t.length)return{queued:0,reused:0};let n=await e.db.query(`SELECT p.crm_person_ref,
|
|
229
|
+
(SELECT count(DISTINCT s.id)::text FROM analytics_sessions s WHERE s.site_id=p.site_id AND s.person_id=p.id) session_count,
|
|
230
|
+
((SELECT count(*) FROM analytics_events e WHERE e.site_id=p.site_id AND e.person_id=p.id)+
|
|
231
|
+
(SELECT count(*) FROM analytics_external_events x WHERE x.site_id=p.site_id AND x.person_id=p.id))::text event_count
|
|
232
|
+
FROM analytics_people p WHERE p.site_id=$1 AND p.id=$2`,[e.siteId,e.personId]);if(!n.rows[0])return{queued:0,reused:0};let i=0,r=0;for(let s of t){if(s.kind==="pipeline_event"&&(!s.mapping||s.mapping.eventName!==e.event.eventName))continue;let a=er({personId:e.personId,crmPersonRef:n.rows[0].crm_person_ref,event:e.event,policy:s,consent:e.consent,sessionCount:Number(n.rows[0].session_count),eventCount:Number(n.rows[0].event_count)}),o=s.kind==="pipeline_event"?ct.parse({eventId:e.canonicalEventId,eventName:e.event.eventName,occurredAt:e.event.occurredAt,...s.mapping?.valueSource==="verified_revenue"&&e.event.valueMinor!==void 0?{verifiedRevenueMinor:e.event.valueMinor,currency:e.event.currency?.toUpperCase()}:{}}):null;if(s.kind==="pipeline_event"&&s.mapping?.valueSource==="verified_revenue"&&o?.verifiedRevenueMinor===void 0)continue;let c=In.parse({person:a,event:o,mapping:s.mapping}),d=`${s.policyId}:${s.version}:${e.canonicalEventId}`,_=Be({policy:s,personId:e.personId,event:o,projection:a});(await mn({jobId:Zi("crmjob",{idempotencyKey:d}),policy:s,personId:e.personId,sourceEventId:e.canonicalEventId,payloadCiphertext:de(JSON.stringify(c),"crm-outbound-payload"),idempotencyKey:d,requestFingerprint:_,db:e.db})).reused?r+=1:i+=1}return{queued:i,reused:r}}var Ye={async resolveIdentity(e){return(await Mt(e))?.email??null},async callAction(e){return Yt({identity:e.identity,connectionId:e.connectionId,tool:e.action,input:e.payload,classification:"action",operationKind:"sync",requestId:e.idempotencyKey})},authorizeSend:e=>yn(e),cancelUnauthorized:e=>gn(e)};function tr(e){let t=e instanceof Error?e.message:String(e);return/schema_changed|mapping_version|not_mapped|verified_revenue|required|disabled/i.test(t)?{code:"crm_outbound_contract_invalid",retryable:!1,rateLimited:!1}:/429|rate.?limit|too many/i.test(t)?{code:"crm_outbound_rate_limited",retryable:!0,rateLimited:!0}:/401|403|scope|permission|unauthorized|forbidden/i.test(t)?{code:"crm_outbound_authorization_required",retryable:!1,rateLimited:!1}:{code:"crm_outbound_provider_unavailable",retryable:!0,rateLimited:!1}}async function Ra(e=10,t=Date.now()+Gi,n=Ye){let i=Math.max(1,Math.min(Xi,e)),r=v();await r.query(`INSERT INTO analytics_crm_outbound_worker_state(worker_key,last_started_at,last_error_code,updated_at)
|
|
233
|
+
VALUES('default',now(),NULL,now()) ON CONFLICT(worker_key) DO UPDATE SET last_started_at=now(),last_error_code=NULL,updated_at=now()`);try{let s=await pn({limit:i,leaseSeconds:90}),a=0,o=0,c=0;for(let u of s){if(Date.now()>=t){await Ve({jobId:u.job.jobId,status:"retrying",safeErrorCode:"crm_outbound_tick_deadline",retryAt:new Date(Date.now()+6e4).toISOString()}),o+=1;continue}try{if(!u.schemaCurrent)throw new Error("analytics_crm_schema_changed");let y=he(u.payloadCiphertext,"crm-outbound-payload");if(!y)throw new Error("crm_outbound_payload_unreadable");let E=In.parse(JSON.parse(y)),b=await n.resolveIdentity(u.ownerUserId);if(!b)throw new Error("crm_outbound_owner_identity_unavailable");if(!await(n.authorizeSend??(n===Ye?Ye.authorizeSend:async()=>!0))({jobId:u.job.jobId,attempt:u.job.attempts,purpose:u.purpose,consentReceiptRef:E.person.consentReceiptRef})){await(n.cancelUnauthorized??Ye.cancelUnauthorized)({jobId:u.job.jobId,attempt:u.job.attempts,safeErrorCode:"crm_outbound_privacy_fence_closed"}),c+=1;continue}let h={callAction:({connectionId:m,action:K,payload:w,idempotencyKey:A})=>n.callAction({identity:b,connectionId:m,action:K,payload:w,idempotencyKey:A})},T=u.job.kind==="person_summary"?await bn({provider:u.provider,connectionId:u.connectionId,person:E.person,dependencies:h}):await An({provider:u.provider,connectionId:u.connectionId,person:E.person,event:E.event,mapping:E.mapping,dependencies:h});await Ve({jobId:u.job.jobId,status:"accepted",externalReceiptId:T.receiptId}),a+=1}catch(y){let E=tr(y),b=new Date(Date.now()+(E.rateLimited?30*6e4:Math.min(360*6e4,3e4*2**u.job.attempts))).toISOString();await Ve({jobId:u.job.jobId,status:E.retryable?"retrying":"failed",safeErrorCode:E.code,...E.retryable?{retryAt:b}:{}}),E.retryable&&u.job.attempts<5?o+=1:c+=1}}let d=await r.query("SELECT count(*)::int due FROM analytics_crm_outbound_jobs WHERE state IN ('pending','retrying') AND next_attempt_at <= now()"),_={claimed:s.length,delivered:a,retried:o,deadLettered:c,remainingDue:Number(d.rows[0]?.due??0)};return await r.query(`UPDATE analytics_crm_outbound_worker_state SET last_completed_at=now(),last_success_at=now(),
|
|
234
|
+
last_error_code=NULL,last_claimed=$1,last_remaining_due=$2,updated_at=now() WHERE worker_key='default'`,[_.claimed,_.remainingDue]),_}catch(s){throw await r.query(`UPDATE analytics_crm_outbound_worker_state SET last_completed_at=now(),last_error_code='crm_outbound_worker_failed',updated_at=now()
|
|
235
|
+
WHERE worker_key='default'`).catch(()=>{}),s}}function hn(e,t){return e&&(t??0)>0}import{createHash as Co,createHmac as Dt,generateKeyPairSync as Wr,randomUUID as se,sign as Br}from"crypto";import Vr from"twilio";import{z as l}from"zod";import{z as R}from"zod";var nr=new Set(["AED","AFN","ALL","AMD","ANG","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BHD","BIF","BMD","BND","BOB","BOV","BRL","BSD","BTN","BWP","BYN","BZD","CAD","CDF","CHE","CHF","CHW","CLF","CLP","CNY","COP","COU","CRC","CUP","CVE","CZK","DJF","DKK","DOP","DZD","EGP","ERN","ETB","EUR","FJD","FKP","GBP","GEL","GHS","GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HTG","HUF","IDR","ILS","INR","IQD","IRR","ISK","JMD","JOD","JPY","KES","KGS","KHR","KMF","KPW","KRW","KWD","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LYD","MAD","MDL","MGA","MKD","MMK","MNT","MOP","MRU","MUR","MVR","MWK","MXN","MXV","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PAB","PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB","RWF","SAR","SBD","SCR","SDG","SEK","SGD","SHP","SLE","SOS","SRD","SSP","STN","SVC","SYP","SZL","THB","TJS","TMT","TND","TOP","TRY","TTD","TWD","TZS","UAH","UGX","USD","USN","UYI","UYU","UYW","UZS","VED","VES","VND","VUV","WST","XAF","XAG","XAU","XBA","XBB","XBC","XBD","XCD","XDR","XOF","XPD","XPF","XPT","XSU","XTS","XUA","XXX","YER","ZAR","ZMW","ZWG"]),Re=R.string().trim().length(3).transform(e=>e.toUpperCase()).refine(e=>nr.has(e),"Provide a valid ISO-4217 currency."),Xe=R.number().int("Money must be represented as integer minor units.").min(0,"Money cannot be negative.").max(Number.MAX_SAFE_INTEGER),Ge=R.string().trim().min(1).max(80).default("1"),ir=R.enum(["semantic_markup","source_event","crm_field","transaction"]),rr=R.object({mode:R.literal("none"),ruleVersion:Ge}).strict(),sr=R.object({mode:R.literal("fixed"),valueMinor:Xe,currency:Re,ruleVersion:Ge}).strict(),ar=R.object({mode:R.literal("dynamic"),source:ir,currency:Re,fallbackValueMinor:Xe.nullable().default(null),ruleVersion:Ge}).strict(),Nn=R.discriminatedUnion("mode",[rr,sr,ar]),or=R.object({linkedEventName:R.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),relation:R.enum(["before","after","either"]),intervalSeconds:R.number().int().min(0).max(10*365*24*60*60).nullable(),overrideValueMinor:Xe,currency:Re}).strict(),Ze=R.object({targetEventName:R.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),base:Nn,conditionalOverride:or.nullable().default(null),ruleVersion:Ge}).strict().superRefine((e,t)=>{let n=e.conditionalOverride;n&&e.base.mode!=="none"&&n.currency!==e.base.currency&&t.addIssue({code:"custom",path:["conditionalOverride","currency"],message:"Conditional override currency must match the base value currency."})});function ne(e,t){let n=Date.parse(e);if(!Number.isFinite(n))throw new Error(`${t} must be an ISO timestamp.`);return n}function cr(e,t){let n=Xe.parse(e.valueMinor),i=Cn(t,e.currency);return ne(e.observedAt,"observedAt"),{valueMinor:n,currency:i,observedAt:new Date(e.observedAt).toISOString()}}function dr(e){return Nn.parse(e)}function Cn(e,t){let i=(Array.isArray(e)?e:[e,t]).filter(r=>typeof r=="string"&&r.trim().length>0).map(r=>Re.parse(r));if(i.length===0)throw new Error("A currency is required for a monetary value.");if(new Set(i).size!==1)throw new Error(`Mixed currencies are not compatible: ${[...new Set(i)].join(", ")}.`);return i[0]}function lr(e,t={}){let n=dr(e);if(n.mode==="none")return{valueMinor:null,currency:null,provenance:{mode:"none",source:null,observedAt:null,fallback:!1,ruleVersion:n.ruleVersion,resolution:"absent",baseValueMinor:null,resolvedValueMinor:null,currency:null,linkedEvent:null}};if(n.mode==="fixed"){let s=t.observedAt?new Date(ne(t.observedAt,"observedAt")).toISOString():null;return{valueMinor:n.valueMinor,currency:n.currency,provenance:{mode:"fixed",source:null,observedAt:s,fallback:!1,ruleVersion:n.ruleVersion,resolution:"fixed",baseValueMinor:n.valueMinor,resolvedValueMinor:n.valueMinor,currency:n.currency,linkedEvent:null}}}let i=t.observations?.[n.source];if(i){let s=cr(i,n.currency);return{valueMinor:s.valueMinor,currency:s.currency,provenance:{mode:"dynamic",source:n.source,observedAt:s.observedAt,fallback:!1,ruleVersion:n.ruleVersion,resolution:"dynamic",baseValueMinor:s.valueMinor,resolvedValueMinor:s.valueMinor,currency:s.currency,linkedEvent:null}}}let r=n.fallbackValueMinor;return{valueMinor:r,currency:r===null?null:n.currency,provenance:{mode:"dynamic",source:n.source,observedAt:null,fallback:r!==null,ruleVersion:n.ruleVersion,resolution:r===null?"absent":"fallback",baseValueMinor:r,resolvedValueMinor:r,currency:r===null?null:n.currency,linkedEvent:null}}}function ur(e,t,n){let i=ne(t.occurredAt,"targetEvent.occurredAt"),r=n.filter(s=>{if(s.eventName!==e.linkedEventName)return!1;let o=(ne(s.occurredAt,"linkedEvent.occurredAt")-i)/1e3;return e.relation==="before"&&o>0||e.relation==="after"&&o<0?!1:e.intervalSeconds===null||Math.abs(o)<=e.intervalSeconds});return r.sort((s,a)=>{let o=Math.abs(ne(s.occurredAt,"linkedEvent.occurredAt")-i),c=Math.abs(ne(a.occurredAt,"linkedEvent.occurredAt")-i);return o-c||s.eventId.localeCompare(a.eventId)}),r[0]??null}function Rn(e,t){let n=Ze.parse(e);if(t.targetEvent.eventName!==n.targetEventName)throw new Error(`Value rule targets ${n.targetEventName}, not ${t.targetEvent.eventName}.`);let i=lr({...n.base,ruleVersion:n.ruleVersion},t);if(!n.conditionalOverride)return i;let r=ur(n.conditionalOverride,t.targetEvent,t.linkedEvents);if(!r)return i;if(t.providerOperation==="restatement"&&r.identityTier!=="confirmed")throw new Error("Candidate-only evidence cannot be used for a provider restatement.");let s=i.currency?Cn(i.currency,n.conditionalOverride.currency):Re.parse(n.conditionalOverride.currency);return{valueMinor:n.conditionalOverride.overrideValueMinor,currency:s,provenance:{...i.provenance,ruleVersion:n.ruleVersion,resolution:"conditional_override",resolvedValueMinor:n.conditionalOverride.overrideValueMinor,currency:s,linkedEvent:{eventId:r.eventId,eventName:r.eventName,occurredAt:new Date(ne(r.occurredAt,"linkedEvent.occurredAt")).toISOString(),identityTier:r.identityTier??"candidate"}}}}var wn=l.object({gclid:l.string().trim().max(500).optional(),gbraid:l.string().trim().max(500).optional(),wbraid:l.string().trim().max(500).optional(),fbclid:l.string().trim().max(500).optional(),fbp:l.string().trim().max(500).optional(),fbc:l.string().trim().max(500).optional(),ttclid:l.string().trim().max(500).optional(),rdt_cid:l.string().trim().max(500).optional(),msclkid:l.string().trim().max(500).optional(),li_fat_id:l.string().trim().max(500).optional(),snapclid:l.string().trim().max(500).optional()}).strict(),_r=l.object({site_analytics:l.enum(["granted","denied","unknown"]),enhanced_matching:l.enum(["granted","denied","unknown"]),advertising_measurement:l.enum(["granted","denied","unknown"])}).strict(),mr=l.object({analytics:l.enum(["granted","denied","unknown"]).default("unknown"),adUserData:l.enum(["granted","denied","unknown"]).default("unknown"),adPersonalization:l.enum(["granted","denied","unknown"]).default("unknown"),globalPrivacyControl:l.boolean().default(!1),occurredAt:l.string().datetime(),receiptId:l.string().uuid().optional(),policyVersion:l.number().int().positive().optional(),purposes:_r.optional()}).strict().transform(e=>({...e,purposes:e.purposes??{site_analytics:e.analytics,enhanced_matching:e.adUserData,advertising_measurement:e.adPersonalization}})),On=l.enum(["browser","form","call","crm","transaction","custom_server","conversion","delivery"]),pr=l.object({schemaVersion:l.literal(1).default(1),eventId:l.string().trim().min(8).max(200),sourceEventId:l.string().trim().min(1).max(300).optional(),source:l.string().trim().min(1).max(80).default("xray"),sourceAccountRef:l.string().trim().max(240).optional(),eventKind:On,eventName:l.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),occurredAt:l.string().datetime(),visitorId:l.string().trim().max(160).optional(),sessionId:l.string().trim().max(160).optional(),personRef:l.string().trim().max(240).optional(),orderId:l.string().trim().max(240).optional(),callId:l.string().trim().max(240).optional(),dealId:l.string().trim().max(240).optional(),path:l.string().trim().max(2e3).optional(),canonicalUrl:l.string().url().max(3e3).optional(),sourceName:l.string().trim().max(180).optional(),medium:l.string().trim().max(180).optional(),campaign:l.string().trim().max(240).optional(),clickIds:wn.optional(),consent:mr.optional(),valueMinor:l.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),currency:l.string().trim().regex(/^[A-Za-z]{3}$/).optional(),properties:l.record(l.string().max(80),l.unknown()).default({})}).strict(),Qe=pr.extend({sourceEventId:l.string().trim().min(1).max(300),source:l.string().trim().min(1).max(80),sourceAccountRef:l.string().trim().min(1).max(240)}),$a=l.object({sourceEventId:l.string().trim().min(1).max(300),source:l.string().trim().min(1).max(80),sourceAccountRef:l.string().trim().min(1).max(240),contactId:l.string().trim().max(240).optional(),dealId:l.string().trim().min(1).max(240),pipelineId:l.string().trim().max(240).optional(),stageId:l.string().trim().min(1).max(240),stageName:l.string().trim().max(240).optional(),occurredAt:l.string().datetime(),valueMinor:l.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),currency:l.string().trim().regex(/^[A-Za-z]{3}$/).optional(),visitorId:l.string().trim().max(160).optional(),sessionId:l.string().trim().max(160).optional(),clickIds:wn.optional(),email:l.string().email().max(320).optional(),phone:l.string().trim().max(80).optional(),orderId:l.string().trim().max(240).optional(),status:l.enum(["open","won","lost","spam","unknown"]).default("unknown")}).strict(),yr=l.object({eventName:l.string().regex(/^[a-z][a-z0-9_]{1,79}$/).optional(),eventKind:On.optional(),source:l.string().trim().max(80).optional(),stageId:l.string().trim().max(240).optional(),status:l.enum(["open","won","lost","spam","unknown"]).optional(),answered:l.boolean().optional(),minimumCallDurationSeconds:l.number().int().min(0).max(86400).optional(),excludedTags:l.array(l.string().trim().max(80)).max(30).default([]),requiredTags:l.array(l.string().trim().max(80)).max(30).default([])}).strict(),Dn=l.object({name:l.string().trim().min(1).max(120),conversionKind:l.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),condition:yr,defaultValueMinor:l.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),defaultCurrency:l.string().trim().regex(/^[A-Za-z]{3}$/).default("USD"),valueRule:Ze.optional(),enabled:l.boolean().default(!0)}).strict();function gr(e){let t=e.trim().toLowerCase().replace(/\.$/,"");if(!t||t.includes("://")||/[/?#]/.test(t))return"";try{let n=new URL(`https://${t}`).hostname.toLowerCase().replace(/\.$/,"");return!n||n.length>253||n!=="localhost"&&!n.includes(".")||n.split(".").some(i=>!i||i.length>63||!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(i))?"":n}catch{return""}}var fr=l.string().trim().max(253).transform(gr).refine(Boolean,"Provide a valid bare hostname."),we=l.string().trim().max(80).regex(/^[A-Za-z][A-Za-z0-9_-]{0,79}$/).refine(e=>(e.match(/\d/g)?.length??0)<7,"Semantic dimensions cannot contain phone-like values.").transform(e=>e.toLowerCase()),vr=l.object({value:l.number().finite().min(0).max(1e12).optional(),currency:l.string().trim().regex(/^[A-Za-z]{3}$/).transform(e=>e.toUpperCase()).optional(),orderId:l.string().trim().min(1).max(200).optional(),contentId:l.string().trim().min(1).max(200).optional(),contentType:l.string().trim().min(1).max(80).optional(),placement:we.optional(),component:we.optional(),action:we.optional(),destination:we.optional(),plan:we.optional()}).strict(),At=l.object({name:l.string().trim().min(1).max(120),eventName:l.string().trim().regex(/^[a-z][a-z0-9_]{1,79}$/),triggerKind:l.enum(["page","click","form","semantic"]),path:l.string().trim().startsWith("/").max(2e3).optional(),pathMatch:l.enum(["exact","prefix"]).default("exact"),selector:l.string().trim().min(1).max(500).optional(),hostnames:l.array(fr).max(25).transform(e=>[...new Set(e)]).default([]),metadata:vr.optional(),valueRule:Ze.optional(),oncePerSession:l.boolean().default(!1),enabled:l.boolean().default(!0)}).strict().superRefine((e,t)=>{e.triggerKind==="page"&&!e.path&&t.addIssue({code:"custom",path:["path"],message:"Page triggers require a path."}),["click","form"].includes(e.triggerKind)&&!e.selector&&t.addIssue({code:"custom",path:["selector"],message:"Click and form triggers require a selector."}),e.triggerKind==="semantic"&&(e.path||e.selector)&&t.addIssue({code:"custom",path:["triggerKind"],message:"Semantic triggers use data-xray-event and cannot include a path or selector."})});import{createHmac as Er,randomUUID as It}from"crypto";import Va from"ipaddr.js";import{randomUUID as Tn}from"crypto";async function Ua(e){return(await v().query(`SELECT id, site_id, name, status, version, approved_at
|
|
236
|
+
FROM analytics_identity_namespaces
|
|
237
|
+
WHERE site_id=$1 AND status IN ('draft','approved','disabled')
|
|
238
|
+
ORDER BY CASE status WHEN 'approved' THEN 0 WHEN 'draft' THEN 1 ELSE 2 END,
|
|
239
|
+
version DESC, name, id`,[e.siteId])).rows.map(n=>({id:n.id,siteId:n.site_id,name:n.name,status:n.status,version:n.version,approvedAt:n.approved_at===null?null:new Date(n.approved_at).toISOString()}))}var xn={domain:"host",application:"server",phone:"call",crm:"crm"},Ln={host:"domain",server:"application",call:"phone",crm:"crm"};function $n(e,t){let n=t.trim().toLowerCase();if(!n||n.length>500)throw new f("analytics_namespace_source_invalid","Provide a valid namespace source.",400);if(e==="domain"&&(n.includes("://")||/[/?#]/.test(n)||!n.includes(".")))throw new f("analytics_namespace_domain_invalid","Provide a bare approved domain.",400);return n}async function Fa(e){let t=e.name.trim();if(!t||t.length>160)throw new f("analytics_namespace_name_invalid","Provide a namespace name.",400);let n=await v().connect();await n.query("BEGIN");try{await n.query("SELECT pg_advisory_xact_lock(hashtext($1))",[e.siteId]);let r=(await n.query(`SELECT id, site_id, name, status, version, approved_at
|
|
240
|
+
FROM analytics_identity_namespaces
|
|
241
|
+
WHERE site_id=$1 AND status IN ('draft','approved','disabled')
|
|
242
|
+
ORDER BY version DESC LIMIT 1`,[e.siteId])).rows[0];if(r){if(r.name!==t)throw new f("analytics_namespace_already_exists","This site already belongs to an identity namespace.",409);return await n.query("COMMIT"),{id:r.id,siteId:r.site_id,name:r.name,status:r.status,version:r.version,approvedAt:r.approved_at?.toISOString()??null}}let a=(await n.query(`INSERT INTO analytics_identity_namespaces(
|
|
243
|
+
id, site_id, name, status, approved_hosts, approved_sources, version, approved_by_user_id
|
|
244
|
+
) VALUES ($1,$2,$3,'draft','[]'::jsonb,'[]'::jsonb,1,$4)
|
|
245
|
+
RETURNING id, site_id, name, status, version, approved_at`,[Tn(),e.siteId,t,e.createdByUserId])).rows[0];return await n.query("COMMIT"),{id:a.id,siteId:a.site_id,name:a.name,status:a.status,version:a.version,approvedAt:a.approved_at?.toISOString()??null}}catch(i){throw await n.query("ROLLBACK"),i}finally{n.release()}}async function ja(e){let t=$n(e.sourceKind,e.sourceRef),n=await v().connect();await n.query("BEGIN");try{let i=await n.query(`SELECT id, status FROM analytics_identity_namespaces
|
|
246
|
+
WHERE id=$1 AND site_id=$2 FOR UPDATE`,[e.namespaceId,e.siteId]);if(!i.rows[0])throw new f("analytics_namespace_not_found","Identity namespace not found.",404);if(["archived","disabled"].includes(i.rows[0].status))throw new f("analytics_namespace_inactive","Identity namespace is not active.",409);let r=xn[e.sourceKind],s=await n.query(`INSERT INTO analytics_identity_sources(id, namespace_id, site_id, source_kind, source_ref, status)
|
|
247
|
+
VALUES ($1,$2,$3,$4,$5,'approved')
|
|
248
|
+
ON CONFLICT(namespace_id, source_kind, source_ref) DO UPDATE
|
|
249
|
+
SET status='approved', revoked_at=NULL
|
|
250
|
+
RETURNING id, namespace_id, site_id, source_kind, source_ref, status, created_at, revoked_at`,[Tn(),e.namespaceId,e.siteId,r,t]);await n.query(`UPDATE analytics_identity_namespaces
|
|
251
|
+
SET status='approved', approved_by_user_id=$3, approved_at=COALESCE(approved_at, now()),
|
|
252
|
+
approved_hosts=CASE WHEN $4='host'
|
|
253
|
+
THEN (SELECT jsonb_agg(DISTINCT value) FROM jsonb_array_elements_text(approved_hosts || to_jsonb($5::text)) value)
|
|
254
|
+
ELSE approved_hosts END,
|
|
255
|
+
approved_sources=(SELECT jsonb_agg(DISTINCT value) FROM jsonb_array_elements_text(approved_sources || to_jsonb(($4 || ':' || $5)::text)) value),
|
|
256
|
+
updated_at=now()
|
|
257
|
+
WHERE id=$1 AND site_id=$2`,[e.namespaceId,e.siteId,e.approvedByUserId,r,t]),await n.query("COMMIT");let a=s.rows[0];return{id:a.id,namespaceId:a.namespace_id,siteId:a.site_id,sourceKind:Ln[a.source_kind],sourceRef:a.source_ref,status:a.status,createdAt:a.created_at instanceof Date?a.created_at.toISOString():new Date(a.created_at).toISOString(),revokedAt:a.revoked_at==null?null:a.revoked_at instanceof Date?a.revoked_at.toISOString():new Date(a.revoked_at).toISOString()}}catch(i){throw await n.query("ROLLBACK"),i}finally{n.release()}}async function et(e){let t=v();if(!(await t.query(`SELECT id FROM analytics_identity_namespaces
|
|
258
|
+
WHERE id=$1 AND site_id=$2 AND status='approved'`,[e.namespaceId,e.siteId])).rows[0])throw new f("analytics_namespace_boundary","The identity namespace is not approved for this site.",403);let i=[...new Set(e.identityNodeIds??[])];if(i.length&&(await t.query(`SELECT id FROM analytics_identity_nodes
|
|
259
|
+
WHERE site_id=$1 AND namespace_id=$2 AND id = ANY($3::uuid[])`,[e.siteId,e.namespaceId,i])).rows.length!==i.length)throw new f("analytics_namespace_boundary","Identity evidence cannot cross namespaces.",403);if(e.personId&&!(await t.query("SELECT id FROM analytics_people WHERE id=$1 AND site_id=$2 AND namespace_id=$3",[e.personId,e.siteId,e.namespaceId])).rows[0])throw new f("analytics_namespace_boundary","The person does not belong to this namespace.",403);if(e.source){let r=$n(e.source.kind,e.source.ref);if(!(await t.query(`SELECT id FROM analytics_identity_sources
|
|
260
|
+
WHERE namespace_id=$1 AND site_id=$2 AND source_kind=$3 AND source_ref=$4 AND status='approved'`,[e.namespaceId,e.siteId,xn[e.source.kind],r])).rows[0])throw new f("analytics_namespace_source_unapproved","This identity source is not approved.",403)}}async function Ha(e){return await et(e),(await v().query(`SELECT id, namespace_id, site_id, source_kind, source_ref, status, created_at, revoked_at
|
|
261
|
+
FROM analytics_identity_sources
|
|
262
|
+
WHERE namespace_id=$1 AND site_id=$2
|
|
263
|
+
ORDER BY source_kind, source_ref`,[e.namespaceId,e.siteId])).rows.map(n=>({id:n.id,namespaceId:n.namespace_id,siteId:n.site_id,sourceKind:Ln[n.source_kind],sourceRef:n.source_ref,status:n.status,createdAt:n.created_at.toISOString(),revokedAt:n.revoked_at?.toISOString()??null}))}function br(){let e=process.env.ANALYTICS_IDENTITY_SECRET?.trim()||process.env.SESSION_SECRET?.trim();if(!e)throw new f("analytics_identity_secret_missing","Candidate identity derivation is not configured.",503);return e}function St(e,t){return Er("sha256",br()).update(`xray-candidate:v1:${e}:${t}`).digest("hex")}async function za(e){await et(e);let t=Math.max(1,Math.min(200,Math.floor(e.limit??50)));return(await v().query(`SELECT a.id, a.model_version, a.first_observed_at, a.last_observed_at, a.expires_at,
|
|
264
|
+
array_remove(array_agg(DISTINCT e.feature_kind) FILTER (WHERE e.contribution > 0), NULL) supporting,
|
|
265
|
+
array_remove(array_agg(DISTINCT e.feature_kind) FILTER (WHERE e.contribution < 0), NULL) conflicting
|
|
266
|
+
FROM analytics_candidate_associations a
|
|
267
|
+
LEFT JOIN analytics_candidate_evidence e ON e.association_id=a.id AND e.namespace_id=a.namespace_id
|
|
268
|
+
WHERE a.site_id=$1 AND a.namespace_id=$2 AND a.status='active' AND a.confidence_band='medium'
|
|
269
|
+
AND a.expires_at > $3::timestamptz
|
|
270
|
+
GROUP BY a.id
|
|
271
|
+
ORDER BY a.updated_at DESC LIMIT $4`,[e.siteId,e.namespaceId,e.now??new Date().toISOString(),t])).rows.map(i=>({associationId:i.id,band:"medium",supportingFeatures:i.supporting??[],conflictingFeatures:i.conflicting??[],modelVersion:i.model_version,firstObservedAt:i.first_observed_at.toISOString(),lastObservedAt:i.last_observed_at.toISOString(),expiresAt:i.expires_at.toISOString()}))}async function Pn(e){await et({siteId:e.siteId,namespaceId:e.namespaceId,personId:e.personId});let t=new Date(e.occurredAt??Date.now());if(!e.evidenceRef.trim())throw new f("analytics_candidate_promotion_evidence_required","Deterministic evidence is required.",400);let n=e.promotedBy??"system";if(n==="user"&&!e.promotedByUserId)throw new f("analytics_candidate_promotion_actor_required","User-confirmed promotion requires an actor.",400);let i=St("journey_pattern",`${e.evidenceKind}:${e.evidenceRef.trim().toLowerCase()}`),s=await v().connect();await s.query("BEGIN");try{let a=await s.query(`UPDATE analytics_candidate_associations
|
|
272
|
+
SET status='promoted', promoted_person_id=$4, promoted_at=$5, updated_at=now()
|
|
273
|
+
WHERE id=$1 AND site_id=$2 AND namespace_id=$3
|
|
274
|
+
AND (status IN ('active','expired') OR (status='promoted' AND promoted_person_id=$4))
|
|
275
|
+
RETURNING id,left_identity_node_id,right_identity_node_id`,[e.associationId,e.siteId,e.namespaceId,e.personId,t.toISOString()]);if(!a.rows[0])throw new f("analytics_candidate_not_found","Candidate association not found in this namespace.",404);return await s.query(`INSERT INTO analytics_candidate_promotions(
|
|
276
|
+
id, site_id, namespace_id, association_id, person_id, evidence_kind,
|
|
277
|
+
evidence_ref_hmac, promoted_by, promoted_by_user_id, promoted_at
|
|
278
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
|
279
|
+
ON CONFLICT(association_id, person_id) DO NOTHING`,[It(),e.siteId,e.namespaceId,e.associationId,e.personId,e.evidenceKind,i,n,e.promotedByUserId??null,t.toISOString()]),await s.query(`INSERT INTO analytics_privacy_audits(
|
|
280
|
+
id, site_id, namespace_ref, subject_ref, action, idempotency_key,
|
|
281
|
+
actor_user_id, result_code, affected_counts, completed_at
|
|
282
|
+
) VALUES ($1,$2,$3,$4,'candidate_promoted',$5,$6,'promoted',$7::jsonb,$8)
|
|
283
|
+
ON CONFLICT(site_id, idempotency_key) DO NOTHING`,[It(),e.siteId,e.namespaceId,e.associationId,`candidate-promotion:${e.associationId}:${e.personId}`,e.promotedByUserId??null,JSON.stringify({candidateAssociations:1,deterministicPeople:1}),t.toISOString()]),await s.query(`INSERT INTO analytics_identity_promotion_receipts(
|
|
284
|
+
id,site_id,namespace_id,canonical_person_id,canonical_person_ref_hmac,
|
|
285
|
+
evidence_kind,evidence_ref_hmac,prior_person_ids,prior_identity_node_ids,
|
|
286
|
+
candidate_association_ids,affected_history_counts,schema_version,idempotency_key,promoted_at
|
|
287
|
+
) VALUES($1,$2,$3,$4,$5,$6,$7,'{}'::uuid[],$8::uuid[],$9::uuid[],$10::jsonb,
|
|
288
|
+
'xray.identity-promotion.v1',$11,$12)
|
|
289
|
+
ON CONFLICT(site_id,idempotency_key) DO NOTHING`,[It(),e.siteId,e.namespaceId,e.personId,St("journey_pattern",`person:${e.personId}`),e.evidenceKind,i,[a.rows[0].left_identity_node_id,a.rows[0].right_identity_node_id],[e.associationId],JSON.stringify({events:0,sessions:0,touches:0,conversions:0,candidateTouches:0}),St("journey_pattern",`promotion:${e.siteId}:${e.associationId}:${e.personId}:${e.evidenceKind}:${e.evidenceRef}`),t.toISOString()]),await s.query("COMMIT"),{associationId:e.associationId,personId:e.personId,promotedAt:t.toISOString(),reason:e.evidenceKind}}catch(a){throw await s.query("ROLLBACK"),a}finally{s.release()}}import{createHash as Ar,randomUUID as kn}from"crypto";import{z as g}from"zod";var ie=g.string().trim().min(1).max(160).regex(/^[a-zA-Z0-9_.:-]+$/),ht=g.string().trim().min(1).max(160).regex(/^[a-zA-Z][a-zA-Z0-9_.:-]*$/),Ir=g.discriminatedUnion("kind",[g.object({kind:g.literal("event_completed"),eventName:ie,withinDays:g.number().int().min(1).max(3650).optional()}).strict(),g.object({kind:g.literal("event_frequency"),eventName:ie,minimum:g.number().int().min(1).max(1e3),withinDays:g.number().int().min(1).max(3650).optional()}).strict(),g.object({kind:g.literal("event_recency"),eventName:ie,withinDays:g.number().int().min(1).max(3650)}).strict(),g.object({kind:g.literal("page_visited"),path:g.string().trim().startsWith("/").max(2e3),withinDays:g.number().int().min(1).max(3650).optional()}).strict(),g.object({kind:g.literal("referrer_equals"),value:g.string().trim().min(1).max(500)}).strict(),g.object({kind:g.literal("source_equals"),value:g.string().trim().min(1).max(180)}).strict(),g.object({kind:g.literal("call_outcome"),outcome:ie,withinDays:g.number().int().min(1).max(3650).optional()}).strict(),g.object({kind:g.literal("crm_stage"),stage:ie}).strict(),g.object({kind:g.literal("property_equals"),property:ht,value:g.union([g.string().max(500),g.number().finite(),g.boolean()])}).strict(),g.object({kind:g.literal("property_present"),property:ht}).strict()]),Sr=g.object({id:ie,label:g.string().trim().min(1).max(160),operation:g.enum(["add","subtract"]),points:g.number().int().min(0).max(1e5),cap:g.number().int().min(0).max(1e5).optional(),predicates:g.array(Ir).min(1).max(50)}).strict(),Mn=g.object({namespaceId:ie,name:g.string().trim().min(1).max(160),kind:g.enum(["fit","engagement","combined"]),targetObject:g.enum(["person","visitor"]),minimum:g.number().int().min(-1e6).max(1e6),maximum:g.number().int().min(-1e6).max(1e6),groups:g.array(Sr).min(1).max(100),permittedProperties:g.array(ht).max(200).default([]),enabled:g.boolean().default(!0)}).strict().superRefine((e,t)=>{e.minimum>=e.maximum&&t.addIssue({code:"custom",path:["maximum"],message:"maximum must be greater than minimum"});let n=new Set(e.permittedProperties);for(let[i,r]of e.groups.entries())for(let[s,a]of r.predicates.entries())(a.kind==="property_equals"||a.kind==="property_present")&&!n.has(a.property)&&t.addIssue({code:"custom",path:["groups",i,"predicates",s,"property"],message:"property is not allowlisted"})});function Un(){return{definitions:new Map,evaluations:new Map,evaluationHistory:[]}}var Y=class extends Error{constructor(n,i){super(i);this.code=n;this.name="AnalyticsLeadScoringError"}code};function Nt(e){let t=e instanceof Date?e.toISOString():e??new Date().toISOString();if(!Number.isFinite(Date.parse(t)))throw new Y("analytics_score_invalid_time","A valid evaluation time is required.");return new Date(t).toISOString()}function Oe(e,t){return`${e}:${t}`}function Ct(e){return{...e,groups:e.groups.map(t=>({...t,predicates:t.predicates.map(n=>({...n}))})),permittedProperties:[...e.permittedProperties]}}function hr(e,t,n){let i=Date.parse(e),r=Date.parse(t);return!Number.isFinite(i)||i>r?!1:n===void 0||r-i<=n*864e5}function Nr(e,t,n){let i=(r,s,a)=>(r??[]).filter(o=>o.name===s&&hr(o.occurredAt,n,a));switch(e.kind){case"event_completed":{let r=i(t.events,e.eventName,e.withinDays);return r.length?r.map(s=>s.ref):null}case"event_frequency":{let r=i(t.events,e.eventName,e.withinDays);return r.length>=e.minimum?r.map(s=>s.ref):null}case"event_recency":{let r=i(t.events,e.eventName,e.withinDays);return r.length?r.map(s=>s.ref):null}case"page_visited":{let r=i(t.pages,e.path,e.withinDays);return r.length?r.map(s=>s.ref):null}case"referrer_equals":{let r=(t.referrers??[]).filter(s=>s.name===e.value);return r.length?r.map(s=>s.ref):null}case"source_equals":{let r=(t.sources??[]).filter(s=>s.name===e.value);return r.length?r.map(s=>s.ref):null}case"call_outcome":{let r=i(t.calls,e.outcome,e.withinDays);return r.length?r.map(s=>s.ref):null}case"crm_stage":{let r=i(t.crmStages,e.stage);return r.length?r.map(s=>s.ref):null}case"property_equals":return t.permittedProperties?.[e.property]===e.value?[`property:${e.property}`]:null;case"property_present":{let r=t.permittedProperties?.[e.property];return r!=null?[`property:${e.property}`]:null}}}function Cr(e){let t=n=>Array.isArray(n)?n.map(t):n&&typeof n=="object"?Object.fromEntries(Object.entries(n).sort(([i],[r])=>i.localeCompare(r)).map(([i,r])=>[i,t(r)])):n;return Ar("sha256").update(JSON.stringify(t(e))).digest("hex")}function Fn(e){let t=Mn.parse(e.definition),n=e.id??kn();if(e.tables.definitions.has(Oe(t.namespaceId,n)))throw new Y("analytics_score_exists","A lead score with this ID already exists.");let i=Nt(e.now),r={id:n,...t,groups:t.groups.map(s=>({...s,predicates:s.predicates.map(a=>({...a}))})),version:1,createdAt:i,updatedAt:i};return e.tables.definitions.set(Oe(t.namespaceId,n),r),Ct(r)}function Za(e){let t=e.tables.definitions.get(Oe(e.namespaceId,e.scoreId));if(!t)throw new Y("analytics_score_not_found","Lead score not found.");if(t.version!==e.expectedVersion)throw new Y("analytics_score_version_conflict","The lead score changed; reload before updating.");let n=Mn.parse(e.definition);if(n.namespaceId!==e.namespaceId)throw new Y("analytics_score_namespace_mismatch","Lead scores cannot move between namespaces.");let i={...t,...n,groups:n.groups.map(r=>({...r,predicates:r.predicates.map(s=>({...s}))})),version:t.version+1,updatedAt:Nt(e.now)};return e.tables.definitions.set(Oe(e.namespaceId,e.scoreId),i),Ct(i)}function Qa(e){return[...e.tables.definitions.values()].filter(t=>t.namespaceId===e.namespaceId).sort((t,n)=>t.name.localeCompare(n.name)).map(Ct)}function jn(e){let t=e.tables.definitions.get(Oe(e.namespaceId,e.scoreId));if(!t||!t.enabled)throw new Y("analytics_score_unavailable","Lead score is missing or disabled.");let n=Nt(e.now),i=t.groups.map(_=>{let u=_.predicates.map(b=>Nr(b,e.facts,n)),y=u.every(b=>b!==null),E=y?Math.min(_.points,_.cap??_.points):0;return{groupId:_.id,operation:_.operation,matched:y,points:_.operation==="subtract"?-E:E,evidenceRefs:y?[...new Set(u.flatMap(b=>b??[]))].sort():[]}}),r=i.reduce((_,u)=>_+u.points,0),s=Math.max(t.minimum,Math.min(t.maximum,r)),a=[...new Set(i.flatMap(_=>_.evidenceRefs))].sort(),o=Cr({namespaceId:e.namespaceId,scoreId:e.scoreId,version:t.version,subjectRef:e.subjectRef,identityTier:e.identityTier,components:i,value:s}),c=e.tables.evaluations.get(o);if(c)return{...c,components:c.components.map(_=>({..._,evidenceRefs:[..._.evidenceRefs]})),evidenceRefs:[...c.evidenceRefs]};let d={id:kn(),namespaceId:e.namespaceId,subjectRef:e.subjectRef,identityTier:e.identityTier,scoreId:t.id,scoreVersion:t.version,scoreKind:t.kind,value:s,components:i,evidenceRefs:a,evaluatedAt:n,idempotencyKey:o,providerEligible:e.identityTier==="confirmed"};return e.tables.evaluations.set(o,d),e.tables.evaluationHistory.push(d),{...d,components:d.components.map(_=>({..._,evidenceRefs:[..._.evidenceRefs]})),evidenceRefs:[...d.evidenceRefs]}}import{createHmac as Rr,timingSafeEqual as wr}from"crypto";function p(e,t=500){if(typeof e!="string"&&typeof e!="number")return;let n=String(e).trim();return n?n.slice(0,t):void 0}function W(e){let t=Number(e);return Number.isFinite(t)?t:void 0}function re(e){if(typeof e=="boolean")return e;if(e==="true"||e==="1"||e===1)return!0;if(e==="false"||e==="0"||e===0)return!1}function De(e){if(Array.isArray(e))return e.map(n=>p(n,80)).filter(n=>!!n).slice(0,30);let t=p(e,1e3);return t?t.split(",").map(n=>n.trim()).filter(Boolean).slice(0,30):[]}function B(...e){for(let t of e){let n=p(t,100);if(!n)continue;let i=new Date(n);if(Number.isFinite(i.getTime()))return i.toISOString()}return new Date().toISOString()}function pe(e,t,n){return Rr(e,t).update(n).digest("base64")}function ye(e,t){if(!e||e.length!==t.length)return!1;try{return wr(Buffer.from(e),Buffer.from(t))}catch{return!1}}function ge(e,t=Date.now(),n=300*1e3){let i=p(e,100);if(!i)return!1;let r=Number(i),s=Number.isFinite(r)?r>1e10?r:r*1e3:Date.parse(i);return Number.isFinite(s)&&Math.abs(t-s)<=n}function no(e){let t=new URL(e.requestUrl);for(let s of e.internalRewriteQueryKeys??[])t.searchParams.delete(s);let n=e.publicOrigin?.trim();if(n){let s=new URL(n);if(s.protocol!=="https:"||s.username||s.password)throw new Error("analytics webhook public origin must be an HTTPS origin");return s.pathname=t.pathname,s.search=t.search,s.hash="",s.toString()}let i=e.forwardedProto?.split(",")[0]?.trim(),r=e.forwardedHost?.split(",")[0]?.trim();return i&&(t.protocol=`${i}:`),r&&(t.host=r),t.toString()}function Hn(e){return ge(e.payloadTimestamp,e.now)&&ye(e.signature,pe("sha1",e.secret,e.rawBody))}function qn(e){let t=e.payload,n=p(t.resource_id??t.id,240)||"unknown",i=B(t.timestamp,t.start_time,t.created_at),r=De(t.tags??t.tag),s=p(t.lead_status,80),a=s==="not_a_lead"||r.some(c=>c.toLowerCase()==="spam")||re(t.spam)===!0,o={...p(t.gclid)?{gclid:p(t.gclid)}:{},...p(t.gbraid)?{gbraid:p(t.gbraid)}:{},...p(t.wbraid)?{wbraid:p(t.wbraid)}:{},...p(t.fbclid)?{fbclid:p(t.fbclid)}:{},...p(t.msclkid)?{msclkid:p(t.msclkid)}:{}};return{schemaVersion:1,eventId:`callrail_${n}_${e.phase}`,sourceEventId:`${n}:${e.phase}:${i}`,source:"callrail",sourceAccountRef:e.sourceAccountRef,eventKind:"call",eventName:e.phase==="pre_call"?"call_started":e.phase==="routing_complete"?"call_routed":e.phase==="call_modified"?"call_modified":"call_completed",occurredAt:i,callId:n,personRef:p(t.person_resource_id,240),visitorId:p(t.visitor_id??t.browser_id??t.ga,160),canonicalUrl:p(t.landing_page_url,3e3),sourceName:p(t.utm_source??t.source_name,180),medium:p(t.utm_medium??t.medium,180),campaign:p(t.utm_campaign??t.campaign,240),clickIds:o,properties:{answered:re(t.answered)??!1,durationSeconds:W(t.duration)??0,status:a?"spam":s||"unknown",tags:r,direction:p(t.direction,40)||"unknown",trackingNumber:p(t.tracking_phone_number,80)||null,changes:De(t.changes)}}}function Wn(e){return ge(e.requestTime,e.now)&&ye(e.signature.trim(),pe("sha1",e.secret,`${e.requestTime}${e.rawBody}`))}function Bn(e){let t=e.payload,n=p(t.activity_id??t.call_id??t.id,240)||"unknown",i=B(t.timestamp,t.event_time,t.started_at,t.created_at),r=De(t.tags??t.tag_list),s=p(t.status??t.disposition,80)||"unknown",a=s.toLowerCase().includes("spam")||r.some(o=>o.toLowerCase()==="spam");return{schemaVersion:1,eventId:`ctm_${n}_${e.phase}`,sourceEventId:`${n}:${e.phase}:${i}`,source:"calltrackingmetrics",sourceAccountRef:e.sourceAccountRef,eventKind:"call",eventName:e.phase.includes("start")?"call_started":e.phase.includes("answer")?"call_answered":"call_completed",occurredAt:i,callId:n,personRef:p(t.contact_id??t.visitor_id,240),visitorId:p(t.visitor_id??t.cookie_id,160),sessionId:p(t.session_id,160),canonicalUrl:p(t.landing_page_url??t.landing_url,3e3),sourceName:p(t.source??t.utm_source,180),medium:p(t.medium??t.utm_medium,180),campaign:p(t.campaign??t.utm_campaign,240),clickIds:{...p(t.gclid)?{gclid:p(t.gclid)}:{},...p(t.fbclid)?{fbclid:p(t.fbclid)}:{},...p(t.msclkid)?{msclkid:p(t.msclkid)}:{}},properties:{answered:re(t.answered)??e.phase.includes("answer"),durationSeconds:W(t.duration??t.talk_time)??0,status:a?"spam":s,tags:r,direction:p(t.direction,40)||"unknown",trackingNumber:p(t.tracking_number??t.dialed_number,80)||null,score:W(t.score)??null}}}import Or from"twilio";function Vn(e){return Or.validateRequest(e.authToken,e.signature,e.url,e.params)}function Kn(e){let t=e.params,n=p(t.CallSid,240)||"unknown",i=(p(t.CallStatus,80)||"unknown").toLowerCase(),r=B(t.Timestamp,t.CallTimestamp,t.StartTime),s=["completed","busy","failed","no-answer","canceled"].includes(i),a=["in-progress","answered","completed"].includes(i);return{schemaVersion:1,eventId:`twilio_${n}_${i}`,sourceEventId:`${n}:${i}`,source:"twilio",sourceAccountRef:e.sourceAccountRef,eventKind:"call",eventName:s?"call_completed":a?"call_answered":"call_started",occurredAt:r,callId:n,visitorId:p(t.XRayVisitorId,160),sessionId:p(t.XRaySessionId,160),personRef:p(t.ContactId,240),properties:{answered:re(t.Answered)??a,durationSeconds:W(t.CallDuration??t.Duration)??0,status:i,direction:p(t.Direction,40)||"unknown",trackingNumber:p(t.To,80)||null,attributionLink:t.XRaySessionId?"explicit_session":"unattributed"}}}import{createCipheriv as Dr,createDecipheriv as Tr,createHash as xr,createHmac as Lr}from"crypto";var Te={session:"xrs_",visitor:"xrv_",person:"xrp_",cursor:"xrc_",candidate:"xri_"};function wt(e){return xr("sha256").update(`xray-report:${e}:`).update(Me()).digest()}function O(e,t,n){let i=wt("reference"),r=Buffer.from(`v1:${e}:${t}`),s=Buffer.from(n),a=Lr("sha256",wt("reference-iv")).update(r).update(s).digest().subarray(0,12),o=Dr("aes-256-gcm",i,a);o.setAAD(r);let c=Buffer.concat([o.update(s),o.final()]);return`${Te[e]}${Buffer.concat([a,c,o.getAuthTag()]).toString("base64url")}`}function tt(e,t,n){let i=Te[e];if(!n.startsWith(i))throw new Error("analytics_reference_invalid");try{let r=Buffer.from(n.slice(i.length),"base64url");if(r.length<29)throw new Error("short reference");let s=r.subarray(0,12),a=r.subarray(r.length-16),o=r.subarray(12,r.length-16),c=Tr("aes-256-gcm",wt("reference"),s);return c.setAAD(Buffer.from(`v1:${e}:${t}`)),c.setAuthTag(a),Buffer.concat([c.update(o),c.final()]).toString("utf8")}catch{throw new Error("analytics_reference_invalid")}}function $r(e,t){return O("cursor",e,JSON.stringify(t))}function ve(e,t){if(!t)return null;try{let n=JSON.parse(tt("cursor",e,t));if(!n.at||!n.id||Number.isNaN(Date.parse(n.at)))throw new Error("invalid");return n}catch{throw new Error("analytics_cursor_invalid")}}function Ee(e){if(e===void 0)return 50;if(!Number.isInteger(e)||e<1||e>100)throw new Error("analytics_limit_invalid");return e}function Rt(e){if(typeof e!="string"||!e)return null;try{let t=new URL(e);return`${t.origin}${t.pathname}`.slice(0,2e3)}catch{return null}}function fe(e){if(e==null)return null;let t=Number(e);return Number.isFinite(t)?t:null}function S(e){let t=e instanceof Date?e:new Date(e);if(!Number.isFinite(t.getTime()))throw new Error("analytics_timestamp_invalid");return t.toISOString()}function Jn(e){return e===null?null:S(e)}function be(e,t,n,i){let r=t.length>n,s=t.slice(0,n),a=s.at(-1);return{items:s.map(i),pageInfo:{hasNextPage:r,nextCursor:r&&a?$r(e,{at:a._cursorAt,id:a._cursorId}):null}}}async function Ae(e,t){await U(t,e.siteId,e.userId)}async function yo(e,t=v()){await Ae(e,t);let n=Ee(e.limit),i=ve(e.siteId,e.cursor),s=(await t.query(`/* analyticsSessions:keyset */
|
|
290
|
+
WITH ranked AS (
|
|
291
|
+
SELECT s.*,
|
|
292
|
+
row_number() OVER (PARTITION BY s.visitor_hmac ORDER BY s.started_at, s.id)::int AS session_number
|
|
293
|
+
FROM analytics_sessions s
|
|
294
|
+
WHERE s.site_id = $1
|
|
295
|
+
), facts AS (
|
|
296
|
+
SELECT DISTINCT ON (e.session_hmac)
|
|
297
|
+
e.session_hmac, e.country_code, e.region_code, e.device_class,
|
|
298
|
+
e.source, e.medium, e.campaign
|
|
299
|
+
FROM analytics_events e
|
|
300
|
+
WHERE e.site_id = $1
|
|
301
|
+
ORDER BY e.session_hmac, e.occurred_at
|
|
302
|
+
), conversions AS (
|
|
303
|
+
SELECT c.session_hmac, count(*)::int AS conversion_count,
|
|
304
|
+
sum(c.value_minor)::bigint AS revenue_minor,
|
|
305
|
+
CASE WHEN count(DISTINCT c.currency) = 1 THEN min(c.currency) END AS currency
|
|
306
|
+
FROM analytics_conversions c WHERE c.site_id = $1 GROUP BY c.session_hmac
|
|
307
|
+
)
|
|
308
|
+
SELECT r.id AS internal_id, r.visitor_hmac, r.person_id, r.started_at, r.last_active_at,
|
|
309
|
+
r.ended_at, r.duration_ms, r.landing_path, r.exit_path, r.pageview_count,
|
|
310
|
+
r.event_count, r.session_number, f.country_code, f.region_code, f.device_class,
|
|
311
|
+
f.source, f.medium, f.campaign, COALESCE(c.conversion_count, 0)::int AS conversion_count,
|
|
312
|
+
c.revenue_minor, c.currency
|
|
313
|
+
FROM ranked r
|
|
314
|
+
LEFT JOIN facts f ON f.session_hmac = r.session_hmac
|
|
315
|
+
LEFT JOIN conversions c ON c.session_hmac = r.session_hmac
|
|
316
|
+
WHERE ($2::timestamptz IS NULL OR (r.started_at, r.id) < ($2::timestamptz, $3::uuid))
|
|
317
|
+
AND ($4::boolean IS NULL OR (r.session_number > 1) = $4)
|
|
318
|
+
AND ($5::boolean IS NULL OR (COALESCE(c.conversion_count, 0) > 0) = $5)
|
|
319
|
+
AND ($6::bigint IS NULL OR r.duration_ms >= $6)
|
|
320
|
+
AND ($7::bigint IS NULL OR r.duration_ms <= $7)
|
|
321
|
+
AND ($8::text IS NULL OR EXISTS (
|
|
322
|
+
SELECT 1 FROM analytics_events ee
|
|
323
|
+
WHERE ee.site_id = $1 AND ee.session_hmac = r.session_hmac AND ee.event_name = $8
|
|
324
|
+
))
|
|
325
|
+
ORDER BY r.started_at DESC, r.id DESC LIMIT $9`,[e.siteId,i?.at??null,i?.id??null,e.returning??null,e.converted??null,e.minimumDurationMs??null,e.maximumDurationMs??null,e.eventName?.slice(0,160)??null,n+1])).rows.map(a=>({...a,_cursorAt:S(a.started_at),_cursorId:a.internal_id}));return be(e.siteId,s,n,a=>({sessionRef:O("session",e.siteId,a.internal_id),visitorRef:O("visitor",e.siteId,`visitor:${a.visitor_hmac}`),personRef:a.person_id?O("person",e.siteId,a.person_id):null,returning:a.session_number>1,sessionNumber:a.session_number,startedAt:S(a.started_at),lastActiveAt:S(a.last_active_at),endedAt:Jn(a.ended_at),durationMs:fe(a.duration_ms),landingPath:a.landing_path,exitPath:a.exit_path,pages:a.pageview_count,events:a.event_count,device:a.device_class,location:{country:a.country_code,region:a.region_code},acquisition:{source:a.source,medium:a.medium,campaign:a.campaign},conversions:a.conversion_count,revenue:a.revenue_minor===null?null:{valueMinor:Number(a.revenue_minor),currency:a.currency}}))}async function go(e,t=v()){await Ae(e,t);let n=Ee(e.limit),i=ve(e.siteId,e.cursor),s=(await t.query(`/* analyticsVisitors:keyset */
|
|
326
|
+
WITH visitor_sessions AS (
|
|
327
|
+
SELECT COALESCE(s.person_id::text, s.visitor_hmac) AS identity_key, s.person_id,
|
|
328
|
+
min(s.started_at) AS first_seen_at, max(s.last_active_at) AS last_seen_at,
|
|
329
|
+
count(*)::int AS session_count, sum(s.duration_ms)::bigint AS duration_ms,
|
|
330
|
+
sum(s.pageview_count)::int AS pageview_count, sum(s.event_count)::int AS event_count,
|
|
331
|
+
(array_agg(s.id ORDER BY s.last_active_at DESC, s.id DESC))[1] AS last_session_id
|
|
332
|
+
FROM analytics_sessions s WHERE s.site_id = $1
|
|
333
|
+
GROUP BY COALESCE(s.person_id::text, s.visitor_hmac), s.person_id
|
|
334
|
+
), conversions AS (
|
|
335
|
+
SELECT COALESCE(c.person_id::text, s.visitor_hmac) AS identity_key,
|
|
336
|
+
count(*)::int AS conversion_count, sum(c.value_minor)::bigint AS revenue_minor,
|
|
337
|
+
CASE WHEN count(DISTINCT c.currency) = 1 THEN min(c.currency) END AS currency
|
|
338
|
+
FROM analytics_conversions c
|
|
339
|
+
LEFT JOIN analytics_sessions s ON s.site_id=c.site_id AND s.session_hmac=c.session_hmac
|
|
340
|
+
WHERE c.site_id=$1 GROUP BY COALESCE(c.person_id::text, s.visitor_hmac)
|
|
341
|
+
)
|
|
342
|
+
SELECT v.*, COALESCE(c.conversion_count,0)::int AS conversion_count, c.revenue_minor, c.currency
|
|
343
|
+
FROM visitor_sessions v LEFT JOIN conversions c USING(identity_key)
|
|
344
|
+
WHERE ($2::timestamptz IS NULL OR (v.last_seen_at, v.last_session_id) < ($2::timestamptz, $3::uuid))
|
|
345
|
+
AND ($4::boolean IS NULL OR (v.session_count > 1) = $4)
|
|
346
|
+
AND ($5::boolean IS NULL OR (COALESCE(c.conversion_count,0) > 0) = $5)
|
|
347
|
+
ORDER BY v.last_seen_at DESC, v.last_session_id DESC LIMIT $6`,[e.siteId,i?.at??null,i?.id??null,e.returning??null,e.converted??null,n+1])).rows.map(a=>({...a,_cursorAt:S(a.last_seen_at),_cursorId:a.last_session_id}));return be(e.siteId,s,n,a=>({visitorRef:O("visitor",e.siteId,a.person_id?`person:${a.person_id}`:`visitor:${a.identity_key}`),personRef:a.person_id?O("person",e.siteId,a.person_id):null,known:!!a.person_id,returning:a.session_count>1,sessions:a.session_count,firstSeenAt:S(a.first_seen_at),lastSeenAt:S(a.last_seen_at),durationMs:fe(a.duration_ms),pages:a.pageview_count,events:a.event_count,conversions:a.conversion_count,revenue:a.revenue_minor===null?null:{valueMinor:Number(a.revenue_minor),currency:a.currency}}))}async function Pr(e,t){if(t.startsWith(Te.person))return{kind:"person",personId:tt("person",e,t),visitorHmac:null,sessionId:null};if(t.startsWith(Te.visitor)){let n=tt("visitor",e,t);if(n.startsWith("person:"))return{kind:"visitor",personId:n.slice(7),visitorHmac:null,sessionId:null};if(n.startsWith("visitor:"))return{kind:"visitor",personId:null,visitorHmac:n.slice(8),sessionId:null};throw new Error("analytics_reference_invalid")}if(t.startsWith(Te.session))return{kind:"session",personId:null,visitorHmac:null,sessionId:tt("session",e,t)};throw new Error("analytics_reference_invalid")}var kr=2160*60*60*1e3,Mr=366*24*60*60*1e3;function Ur(e,t){let n=t===void 0?Date.now():Date.parse(t),i=e===void 0?n-kr:Date.parse(e);if(!Number.isFinite(i)||!Number.isFinite(n)||i>=n||n-i>Mr)throw new f("analytics_journey_date_range_invalid","Choose a valid from-inclusive, to-exclusive date range of at most 366 days.",400);return{from:new Date(i).toISOString(),to:new Date(n).toISOString()}}async function fo(e,t=v()){await Ae(e,t);let n=Ee(e.limit),i=ve(e.siteId,e.cursor),r;try{r=await Pr(e.siteId,e.reference)}catch{throw new f("analytics_journey_not_found","Journey not found.",404)}if(!(await t.query(`/* analyticsEnhancedJourney:subject */
|
|
348
|
+
SELECT 1 WHERE
|
|
349
|
+
($2::uuid IS NOT NULL AND EXISTS (SELECT 1 FROM analytics_people p WHERE p.site_id=$1 AND p.id=$2))
|
|
350
|
+
OR ($3::text IS NOT NULL AND EXISTS (SELECT 1 FROM analytics_sessions s WHERE s.site_id=$1 AND s.visitor_hmac=$3))
|
|
351
|
+
OR ($4::uuid IS NOT NULL AND EXISTS (SELECT 1 FROM analytics_sessions s WHERE s.site_id=$1 AND s.id=$4))
|
|
352
|
+
LIMIT 1`,[e.siteId,r.personId,r.visitorHmac,r.sessionId])).rowCount)throw new f("analytics_journey_not_found","Journey not found.",404);let a=Ur(e.from,e.to),o=[e.siteId,r.personId,r.visitorHmac,r.sessionId,a.from,a.to],c=`
|
|
353
|
+
SELECT s.id, s.session_hmac, s.visitor_hmac, s.person_id, s.started_at,
|
|
354
|
+
s.last_active_at, s.ended_at, s.duration_ms, s.landing_path, s.exit_path
|
|
355
|
+
FROM analytics_sessions s
|
|
356
|
+
WHERE s.site_id=$1
|
|
357
|
+
AND ($2::uuid IS NULL OR s.person_id=$2)
|
|
358
|
+
AND ($3::text IS NULL OR s.visitor_hmac=$3 OR s.person_id=(SELECT person_id FROM analytics_sessions WHERE site_id=$1 AND visitor_hmac=$3 AND person_id IS NOT NULL LIMIT 1))
|
|
359
|
+
AND ($4::uuid IS NULL OR s.id=$4)`,[d,_,u,y,E,b]=await Promise.all([t.query(`/* analyticsSessionTimeline:keyset */
|
|
360
|
+
WITH subject_sessions AS (
|
|
361
|
+
${c}
|
|
362
|
+
), entries AS (
|
|
363
|
+
SELECT s.started_at AS occurred_at, s.id::text||':start' AS stable_id,
|
|
364
|
+
'session' AS entry_type, 'session_started' AS name, s.landing_path AS path,
|
|
365
|
+
NULL::text AS referrer, NULL::text AS source, NULL::text AS medium,
|
|
366
|
+
NULL::text AS campaign, NULL::bigint AS value_minor, NULL::text AS currency,
|
|
367
|
+
'deterministic'::text AS evidence_class, 'persisted_session_bounds'::text AS provenance
|
|
368
|
+
FROM subject_sessions s
|
|
369
|
+
UNION ALL
|
|
370
|
+
SELECT COALESCE(s.ended_at,s.last_active_at), s.id::text||':end', 'session', 'session_ended',
|
|
371
|
+
s.exit_path, NULL, NULL, NULL, NULL, NULL, NULL, 'deterministic', 'persisted_session_bounds'
|
|
372
|
+
FROM analytics_sessions s JOIN subject_sessions ss ON ss.id=s.id
|
|
373
|
+
UNION ALL
|
|
374
|
+
SELECT e.occurred_at, e.id::text AS stable_id,
|
|
375
|
+
CASE WHEN e.event_name='page_view' THEN 'page' ELSE 'event' END AS entry_type,
|
|
376
|
+
e.event_name AS name, e.path, e.referrer, e.source, e.medium, e.campaign,
|
|
377
|
+
NULL::bigint AS value_minor, NULL::text AS currency,
|
|
378
|
+
'deterministic'::text AS evidence_class, 'pixel_event'::text AS provenance
|
|
379
|
+
FROM analytics_events e JOIN subject_sessions s ON s.session_hmac=e.session_hmac AND e.site_id=$1
|
|
380
|
+
UNION ALL
|
|
381
|
+
SELECT t.occurred_at, t.id::text, 'touch', COALESCE(t.touch_kind,'touch'), t.landing_path,
|
|
382
|
+
t.referrer, t.source, t.medium, t.campaign, NULL, NULL, 'deterministic', 'attribution_touch'
|
|
383
|
+
FROM analytics_attribution_touches t JOIN subject_sessions s ON s.id=t.session_id AND t.site_id=$1
|
|
384
|
+
UNION ALL
|
|
385
|
+
SELECT x.occurred_at, x.id::text, x.event_kind, x.event_name, NULL, NULL, x.source, NULL, NULL,
|
|
386
|
+
x.value_minor, x.currency, 'deterministic', 'verified_external_event'
|
|
387
|
+
FROM analytics_external_events x
|
|
388
|
+
WHERE x.site_id=$1 AND (x.person_id=$2 OR x.session_hmac IN (SELECT session_hmac FROM subject_sessions))
|
|
389
|
+
UNION ALL
|
|
390
|
+
SELECT c.occurred_at, c.id::text, 'conversion', c.conversion_kind, NULL, NULL, NULL, NULL, NULL,
|
|
391
|
+
c.value_minor, c.currency, 'deterministic', COALESCE(c.value_source,'conversion')
|
|
392
|
+
FROM analytics_conversions c
|
|
393
|
+
WHERE c.site_id=$1 AND (c.person_id=$2 OR c.session_hmac IN (SELECT session_hmac FROM subject_sessions))
|
|
394
|
+
UNION ALL
|
|
395
|
+
SELECT e.occurred_at, ve.id::text, 'value', e.event_name, e.path, NULL, e.source, e.medium,
|
|
396
|
+
e.campaign, ve.value_minor, ve.currency, 'deterministic', ve.value_source
|
|
397
|
+
FROM analytics_event_value_evaluations ve
|
|
398
|
+
JOIN analytics_events e ON e.id=ve.source_event_id AND e.site_id=ve.site_id
|
|
399
|
+
JOIN subject_sessions s ON s.session_hmac=e.session_hmac
|
|
400
|
+
WHERE ve.site_id=$1
|
|
401
|
+
UNION ALL
|
|
402
|
+
SELECT se.evaluated_at, se.id::text, 'score', sd.name, NULL, NULL, NULL, NULL, NULL,
|
|
403
|
+
se.score::bigint, NULL, 'deterministic', 'lead_score:'||sd.score_kind
|
|
404
|
+
FROM analytics_score_evaluations se JOIN analytics_score_definitions sd ON sd.id=se.definition_id
|
|
405
|
+
WHERE se.site_id=$1 AND se.person_id=$2
|
|
406
|
+
UNION ALL
|
|
407
|
+
SELECT j.created_at, j.id::text, 'receipt', j.status, NULL, NULL, d.platform, NULL, NULL,
|
|
408
|
+
NULL, NULL, 'deterministic', COALESCE(j.response_category,'delivery_receipt')
|
|
409
|
+
FROM analytics_activation_jobs j JOIN analytics_activation_destinations d ON d.id=j.destination_id
|
|
410
|
+
WHERE d.site_id=$1 AND j.person_id=$2
|
|
411
|
+
UNION ALL
|
|
412
|
+
SELECT ca.promoted_at, ca.id::text, 'identity_transition', 'candidate_promoted_to_confirmed', NULL, NULL,
|
|
413
|
+
NULL, NULL, NULL, NULL, NULL, 'deterministic', 'later_deterministic_evidence'
|
|
414
|
+
FROM analytics_candidate_associations ca
|
|
415
|
+
WHERE ca.site_id=$1 AND ca.promoted_person_id=$2 AND ca.status='promoted'
|
|
416
|
+
)
|
|
417
|
+
SELECT * FROM entries
|
|
418
|
+
WHERE occurred_at IS NOT NULL
|
|
419
|
+
AND occurred_at >= $5::timestamptz AND occurred_at < $6::timestamptz
|
|
420
|
+
AND ($7::timestamptz IS NULL OR (occurred_at, stable_id) > ($7::timestamptz, $8::text))
|
|
421
|
+
ORDER BY occurred_at, stable_id LIMIT $9`,[...o,i?.at??null,i?.id??null,n+1]),t.query(`/* analyticsEnhancedJourney:summary */
|
|
422
|
+
WITH subject_sessions AS (${c}), ranged_sessions AS (
|
|
423
|
+
SELECT * FROM subject_sessions WHERE started_at >= $5::timestamptz AND started_at < $6::timestamptz
|
|
424
|
+
), ranged_events AS (
|
|
425
|
+
SELECT e.* FROM analytics_events e JOIN subject_sessions s ON s.session_hmac=e.session_hmac
|
|
426
|
+
WHERE e.site_id=$1 AND e.occurred_at >= $5::timestamptz AND e.occurred_at < $6::timestamptz
|
|
427
|
+
), ranged_conversions AS (
|
|
428
|
+
SELECT c.* FROM analytics_conversions c
|
|
429
|
+
WHERE c.site_id=$1 AND c.occurred_at >= $5::timestamptz AND c.occurred_at < $6::timestamptz
|
|
430
|
+
AND (c.person_id=$2 OR c.session_hmac IN (SELECT session_hmac FROM subject_sessions))
|
|
431
|
+
)
|
|
432
|
+
SELECT (SELECT (array_agg(person_id ORDER BY started_at) FILTER (WHERE person_id IS NOT NULL))[1]::text FROM subject_sessions) AS resolved_person_id,
|
|
433
|
+
(SELECT count(*)::int FROM ranged_sessions) AS sessions,
|
|
434
|
+
(SELECT count(*)::int FROM ranged_events WHERE event_name='page_view') AS pageviews,
|
|
435
|
+
(SELECT count(*)::int FROM ranged_events) AS events,
|
|
436
|
+
(SELECT count(*)::int FROM ranged_conversions) AS conversions,
|
|
437
|
+
(SELECT sum(value_minor)::bigint FROM ranged_conversions) AS revenue_minor,
|
|
438
|
+
(SELECT CASE WHEN count(DISTINCT currency)=1 THEN min(currency) END FROM ranged_conversions WHERE value_minor IS NOT NULL) AS currency,
|
|
439
|
+
(SELECT sum(engaged_ms)::bigint FROM ranged_events WHERE event_name='page_engagement' AND engaged_ms IS NOT NULL) AS total_engagement_ms,
|
|
440
|
+
(SELECT round(avg(engaged_ms))::bigint FROM ranged_events WHERE event_name='page_engagement' AND engaged_ms IS NOT NULL) AS average_engagement_ms`,[...o]),t.query(`/* analyticsEnhancedJourney:pages */
|
|
441
|
+
WITH subject_sessions AS (${c}), pageviews AS (
|
|
442
|
+
SELECT e.path, e.session_id, count(*)::int AS visits
|
|
443
|
+
FROM analytics_events e JOIN subject_sessions s ON s.session_hmac=e.session_hmac
|
|
444
|
+
WHERE e.site_id=$1 AND e.event_name='page_view' AND e.path IS NOT NULL
|
|
445
|
+
AND e.occurred_at >= $5::timestamptz AND e.occurred_at < $6::timestamptz
|
|
446
|
+
GROUP BY e.path,e.session_id
|
|
447
|
+
), engagement AS (
|
|
448
|
+
SELECT e.path, sum(e.engaged_ms)::bigint AS engaged_ms
|
|
449
|
+
FROM analytics_events e JOIN subject_sessions s ON s.session_hmac=e.session_hmac
|
|
450
|
+
WHERE e.site_id=$1 AND e.event_name='page_engagement' AND e.path IS NOT NULL AND e.engaged_ms IS NOT NULL
|
|
451
|
+
AND e.occurred_at >= $5::timestamptz AND e.occurred_at < $6::timestamptz GROUP BY e.path
|
|
452
|
+
), conversion_pages AS (
|
|
453
|
+
SELECT latest.path, count(*)::int AS conversions FROM analytics_conversions c
|
|
454
|
+
JOIN LATERAL (
|
|
455
|
+
SELECT e.path FROM analytics_events e
|
|
456
|
+
WHERE e.site_id=$1 AND e.session_id=c.session_id AND e.event_name='page_view'
|
|
457
|
+
AND e.occurred_at <= c.occurred_at ORDER BY e.occurred_at DESC,e.id DESC LIMIT 1
|
|
458
|
+
) latest ON true
|
|
459
|
+
WHERE c.site_id=$1 AND c.occurred_at >= $5::timestamptz AND c.occurred_at < $6::timestamptz
|
|
460
|
+
AND (c.person_id=$2 OR c.session_hmac IN (SELECT session_hmac FROM subject_sessions))
|
|
461
|
+
GROUP BY latest.path
|
|
462
|
+
)
|
|
463
|
+
SELECT p.path,sum(p.visits)::int AS visits,count(*)::int AS unique_sessions,e.engaged_ms,
|
|
464
|
+
COALESCE(c.conversions,0)::int AS conversions
|
|
465
|
+
FROM pageviews p LEFT JOIN engagement e USING(path) LEFT JOIN conversion_pages c USING(path)
|
|
466
|
+
GROUP BY p.path,e.engaged_ms,c.conversions ORDER BY visits DESC,p.path LIMIT 20`,[...o]),t.query(`/* analyticsEnhancedJourney:acquisition */
|
|
467
|
+
WITH subject_sessions AS (${c}), first_touch AS (
|
|
468
|
+
SELECT DISTINCT ON (e.session_hmac) e.session_hmac,e.source,e.medium,e.campaign,e.referrer
|
|
469
|
+
FROM analytics_events e JOIN subject_sessions s ON s.session_hmac=e.session_hmac
|
|
470
|
+
WHERE e.site_id=$1 AND e.occurred_at >= $5::timestamptz AND e.occurred_at < $6::timestamptz
|
|
471
|
+
ORDER BY e.session_hmac,e.occurred_at,e.id
|
|
472
|
+
), event_counts AS (
|
|
473
|
+
SELECT e.session_hmac,count(*)::int AS events FROM analytics_events e JOIN subject_sessions s ON s.session_hmac=e.session_hmac
|
|
474
|
+
WHERE e.site_id=$1 AND e.occurred_at >= $5::timestamptz AND e.occurred_at < $6::timestamptz GROUP BY e.session_hmac
|
|
475
|
+
), conversion_counts AS (
|
|
476
|
+
SELECT c.session_hmac,count(*)::int AS conversions FROM analytics_conversions c
|
|
477
|
+
WHERE c.site_id=$1 AND c.occurred_at >= $5::timestamptz AND c.occurred_at < $6::timestamptz
|
|
478
|
+
AND (c.person_id=$2 OR c.session_hmac IN (SELECT session_hmac FROM subject_sessions)) GROUP BY c.session_hmac
|
|
479
|
+
)
|
|
480
|
+
SELECT f.source,f.medium,f.campaign,f.referrer,count(*)::int AS sessions,
|
|
481
|
+
sum(COALESCE(e.events,0))::int AS events,sum(COALESCE(c.conversions,0))::int AS conversions
|
|
482
|
+
FROM first_touch f LEFT JOIN event_counts e USING(session_hmac) LEFT JOIN conversion_counts c USING(session_hmac)
|
|
483
|
+
GROUP BY f.source,f.medium,f.campaign,f.referrer ORDER BY sessions DESC,f.source NULLS LAST LIMIT 20`,[...o]),t.query(`/* analyticsEnhancedJourney:events */
|
|
484
|
+
WITH subject_sessions AS (${c}), event_groups AS (
|
|
485
|
+
SELECT e.event_name AS name,count(*)::int AS count,count(DISTINCT e.session_id)::int AS unique_sessions,
|
|
486
|
+
array_agg(DISTINCT e.session_id) AS session_ids
|
|
487
|
+
FROM analytics_events e JOIN subject_sessions s ON s.session_hmac=e.session_hmac
|
|
488
|
+
WHERE e.site_id=$1 AND e.occurred_at >= $5::timestamptz AND e.occurred_at < $6::timestamptz
|
|
489
|
+
GROUP BY e.event_name
|
|
490
|
+
)
|
|
491
|
+
SELECT g.name,g.count,g.unique_sessions,
|
|
492
|
+
(SELECT count(*)::int FROM analytics_conversions c WHERE c.site_id=$1
|
|
493
|
+
AND c.occurred_at >= $5::timestamptz AND c.occurred_at < $6::timestamptz
|
|
494
|
+
AND c.session_id=ANY(g.session_ids)) AS conversions
|
|
495
|
+
FROM event_groups g ORDER BY g.count DESC,g.name LIMIT 50`,[...o]),t.query(`/* analyticsEnhancedJourney:sessions */
|
|
496
|
+
WITH subject_sessions AS (${c}), ranged_sessions AS (
|
|
497
|
+
SELECT * FROM subject_sessions WHERE started_at >= $5::timestamptz AND started_at < $6::timestamptz
|
|
498
|
+
), event_stats AS (
|
|
499
|
+
SELECT e.session_hmac,count(*) FILTER (WHERE e.event_name='page_view')::int AS pageviews,count(*)::int AS events
|
|
500
|
+
FROM analytics_events e JOIN ranged_sessions s ON s.session_hmac=e.session_hmac
|
|
501
|
+
WHERE e.site_id=$1 AND e.occurred_at >= $5::timestamptz AND e.occurred_at < $6::timestamptz GROUP BY e.session_hmac
|
|
502
|
+
), first_touch AS (
|
|
503
|
+
SELECT DISTINCT ON (e.session_hmac) e.session_hmac,e.source,e.medium,e.campaign,e.referrer
|
|
504
|
+
FROM analytics_events e JOIN ranged_sessions s ON s.session_hmac=e.session_hmac
|
|
505
|
+
WHERE e.site_id=$1 ORDER BY e.session_hmac,e.occurred_at,e.id
|
|
506
|
+
), conversion_stats AS (
|
|
507
|
+
SELECT c.session_hmac,count(*)::int AS conversions,sum(c.value_minor)::bigint AS revenue_minor,
|
|
508
|
+
CASE WHEN count(DISTINCT c.currency)=1 THEN min(c.currency) END AS currency
|
|
509
|
+
FROM analytics_conversions c JOIN ranged_sessions s ON s.session_hmac=c.session_hmac
|
|
510
|
+
WHERE c.site_id=$1 AND c.occurred_at >= $5::timestamptz AND c.occurred_at < $6::timestamptz GROUP BY c.session_hmac
|
|
511
|
+
)
|
|
512
|
+
SELECT s.id AS internal_id,s.started_at,s.ended_at,s.duration_ms,s.landing_path,s.exit_path,
|
|
513
|
+
COALESCE(e.pageviews,0)::int AS pageviews,COALESCE(e.events,0)::int AS events,
|
|
514
|
+
COALESCE(c.conversions,0)::int AS conversions,c.revenue_minor,c.currency,
|
|
515
|
+
f.source,f.medium,f.campaign,f.referrer
|
|
516
|
+
FROM ranged_sessions s LEFT JOIN event_stats e ON e.session_hmac=s.session_hmac
|
|
517
|
+
LEFT JOIN first_touch f ON f.session_hmac=s.session_hmac LEFT JOIN conversion_stats c ON c.session_hmac=s.session_hmac
|
|
518
|
+
ORDER BY s.started_at DESC,s.id DESC LIMIT 50`,[...o])]),V=d.rows.map(m=>({...m,_cursorAt:S(m.occurred_at),_cursorId:m.stable_id})),P=be(e.siteId,V,n,m=>({occurredAt:S(m.occurred_at),type:m.entry_type,name:m.name,path:m.path,referrer:Rt(m.referrer),acquisition:m.source||m.medium||m.campaign?{source:m.source,medium:m.medium,campaign:m.campaign}:null,value:m.value_minor===null?null:{valueMinor:Number(m.value_minor),currency:m.currency},evidenceClass:m.evidence_class,provenance:m.provenance})),h=_.rows[0]??{resolved_person_id:null,sessions:0,pageviews:0,events:0,conversions:0,revenue_minor:null,currency:null,total_engagement_ms:null,average_engagement_ms:null},T=h.resolved_person_id??r.personId;return{subject:{kind:T?"person":r.kind,reference:e.reference,personRef:T?O("person",e.siteId,T):null},range:{...a,fromInclusive:!0,toExclusive:!0},journeyTier:"confirmed",candidateAssisted:null,summary:{sessions:Number(h.sessions),pageviews:Number(h.pageviews),events:Number(h.events),conversions:Number(h.conversions),revenue:h.revenue_minor===null?null:{valueMinor:Number(h.revenue_minor),currency:h.currency},totalEngagementMs:fe(h.total_engagement_ms),averageEngagementMs:fe(h.average_engagement_ms)},commonPages:u.rows.map(m=>({path:m.path,visits:Number(m.visits),uniqueSessions:Number(m.unique_sessions),engagedMs:fe(m.engaged_ms),conversions:Number(m.conversions)})),acquisition:y.rows.map(m=>({source:m.source,medium:m.medium,campaign:m.campaign,referrer:Rt(m.referrer),sessions:Number(m.sessions),events:Number(m.events),conversions:Number(m.conversions)})),eventBreakdown:E.rows.map(m=>({name:m.name,count:Number(m.count),uniqueSessions:Number(m.unique_sessions),conversions:Number(m.conversions)})),sessions:b.rows.map(m=>({sessionRef:O("session",e.siteId,m.internal_id),startedAt:S(m.started_at),endedAt:Jn(m.ended_at),durationMs:fe(m.duration_ms),pageviews:Number(m.pageviews),events:Number(m.events),conversions:Number(m.conversions),revenue:m.revenue_minor===null?null:{valueMinor:Number(m.revenue_minor),currency:m.currency},landingPath:m.landing_path,exitPath:m.exit_path,acquisition:m.source||m.medium||m.campaign||m.referrer?{source:m.source,medium:m.medium,campaign:m.campaign,referrer:Rt(m.referrer)}:null})),...P}}async function vo(e,t=v()){await Ae(e,t);let n=Ee(e.limit),i=ve(e.siteId,e.cursor),s=(await t.query(`/* analyticsConfirmedJourneys:keyset */
|
|
519
|
+
SELECT p.id AS person_id, p.namespace_id, sessions.first_seen_at,
|
|
520
|
+
sessions.last_seen_at, COALESCE(sessions.session_count,0)::int AS session_count,
|
|
521
|
+
COALESCE(events.event_count,0)::int AS event_count,
|
|
522
|
+
COALESCE(conversions.conversion_count,0)::int AS conversion_count,
|
|
523
|
+
conversions.revenue_minor, conversions.currency
|
|
524
|
+
FROM analytics_people p
|
|
525
|
+
JOIN analytics_identity_namespaces n ON n.id=p.namespace_id AND n.site_id=p.site_id
|
|
526
|
+
LEFT JOIN LATERAL (
|
|
527
|
+
SELECT min(s.started_at) AS first_seen_at, max(s.last_active_at) AS last_seen_at,
|
|
528
|
+
count(*)::int AS session_count
|
|
529
|
+
FROM analytics_sessions s WHERE s.site_id=p.site_id AND s.person_id=p.id
|
|
530
|
+
) sessions ON true
|
|
531
|
+
LEFT JOIN LATERAL (
|
|
532
|
+
SELECT count(*)::int AS event_count
|
|
533
|
+
FROM analytics_events e WHERE e.site_id=p.site_id AND e.person_id=p.id
|
|
534
|
+
) events ON true
|
|
535
|
+
LEFT JOIN LATERAL (
|
|
536
|
+
SELECT count(*)::int AS conversion_count, sum(c.value_minor)::bigint AS revenue_minor,
|
|
537
|
+
CASE WHEN count(DISTINCT c.currency)=1 THEN min(c.currency) END AS currency
|
|
538
|
+
FROM analytics_conversions c WHERE c.site_id=p.site_id AND c.person_id=p.id
|
|
539
|
+
) conversions ON true
|
|
540
|
+
WHERE p.site_id=$1 AND n.status='approved'
|
|
541
|
+
AND ($2::timestamptz IS NULL OR (sessions.last_seen_at,p.id) < ($2::timestamptz,$3::uuid))
|
|
542
|
+
ORDER BY last_seen_at DESC, p.id DESC LIMIT $4`,[e.siteId,i?.at??null,i?.id??null,n+1])).rows.map(o=>({...o,_cursorAt:S(o.last_seen_at),_cursorId:o.person_id}));return{journeyTier:"confirmed",...be(e.siteId,s,n,o=>({journeyTier:"confirmed",personRef:O("person",e.siteId,o.person_id),firstSeenAt:S(o.first_seen_at),lastSeenAt:S(o.last_seen_at),sessions:o.session_count,events:o.event_count,conversions:o.conversion_count,revenue:o.revenue_minor===null?null:{valueMinor:Number(o.revenue_minor),currency:o.currency},evidenceClass:"deterministic"}))}}async function Eo(e,t=v()){await Ae(e,t);let n=Ee(e.limit),i=ve(e.siteId,e.cursor),s=(await t.query(`/* analyticsBestGuessJourneys:keyset */
|
|
543
|
+
SELECT ca.id AS association_id, ca.namespace_id, ca.model_version, ca.expires_at, ca.updated_at,
|
|
544
|
+
ca.left_identity_node_id AS left_node_id, ca.right_identity_node_id AS right_node_id,
|
|
545
|
+
COALESCE(stats.confirmed_sessions,0)::int AS confirmed_sessions,
|
|
546
|
+
COALESCE(stats.inferred_sessions,0)::int AS inferred_sessions,
|
|
547
|
+
COALESCE(stats.confirmed_events,0)::int AS confirmed_events,
|
|
548
|
+
COALESCE(stats.inferred_events,0)::int AS inferred_events,
|
|
549
|
+
COALESCE(stats.confirmed_conversions,0)::int AS confirmed_conversions,
|
|
550
|
+
COALESCE(stats.inferred_conversions,0)::int AS inferred_conversions,
|
|
551
|
+
stats.confirmed_revenue_minor, stats.inferred_revenue_minor, stats.currency,
|
|
552
|
+
COALESCE(ev.feature_kinds, ARRAY[]::text[]) AS feature_kinds
|
|
553
|
+
FROM analytics_candidate_associations ca
|
|
554
|
+
JOIN analytics_identity_namespaces n ON n.id=ca.namespace_id AND n.site_id=ca.site_id
|
|
555
|
+
LEFT JOIN LATERAL (
|
|
556
|
+
SELECT array_agg(DISTINCT ce.feature_kind ORDER BY ce.feature_kind) AS feature_kinds
|
|
557
|
+
FROM analytics_candidate_evidence ce
|
|
558
|
+
WHERE ce.association_id=ca.id AND ce.namespace_id=ca.namespace_id AND ce.expires_at > now()
|
|
559
|
+
) ev ON true
|
|
560
|
+
LEFT JOIN LATERAL (
|
|
561
|
+
SELECT
|
|
562
|
+
sum(node_stats.session_count) FILTER (WHERE node_stats.node_id=ca.left_identity_node_id)::int AS confirmed_sessions,
|
|
563
|
+
sum(node_stats.session_count) FILTER (WHERE node_stats.node_id=ca.right_identity_node_id)::int AS inferred_sessions,
|
|
564
|
+
sum(node_stats.event_count) FILTER (WHERE node_stats.node_id=ca.left_identity_node_id)::int AS confirmed_events,
|
|
565
|
+
sum(node_stats.event_count) FILTER (WHERE node_stats.node_id=ca.right_identity_node_id)::int AS inferred_events,
|
|
566
|
+
sum(node_stats.conversion_count) FILTER (WHERE node_stats.node_id=ca.left_identity_node_id)::int AS confirmed_conversions,
|
|
567
|
+
sum(node_stats.conversion_count) FILTER (WHERE node_stats.node_id=ca.right_identity_node_id)::int AS inferred_conversions,
|
|
568
|
+
sum(node_stats.revenue_minor) FILTER (WHERE node_stats.node_id=ca.left_identity_node_id)::bigint AS confirmed_revenue_minor,
|
|
569
|
+
sum(node_stats.revenue_minor) FILTER (WHERE node_stats.node_id=ca.right_identity_node_id)::bigint AS inferred_revenue_minor,
|
|
570
|
+
CASE WHEN count(DISTINCT node_stats.currency)=1 THEN min(node_stats.currency) END AS currency
|
|
571
|
+
FROM (
|
|
572
|
+
SELECT inode.id AS node_id,
|
|
573
|
+
(SELECT count(*) FROM analytics_sessions s
|
|
574
|
+
WHERE s.site_id=ca.site_id AND s.visitor_hmac=inode.value_hmac)::int AS session_count,
|
|
575
|
+
(SELECT count(*) FROM analytics_events e JOIN analytics_sessions s
|
|
576
|
+
ON s.site_id=e.site_id AND s.session_hmac=e.session_hmac
|
|
577
|
+
WHERE e.site_id=ca.site_id AND s.visitor_hmac=inode.value_hmac)::int AS event_count,
|
|
578
|
+
(SELECT count(*) FROM analytics_conversions c JOIN analytics_sessions s
|
|
579
|
+
ON s.site_id=c.site_id AND s.session_hmac=c.session_hmac
|
|
580
|
+
WHERE c.site_id=ca.site_id AND s.visitor_hmac=inode.value_hmac)::int AS conversion_count,
|
|
581
|
+
(SELECT sum(c.value_minor) FROM analytics_conversions c JOIN analytics_sessions s
|
|
582
|
+
ON s.site_id=c.site_id AND s.session_hmac=c.session_hmac
|
|
583
|
+
WHERE c.site_id=ca.site_id AND s.visitor_hmac=inode.value_hmac)::bigint AS revenue_minor,
|
|
584
|
+
(SELECT CASE WHEN count(DISTINCT c.currency)=1 THEN min(c.currency) END
|
|
585
|
+
FROM analytics_conversions c JOIN analytics_sessions s
|
|
586
|
+
ON s.site_id=c.site_id AND s.session_hmac=c.session_hmac
|
|
587
|
+
WHERE c.site_id=ca.site_id AND s.visitor_hmac=inode.value_hmac) AS currency
|
|
588
|
+
FROM analytics_identity_nodes inode
|
|
589
|
+
WHERE inode.site_id=ca.site_id AND inode.namespace_id=ca.namespace_id
|
|
590
|
+
AND inode.id IN (ca.left_identity_node_id,ca.right_identity_node_id)
|
|
591
|
+
) node_stats
|
|
592
|
+
) stats ON true
|
|
593
|
+
WHERE ca.site_id=$1 AND n.status='approved' AND ca.status='active'
|
|
594
|
+
AND ca.confidence_band='high' AND ca.expires_at > now()
|
|
595
|
+
AND ($2::timestamptz IS NULL OR (ca.updated_at,ca.id) < ($2::timestamptz,$3::uuid))
|
|
596
|
+
ORDER BY ca.updated_at DESC, ca.id DESC LIMIT $4`,[e.siteId,i?.at??null,i?.id??null,n+1])).rows.map(o=>({...o,_cursorAt:S(o.updated_at),_cursorId:o.association_id}));return{journeyTier:"candidate_assisted",totalsAreSeparated:!0,...be(e.siteId,s,n,o=>({journeyTier:"candidate_assisted",inferenceRef:O("candidate",e.siteId,o.association_id),confirmedTotals:{sessions:o.confirmed_sessions,events:o.confirmed_events,conversions:o.confirmed_conversions,revenue:o.confirmed_revenue_minor===null?null:{valueMinor:Number(o.confirmed_revenue_minor),currency:o.currency}},inferredIncrementalTotals:{sessions:o.inferred_sessions,events:o.inferred_events,conversions:o.inferred_conversions,revenue:o.inferred_revenue_minor===null?null:{valueMinor:Number(o.inferred_revenue_minor),currency:o.currency}},candidateCount:1,confidenceBand:"high",modelVersion:o.model_version,featureClasses:o.feature_kinds,coverage:o.inferred_sessions>0?"incremental_sessions_available":"no_incremental_sessions",expiresAt:S(o.expires_at),provenance:"consented_high_confidence_candidate_association"}))}}async function bo(e,t=v()){await Ae(e,t);let n=Ee(e.limit),i=ve(e.siteId,e.cursor),s=(await t.query(`/* analyticsInferenceReview:keyset */
|
|
597
|
+
SELECT ca.id AS association_id, ca.updated_at, ca.first_observed_at, ca.last_observed_at,
|
|
598
|
+
ca.expires_at, ca.model_version, ca.confidence_band,
|
|
599
|
+
COALESCE(array_agg(DISTINCT ce.feature_kind ORDER BY ce.feature_kind)
|
|
600
|
+
FILTER (WHERE ce.id IS NOT NULL), ARRAY[]::text[]) AS feature_kinds
|
|
601
|
+
FROM analytics_candidate_associations ca
|
|
602
|
+
JOIN analytics_identity_namespaces n ON n.id=ca.namespace_id AND n.site_id=ca.site_id
|
|
603
|
+
LEFT JOIN analytics_candidate_evidence ce ON ce.association_id=ca.id
|
|
604
|
+
AND ce.namespace_id=ca.namespace_id AND ce.expires_at > now()
|
|
605
|
+
WHERE ca.site_id=$1 AND n.status='approved' AND ca.status='active'
|
|
606
|
+
AND ca.confidence_band='medium' AND ca.expires_at > now()
|
|
607
|
+
AND ($2::timestamptz IS NULL OR (ca.updated_at,ca.id) < ($2::timestamptz,$3::uuid))
|
|
608
|
+
GROUP BY ca.id
|
|
609
|
+
ORDER BY ca.updated_at DESC, ca.id DESC LIMIT $4`,[e.siteId,i?.at??null,i?.id??null,n+1])).rows.map(a=>({...a,_cursorAt:S(a.updated_at),_cursorId:a.association_id}));return be(e.siteId,s,n,a=>({inferenceRef:O("candidate",e.siteId,a.association_id),confidenceBand:a.confidence_band,modelVersion:a.model_version,featureClasses:a.feature_kinds,firstObservedAt:S(a.first_observed_at),lastObservedAt:S(a.last_observed_at),expiresAt:S(a.expires_at),action:"diagnostic_only",eligibleForConfirmedMetrics:!1,eligibleForCrmOrActivation:!1}))}import{createHash as Fr,createVerify as jr,verify as Hr}from"crypto";function zn(e){if(!ge(e.timestamp,e.now))return!1;let t=`${e.method.toUpperCase()}${e.url}${e.rawBody}${e.timestamp}`;return ye(e.signature,pe("sha256",e.clientSecret,t))}var qr=`-----BEGIN PUBLIC KEY-----
|
|
610
|
+
MCowBQYDK2VwAyEAi2HR1srL4o18O8BRa7gVJY7G7bupbN3H9AwJrHCDiOg=
|
|
611
|
+
-----END PUBLIC KEY-----`;function Yn(e){try{if(e.ghlSignature&&e.ghlSignature!=="N/A")return Hr(null,Buffer.from(e.rawBody,"utf8"),e.ed25519PublicKey||qr,Buffer.from(e.ghlSignature,"base64"));if(e.legacySignature&&e.legacySignature!=="N/A"&&e.legacyRsaPublicKey){let t=jr("SHA256");return t.update(e.rawBody),t.verify(e.legacyRsaPublicKey,e.legacySignature,"base64")}return!1}catch{return!1}}function Ot(e){return{schemaVersion:1,eventId:`crm_${Fr("sha256").update(`${e.source}:${e.sourceEventId}`).digest("hex").slice(0,24)}`,sourceEventId:e.sourceEventId,source:e.source,sourceAccountRef:e.sourceAccountRef,eventKind:"crm",eventName:"crm_stage_changed",occurredAt:e.occurredAt,visitorId:e.visitorId,sessionId:e.sessionId,personRef:e.contactId,dealId:e.dealId,orderId:e.orderId,valueMinor:e.valueMinor,currency:e.currency,clickIds:e.clickIds,properties:{pipelineId:e.pipelineId||null,stageId:e.stageId,stageName:e.stageName||null,status:e.status}}}function Xn(e){let t=e.item,n=p(t.objectId,240)||"unknown",i=p(t.propertyValue,240)||"unknown";return{sourceEventId:`deal:${n}:stage:${i}:${p(t.occurredAt,100)||"unknown"}`,source:"hubspot",sourceAccountRef:e.sourceAccountRef,dealId:n,stageId:i,stageName:e.stageName,occurredAt:B(t.occurredAt),valueMinor:e.valueMinor,currency:e.currency,status:/closed.?won/i.test(i)?"won":/closed.?lost/i.test(i)?"lost":"open"}}function Gn(e){let t=e.payload,n=t.opportunity&&typeof t.opportunity=="object"?t.opportunity:t,i=p(n.id??t.id,240)||"unknown",r=p(n.pipelineStageId??t.pipelineStageId,240)||"unknown",s=(p(n.status??t.status,80)||"open").toLowerCase();return{sourceEventId:`opportunity:${i}:stage:${r}:${p(t.timestamp??t.dateAdded,100)||"unknown"}`,source:"highlevel",sourceAccountRef:e.sourceAccountRef,contactId:p(n.contactId??t.contactId,240),dealId:i,pipelineId:p(n.pipelineId??t.pipelineId,240),stageId:r,stageName:p(n.pipelineStageName??t.pipelineStageName,240),occurredAt:B(t.timestamp,t.dateAdded,n.updatedAt),valueMinor:Math.round((W(n.monetaryValue??t.monetaryValue)??0)*100),currency:p(n.currency??t.currency,3)?.toUpperCase(),status:s.includes("won")?"won":s.includes("lost")?"lost":s.includes("spam")?"spam":"open"}}var Kr=90;function Qn(e){let t=e.consent;if(!t)return{necessary:!0,siteAnalytics:!1,enhancedMatching:!1,advertisingMeasurement:!1,receiptId:null,reason:"missing"};if(t.globalPrivacyControl)return{necessary:!0,siteAnalytics:!1,enhancedMatching:!1,advertisingMeasurement:!1,receiptId:t.receiptId??null,reason:"gpc"};let n=t.purposes,i={necessary:!0,siteAnalytics:n.site_analytics==="granted",enhancedMatching:n.enhanced_matching==="granted",advertisingMeasurement:n.advertising_measurement==="granted",receiptId:t.receiptId??null};return{...i,reason:i.siteAnalytics||i.enhancedMatching||i.advertisingMeasurement?"granted":"denied"}}function Jr(e){let t=[];return e.confirmedPeople<e.minimumConfirmedPeople&&t.push(`confirmed_people:${e.confirmedPeople}/${e.minimumConfirmedPeople}`),e.positiveOutcomes<e.minimumPositiveOutcomes&&t.push(`positive_outcomes:${e.positiveOutcomes}/${e.minimumPositiveOutcomes}`),e.validatedManifest||t.push("validated_calibrated_manifest"),{...e,eligible:t.length===0,missingEvidence:t}}var zr={callrail:{expectedEventTypes:["call"],expectedEventPhases:["pre_call","routing_complete","post_call","call_modified"],signatureMode:"callrail_hmac_sha1_exact_body",freshnessSlaSeconds:86400,supportsReconciliation:!0},calltrackingmetrics:{expectedEventTypes:["call"],expectedEventPhases:["call_started","call_answered","call_complete","call_modified"],signatureMode:"ctm_hmac_sha1_time_and_exact_body",freshnessSlaSeconds:86400,supportsReconciliation:!1},twilio:{expectedEventTypes:["call_status"],expectedEventPhases:["initiated","ringing","in-progress","completed","busy","failed","no-answer","canceled"],signatureMode:"twilio_sdk_exact_url_and_form_body",freshnessSlaSeconds:86400,supportsReconciliation:!0},hubspot:{expectedEventTypes:["deal.propertyChange"],expectedEventPhases:["dealstage"],signatureMode:"hubspot_v3_hmac_sha256_exact_url_body_timestamp",freshnessSlaSeconds:86400,supportsReconciliation:!1},highlevel:{expectedEventTypes:["OpportunityStageUpdate"],expectedEventPhases:["open","won","lost","spam"],signatureMode:"highlevel_ed25519_or_legacy_rsa",freshnessSlaSeconds:86400,supportsReconciliation:!1},generic_crm:{expectedEventTypes:["crm_stage_changed","transaction","custom_server"],expectedEventPhases:["open","qualified","won","lost","spam"],signatureMode:"xray_hmac_sha256_timestamp_and_exact_body",freshnessSlaSeconds:86400,supportsReconciliation:!1}},Yr=/(^|_)(api_?key|access_?key|api_?token|auth_?token|token|password|private_?key|secret|signing_?key)($|_)/i;function Xr(e){let t=(n,i)=>{if(!(!n||typeof n!="object"))for(let[r,s]of Object.entries(n)){let a=i?`${i}.${r}`:r,o=r.replace(/([a-z0-9])([A-Z])/g,"$1_$2").toLowerCase();if(Yr.test(o)&&!/public_?key/i.test(o))throw new f("analytics_connection_config_contains_credential",`Connection config cannot contain credentials (${a}). Use the encrypted webhook secret or service connection instead.`,400);t(s,a)}};t(e,"")}function Ho(e){return e instanceof Error&&e.message==="webhook_secret_unavailable"?"webhook_secret_unavailable":e instanceof f?e.code.slice(0,80):"invalid_webhook_payload"}function Tt(e){let t=e.provider,n=zr[t];if(!n)throw new f("analytics_connection_provider_unavailable","This analytics connection is not available in the current public release.",404);return{callbackPath:`/analytics/v1/webhooks/${t}/${e.connectionId}`,expectedEventTypes:[...n.expectedEventTypes],expectedEventPhases:[...n.expectedEventPhases],signatureMode:n.signatureMode,freshnessSlaSeconds:n.freshnessSlaSeconds,reconciliationCapability:n.supportsReconciliation?e.serviceConnectionRef?"available":"connection_required":"webhook_only"}}function qo(e,t){let n=String(e.id),i=String(e.provider),r=Tt({provider:i,connectionId:n,serviceConnectionRef:typeof e.service_connection_ref=="string"?e.service_connection_ref:null}),s=String(e.readiness??"configured_unverified"),a=e.verification_evidence==="provider"||e.verification_evidence==="synthetic"?e.verification_evidence:null,o=!!e.webhook_secret_configured,c=e.last_event_at?String(e.last_event_at):null,d=s==="live"&&c?Date.now()-Date.parse(c)>r.freshnessSlaSeconds*1e3:!1;return{...e,callbackUrl:new URL(r.callbackPath,`${t.replace(/\/$/,"")}/`).toString(),expectedEventTypes:r.expectedEventTypes,expectedEventPhases:r.expectedEventPhases,signatureMode:r.signatureMode,freshnessSlaSeconds:r.freshnessSlaSeconds,freshness:d?"stale":c?"fresh":"awaiting_first_event",reconciliationCapability:r.reconciliationCapability,lastVerifiedReceipt:e.last_verified_receipt_id?{receiptId:String(e.last_verified_receipt_id),evidence:a,verifiedAt:e.last_verified_at?String(e.last_verified_at):null}:a?{receiptId:null,evidence:a,verifiedAt:e.last_verified_at?String(e.last_verified_at):null}:null,safeError:e.last_error_code?{code:String(e.last_error_code)}:null,setupChecklist:{callbackUrlGenerated:!0,callbackConfigured:a==="provider",signatureConfigured:i==="highlevel"||o,syntheticTestPassed:a==="synthetic"||a==="provider",providerReceiptReceived:a==="provider"},evidenceStillMissing:a==="provider"?[]:["Send a real provider-signed event to the callback URL to mark this connection live."]}}async function Wo(e){let t=At.parse(e.definition),n=v();await J(n,e.siteId,e.userId);let i=t.enabled?"active":"disabled";return(await n.query(`INSERT INTO analytics_event_definitions(
|
|
612
|
+
id, site_id, name, event_name, trigger_kind, definition, status, created_by_user_id
|
|
613
|
+
) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8)
|
|
614
|
+
ON CONFLICT(site_id, name) DO UPDATE SET event_name=EXCLUDED.event_name,
|
|
615
|
+
trigger_kind=EXCLUDED.trigger_kind, definition=EXCLUDED.definition,
|
|
616
|
+
status=EXCLUDED.status, version=analytics_event_definitions.version+1, updated_at=now()
|
|
617
|
+
RETURNING id, name, event_name, trigger_kind, definition, version, status, created_at, updated_at`,[se(),e.siteId,t.name,t.eventName,t.triggerKind,JSON.stringify(t),i,e.userId])).rows[0]}async function Bo(e,t){let n=v();return await U(n,e,t),(await n.query(`SELECT id, name, event_name, trigger_kind, definition, version, status, created_at, updated_at
|
|
618
|
+
FROM analytics_event_definitions WHERE site_id=$1 AND status <> 'archived'
|
|
619
|
+
ORDER BY created_at DESC`,[e])).rows}async function xt(e){let t=v();e.requireEditor?await J(t,e.siteId,e.userId):await U(t,e.siteId,e.userId);let i=(await t.query(`SELECT id, definition, version, status FROM analytics_event_definitions
|
|
620
|
+
WHERE id=$1 AND site_id=$2`,[e.definitionId,e.siteId])).rows[0];if(!i||i.status==="archived")throw new f("analytics_event_definition_not_found","Event definition not found.",404);return{db:t,row:i}}async function Vo(e){let{db:t,row:n}=await xt({...e,requireEditor:!0}),i=At.parse({...n.definition,...e.changes}),r=i.enabled?"active":"disabled";return(await t.query(`UPDATE analytics_event_definitions SET
|
|
621
|
+
name=$3, event_name=$4, trigger_kind=$5, definition=$6::jsonb,
|
|
622
|
+
status=$7, version=version+1, updated_at=now()
|
|
623
|
+
WHERE id=$1 AND site_id=$2
|
|
624
|
+
RETURNING id, name, event_name, trigger_kind, definition, version, status, created_at, updated_at`,[e.definitionId,e.siteId,i.name,i.eventName,i.triggerKind,JSON.stringify(i),r])).rows[0]}async function Ko(e){let t=v();await J(t,e.siteId,e.userId);let n=await t.query(`UPDATE analytics_event_definitions SET status='archived', updated_at=now()
|
|
625
|
+
WHERE id=$1 AND site_id=$2
|
|
626
|
+
RETURNING id, status`,[e.definitionId,e.siteId]);if(!n.rows[0])throw new f("analytics_event_definition_not_found","Event definition not found.",404);return n.rows[0]}function Gr(e,t,n){let i=t.hostnames.length===0||t.hostnames.includes(n.hostname),r=t.triggerKind===n.triggerKind,s=t.triggerKind==="page"?t.pathMatch==="prefix"?n.path.startsWith(t.path??"/"):n.path===t.path:null,a=t.triggerKind==="semantic"?n.tagEventName===t.eventName:null,o=t.triggerKind==="click"||t.triggerKind==="form"?n.selectorMatched===!0:null;return{definitionId:e,eventName:t.eventName,triggerKind:t.triggerKind,matched:t.enabled&&i&&r&&(s??a??o??!1),checks:{hostname:i,triggerKind:r,path:s,tag:a,selector:o}}}async function Jo(e){let{row:t}=await xt({...e,requireEditor:!1});return Gr(e.definitionId,t.definition,e.preview)}async function zo(e){let{db:t,row:n}=await xt({...e,requireEditor:!1}),i=Math.max(0,Math.min(15e3,Math.trunc(e.timeoutMs??0))),r=Math.max(1,Math.min(86400,Math.trunc(e.maxAgeSeconds??300))),s=Date.now()+i;do{let o=(await t.query(`SELECT event_id, received_at
|
|
627
|
+
FROM analytics_events
|
|
628
|
+
WHERE site_id=$1 AND event_definition_id=$2 AND event_definition_version=$3
|
|
629
|
+
AND received_at >= now() - ($4 * interval '1 second')
|
|
630
|
+
ORDER BY received_at DESC, id DESC
|
|
631
|
+
LIMIT 1`,[e.siteId,e.definitionId,n.version,r])).rows[0];if(o)return{status:"observed_live_match",definitionId:e.definitionId,definitionVersion:n.version,eventId:o.event_id,observedAt:new Date(o.received_at).toISOString(),maxAgeSeconds:r};let c=s-Date.now();if(c<=0)break;await new Promise(d=>setTimeout(d,Math.min(500,c)))}while(Date.now()<=s);return{status:"not_observed",definitionId:e.definitionId,definitionVersion:n.version,eventId:null,observedAt:null,maxAgeSeconds:r}}async function Yo(e){let t=await v().query(`SELECT s.owner_user_id, s.id AS site_id, d.id, d.version, d.definition
|
|
632
|
+
FROM analytics_pixels p JOIN analytics_sites s ON s.id=p.site_id
|
|
633
|
+
LEFT JOIN analytics_event_definitions d ON d.site_id=s.id AND d.status='active'
|
|
634
|
+
WHERE p.public_id=$1 AND p.status='active'`,[e]);return{ownerUserId:t.rows[0]?Number(t.rows[0].owner_user_id):null,siteId:t.rows[0]?.site_id??null,definitions:t.rows.filter(n=>n.id&&n.version&&n.definition).map(n=>({...n.definition,id:n.id,version:n.version}))}}async function Xo(e){Xr(e.config??{});let t=v();await J(t,e.siteId,e.userId);let n=e.webhookSecret?de(e.webhookSecret,`connection-secret:${e.provider}:${e.sourceAccountRef}`):null;return(await t.query(`INSERT INTO analytics_connections(
|
|
635
|
+
id, site_id, provider, name, source_account_ref, service_connection_ref,
|
|
636
|
+
secret_ciphertext, config, created_by_user_id
|
|
637
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9)
|
|
638
|
+
ON CONFLICT(site_id, provider, source_account_ref) DO UPDATE SET
|
|
639
|
+
name = EXCLUDED.name,
|
|
640
|
+
service_connection_ref = COALESCE(EXCLUDED.service_connection_ref, analytics_connections.service_connection_ref),
|
|
641
|
+
secret_ciphertext = COALESCE(EXCLUDED.secret_ciphertext, analytics_connections.secret_ciphertext),
|
|
642
|
+
config = EXCLUDED.config,
|
|
643
|
+
readiness = CASE WHEN analytics_connections.readiness = 'live' THEN 'live' ELSE 'configured_unverified' END,
|
|
644
|
+
updated_at = now()
|
|
645
|
+
RETURNING id, site_id, provider, name, source_account_ref, service_connection_ref,
|
|
646
|
+
readiness, last_verified_at, last_event_at, last_reconciled_at, last_error_code,
|
|
647
|
+
verification_evidence, last_verified_receipt_id,
|
|
648
|
+
(secret_ciphertext IS NOT NULL) AS webhook_secret_configured,
|
|
649
|
+
created_at, updated_at`,[se(),e.siteId,e.provider.trim().toLowerCase().slice(0,80),e.name.trim().slice(0,120),e.sourceAccountRef.trim().slice(0,240),e.serviceConnectionRef?.trim().slice(0,240)||null,n,JSON.stringify(e.config??{}),e.userId])).rows[0]}async function Go(e,t){let n=v();return await U(n,e,t),(await n.query(`SELECT id, site_id, provider, name, source_account_ref, service_connection_ref,
|
|
650
|
+
readiness, last_verified_at, last_event_at, last_reconciled_at,
|
|
651
|
+
last_error_code, verification_evidence, last_verified_receipt_id,
|
|
652
|
+
created_at, updated_at,
|
|
653
|
+
(secret_ciphertext IS NOT NULL) AS webhook_secret_configured
|
|
654
|
+
FROM analytics_connections WHERE site_id = $1 ORDER BY created_at`,[e])).rows}async function Zo(e){let n=(await v().query(`SELECT id, site_id, provider, source_account_ref, secret_ciphertext,
|
|
655
|
+
readiness, config
|
|
656
|
+
FROM analytics_connections WHERE id = $1 AND readiness <> 'disabled'`,[e])).rows[0];if(!n)return null;let i=`connection-secret:${n.provider}:${n.source_account_ref}`;return{id:n.id,siteId:n.site_id,provider:n.provider,sourceAccountRef:n.source_account_ref,secret:n.secret_ciphertext?he(n.secret_ciphertext,i):null,readiness:n.readiness,config:n.config||{}}}async function ei(e){let t=v();await J(t,e.siteId,e.userId);let i=(await t.query(`SELECT id, site_id, provider, source_account_ref, service_connection_ref,
|
|
657
|
+
secret_ciphertext, readiness, config
|
|
658
|
+
FROM analytics_connections
|
|
659
|
+
WHERE id=$1 AND site_id=$2 AND readiness <> 'disabled'`,[e.connectionId,e.siteId])).rows[0];if(!i)throw new f("analytics_connection_not_found","Analytics connection not found.",404);return i}function Zr(e){return e.secret_ciphertext?he(e.secret_ciphertext,`connection-secret:${e.provider}:${e.source_account_ref}`):null}function Qr(e){let t=e.now??Date.now(),n=new Date(t).toISOString(),i=e.secret||"xray-ephemeral-synthetic-fixture-secret";if(e.provider==="callrail"){let c={resource_id:"xray-test-call",timestamp:n,answered:!0,duration:30},d=JSON.stringify(c);return{signatureVerified:Hn({rawBody:d,signature:e.tamperSignature?"invalid":Dt("sha1",i).update(d).digest("base64"),secret:i,payloadTimestamp:n,now:t}),event:qn({payload:c,sourceAccountRef:e.sourceAccountRef,phase:"post_call"})}}if(e.provider==="calltrackingmetrics"){let c={activity_id:"xray-test-call",timestamp:n,status:"completed"},d=JSON.stringify(c),_=String(Math.floor(t/1e3));return{signatureVerified:Wn({rawBody:d,requestTime:_,signature:e.tamperSignature?"invalid":Dt("sha1",i).update(_+d).digest("base64"),secret:i,now:t}),event:Bn({payload:c,sourceAccountRef:e.sourceAccountRef,phase:"call_complete"})}}if(e.provider==="twilio"){let c={CallSid:"CA_XRAY_TEST",CallStatus:"completed",Timestamp:n},d=e.tamperSignature?"invalid":Vr.getExpectedTwilioSignature(i,e.callbackUrl,c);return{signatureVerified:Vn({authToken:i,signature:d,url:e.callbackUrl,params:c}),event:Kn({params:c,sourceAccountRef:e.sourceAccountRef})}}if(e.provider==="hubspot"){let c={subscriptionType:"deal.propertyChange",propertyName:"dealstage",objectId:"xray-test-deal",propertyValue:"qualified",occurredAt:String(t)},d=JSON.stringify([c]),_=String(t),u=Dt("sha256",i).update(`POST${e.callbackUrl}${d}${_}`).digest("base64"),y=e.tamperSignature?"invalid":u;return{signatureVerified:zn({clientSecret:i,signature:y,timestamp:_,method:"POST",url:e.callbackUrl,rawBody:d,now:t}),event:Ot(Xn({item:c,sourceAccountRef:e.sourceAccountRef}))}}if(e.provider==="highlevel"){let c={timestamp:n,opportunity:{id:"xray-test-opportunity",pipelineStageId:"qualified",status:"open"}},d=JSON.stringify(c),{privateKey:_,publicKey:u}=Wr("ed25519"),y=e.tamperSignature?Buffer.alloc(64).toString("base64"):Br(null,Buffer.from(d),_).toString("base64");return{signatureVerified:Yn({rawBody:d,ghlSignature:y,ed25519PublicKey:u.export({type:"spki",format:"pem"}).toString()}),event:Ot(Gn({payload:c,sourceAccountRef:e.sourceAccountRef}))}}let r=Qe.parse({schemaVersion:1,eventId:"generic_xray_test",sourceEventId:"xray-test-event",source:"generic_crm",sourceAccountRef:e.sourceAccountRef,eventKind:"crm",eventName:"crm_stage_changed",occurredAt:n,dealId:"xray-test-deal",properties:{stageId:"qualified",status:"open"}}),s=JSON.stringify(r),a=String(t),o=Qt(e.provider,a,s,i);return{signatureVerified:en(e.tamperSignature?`${o.startsWith("0")?"1":"0"}${o.slice(1)}`:o,o),event:r}}async function Qo(e){let t=await ei(e),n=Zr(t);if(t.provider!=="highlevel"&&!n)throw new f("analytics_connection_secret_unavailable","Configure the provider webhook validation secret before running a connection test.",409);let i=Tt({provider:t.provider,connectionId:t.id,serviceConnectionRef:t.service_connection_ref}),r=new URL(i.callbackPath,`${e.requestOrigin.replace(/\/$/,"")}/`).toString(),s=Qr({provider:t.provider,sourceAccountRef:t.source_account_ref,callbackUrl:r,secret:n});if(!s.signatureVerified)throw await Zn({connectionId:t.id,readiness:"degraded",errorCode:"synthetic_signature_test_failed"}),new f("analytics_connection_test_failed","Connection signature test failed.",409);let a=await Zn({connectionId:t.id,readiness:"verified",evidence:"synthetic"}),o=a?.verification_evidence??"synthetic";return{status:a?.readiness??"verified",evidence:o,provider:t.provider,eventType:s.event.eventKind,eventPhase:s.event.eventName,connection:a,evidenceStillMissing:o==="provider"?[]:["A real provider-signed receipt is required before this connection becomes live."]}}async function ec(e){let t=await ei(e),n=Tt({provider:t.provider,connectionId:t.id,serviceConnectionRef:t.service_connection_ref});return{capability:n.reconciliationCapability,started:!1,evidence:null,nextAction:n.reconciliationCapability==="available"?"Reconciliation is available; the reconciliation worker must claim and execute this request.":n.reconciliationCapability==="connection_required"?"Connect the provider read API before reconciliation can run.":"This provider is webhook only; use webhook retries or historical backfill."}}async function tc(e){let t=se(),n=e.rawBody?de(e.rawBody,"webhook-payload"):null,i=await v().query(`INSERT INTO analytics_webhook_receipts(
|
|
660
|
+
id, site_id, connection_id, provider, source_account_ref, source_event_id,
|
|
661
|
+
signature_status, payload_ciphertext, payload_expires_at
|
|
662
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,now() + interval '7 days')
|
|
663
|
+
ON CONFLICT(site_id, provider, source_account_ref, source_event_id) DO UPDATE SET
|
|
664
|
+
normalization_status = CASE
|
|
665
|
+
WHEN analytics_webhook_receipts.normalization_status = 'accepted' THEN 'replayed'
|
|
666
|
+
ELSE analytics_webhook_receipts.normalization_status
|
|
667
|
+
END
|
|
668
|
+
RETURNING id, normalization_status`,[t,e.siteId,e.connectionId,e.provider,e.sourceAccountRef,e.sourceEventId,e.signatureStatus,n]);return{id:i.rows[0].id,duplicate:i.rows[0].id!==t,status:i.rows[0].normalization_status}}async function nc(e){await v().query(`UPDATE analytics_webhook_receipts SET
|
|
669
|
+
normalization_status = $2,
|
|
670
|
+
error_code = $3,
|
|
671
|
+
normalized_at = now()
|
|
672
|
+
WHERE id = $1`,[e.receiptId,e.accepted?"accepted":"rejected",e.errorCode??null])}async function Zn(e){return(await v().query(`UPDATE analytics_connections SET
|
|
673
|
+
readiness = CASE WHEN readiness='live' AND $2='verified' THEN 'live' ELSE $2 END,
|
|
674
|
+
last_error_code = $3,
|
|
675
|
+
last_verified_at = CASE WHEN $2 IN ('verified','live') THEN now() ELSE last_verified_at END,
|
|
676
|
+
last_event_at = CASE WHEN $4 THEN now() ELSE last_event_at END,
|
|
677
|
+
last_reconciled_at = CASE WHEN $5 THEN now() ELSE last_reconciled_at END,
|
|
678
|
+
verification_evidence = CASE
|
|
679
|
+
WHEN verification_evidence='provider' THEN 'provider'
|
|
680
|
+
ELSE COALESCE($6, verification_evidence)
|
|
681
|
+
END,
|
|
682
|
+
last_verified_receipt_id = COALESCE($7::uuid, last_verified_receipt_id),
|
|
683
|
+
updated_at = now()
|
|
684
|
+
WHERE id = $1
|
|
685
|
+
RETURNING id, readiness, last_verified_at, last_event_at, last_reconciled_at,
|
|
686
|
+
last_error_code, verification_evidence, last_verified_receipt_id`,[e.connectionId,e.readiness,e.errorCode??null,!!e.receipt,!!e.reconciled,e.evidence??null,e.receiptId??null])).rows[0]??null}async function ic(e){let t=Dn.parse(e.rule),n=v();return await J(n,e.siteId,e.userId),(await n.query(`INSERT INTO analytics_conversion_rules(
|
|
687
|
+
id, site_id, name, conversion_kind, condition, default_value_minor,
|
|
688
|
+
default_currency, status, created_by_user_id
|
|
689
|
+
) VALUES ($1,$2,$3,$4,$5::jsonb,$6,$7,$8,$9)
|
|
690
|
+
ON CONFLICT(site_id, name) DO UPDATE SET
|
|
691
|
+
conversion_kind = EXCLUDED.conversion_kind,
|
|
692
|
+
condition = EXCLUDED.condition,
|
|
693
|
+
default_value_minor = EXCLUDED.default_value_minor,
|
|
694
|
+
default_currency = EXCLUDED.default_currency,
|
|
695
|
+
status = EXCLUDED.status,
|
|
696
|
+
version = analytics_conversion_rules.version + 1,
|
|
697
|
+
updated_at = now()
|
|
698
|
+
RETURNING *`,[se(),e.siteId,t.name,t.conversionKind,JSON.stringify({...t.condition,...t.valueRule?{valueRule:t.valueRule}:{}}),t.defaultValueMinor??null,t.defaultCurrency.toUpperCase(),t.enabled?"active":"disabled",e.userId])).rows[0]}async function rc(e,t){let n=v();return await U(n,e,t),(await n.query(`SELECT id, name, conversion_kind, condition, default_value_minor,
|
|
699
|
+
default_currency, version, status, created_at, updated_at
|
|
700
|
+
FROM analytics_conversion_rules WHERE site_id = $1 AND status <> 'archived'
|
|
701
|
+
ORDER BY created_at`,[e])).rows.map(r=>{let s=r.condition&&typeof r.condition=="object"?r.condition:{},{valueRule:a,...o}=s;return{...r,condition:o,value_rule:a??null}})}function es(e){let t=e.properties.tags;return Array.isArray(t)?t.filter(n=>typeof n=="string"):[]}function ts(e,t){let n=[],i=e.condition;i.eventName&&i.eventName!==t.eventName&&n.push("event_name_mismatch"),i.eventKind&&i.eventKind!==t.eventKind&&n.push("event_kind_mismatch"),i.source&&i.source!==t.source&&n.push("source_mismatch"),i.stageId&&i.stageId!==t.properties.stageId&&n.push("stage_mismatch"),i.status&&i.status!==t.properties.status&&n.push("status_mismatch"),i.answered!==void 0&&i.answered!==!!t.properties.answered&&n.push("answered_mismatch"),i.minimumCallDurationSeconds!==void 0&&Number(t.properties.durationSeconds??0)<i.minimumCallDurationSeconds&&n.push("call_too_short");let r=es(t);return i.excludedTags.some(s=>r.includes(s))&&n.push("excluded_tag"),i.requiredTags.some(s=>!r.includes(s))&&n.push("required_tag_missing"),(t.properties.status==="spam"||r.includes("spam"))&&n.push("spam_excluded"),{matches:n.length===0,reasons:n}}async function ns(e,t,n){if(!Qn(n).enhancedMatching)return{personId:null,namespaceId:null,identityTier:"unlinked"};let s=(await e.query(`SELECT id FROM analytics_identity_namespaces
|
|
702
|
+
WHERE site_id=$1 AND status='approved' ORDER BY version DESC LIMIT 1`,[t])).rows[0]?.id??null,a=[];n.visitorId&&a.push(["visitor_id",n.visitorId]),n.sessionId&&a.push(["session_id",n.sessionId]),n.personRef&&a.push(["customer_id",n.personRef]);for(let[o,c]of Object.entries(G(n.clickIds)))a.push(["click_id",`${o}:${c}`]);for(let[o,c]of a){let d=await e.query(`SELECT e.person_id FROM analytics_identity_nodes n
|
|
703
|
+
JOIN analytics_identity_edges e ON e.identity_node_id = n.id
|
|
704
|
+
WHERE n.site_id = $1 AND n.kind = $2 AND n.value_hmac = $3
|
|
705
|
+
AND ($4::uuid IS NULL OR (n.namespace_id=$4 AND e.namespace_id=$4))
|
|
706
|
+
ORDER BY e.confidence DESC, e.last_seen_at DESC LIMIT 1`,[t,o,Ne(o,c),s]);if(d.rows[0])return{personId:d.rows[0].person_id,namespaceId:s,identityTier:"confirmed"}}return{personId:null,namespaceId:s,identityTier:"unlinked"}}async function is(e,t){let i=(await e.query(`SELECT m.person_id::text, m.namespace_id::text,
|
|
707
|
+
c.provider AS connection_provider,
|
|
708
|
+
s.latest_receipt_id::text AS receipt_id, s.policy_version,
|
|
709
|
+
s.site_analytics, s.enhanced_matching, s.advertising_measurement,
|
|
710
|
+
s.gpc, r.action, s.effective_at, s.expires_at
|
|
711
|
+
FROM analytics_crm_person_mappings m
|
|
712
|
+
JOIN analytics_connections c ON c.id=m.connection_id AND c.site_id=m.site_id
|
|
713
|
+
JOIN analytics_people p ON p.id=m.person_id AND p.site_id=m.site_id AND p.namespace_id=m.namespace_id
|
|
714
|
+
JOIN analytics_identity_namespaces n ON n.id=m.namespace_id AND n.site_id=m.site_id AND n.status='approved'
|
|
715
|
+
LEFT JOIN analytics_consent_current_state s
|
|
716
|
+
ON s.site_id=m.site_id AND s.subject_ref=p.public_ref
|
|
717
|
+
LEFT JOIN analytics_consent_receipts r
|
|
718
|
+
ON r.id=s.latest_receipt_id AND r.site_id=s.site_id
|
|
719
|
+
WHERE m.site_id=$1 AND m.connection_id::text=$2
|
|
720
|
+
AND m.provider_person_ref=$3 AND m.person_id::text=$4
|
|
721
|
+
LIMIT 1`,[t.siteId,t.context.connectionId,t.context.providerContactId,t.context.expectedPersonId])).rows[0];if(!i)return{personId:null,namespaceId:null,consent:void 0,reason:"mapping_unavailable"};let r,s;try{r=ee(t.context.provider),s=ee(i.connection_provider)}catch{return{personId:null,namespaceId:null,consent:void 0,reason:"provider_mismatch"}}if(r!==s)return{personId:null,namespaceId:null,consent:void 0,reason:"provider_mismatch"};if(!i.receipt_id||!i.effective_at)return{personId:null,namespaceId:null,consent:void 0,reason:"consent_missing"};if(i.action==="withdraw")return{personId:null,namespaceId:null,consent:void 0,reason:"consent_withdrawn"};if(i.gpc||i.action==="gpc")return{personId:null,namespaceId:null,consent:void 0,reason:"consent_gpc"};let a=new Date(t.at??Date.now()).getTime();if(i.expires_at&&new Date(i.expires_at).getTime()<=a)return{personId:null,namespaceId:null,consent:void 0,reason:"consent_expired"};if(!i.enhanced_matching)return{personId:null,namespaceId:null,consent:void 0,reason:"enhanced_matching_denied"};if(!i.advertising_measurement)return{personId:null,namespaceId:null,consent:void 0,reason:"advertising_measurement_denied"};let o=new Date(i.effective_at).toISOString();return{personId:i.person_id,namespaceId:i.namespace_id,reason:"eligible",consent:{analytics:i.site_analytics?"granted":"denied",adUserData:"granted",adPersonalization:"granted",globalPrivacyControl:!1,occurredAt:o,receiptId:i.receipt_id,policyVersion:i.policy_version??void 0,purposes:{site_analytics:i.site_analytics?"granted":"denied",enhanced_matching:"granted",advertising_measurement:"granted"}}}}async function rs(e,t){let n=G(t.eventClickIds),i=await e.query(`SELECT click_ids FROM analytics_attribution_touches
|
|
722
|
+
WHERE site_id=$1 AND person_id=$2
|
|
723
|
+
AND occurred_at <= $3::timestamptz
|
|
724
|
+
AND occurred_at >= $3::timestamptz - ($4::text || ' days')::interval
|
|
725
|
+
AND click_ids <> '{}'::jsonb
|
|
726
|
+
ORDER BY occurred_at DESC, id DESC`,[t.siteId,t.personId,t.occurredAt,Kr]),r={};for(let s of i.rows)for(let[a,o]of Object.entries(G(s.click_ids)))a in r||(r[a]=o);return{...r,...n}}function ss(e,t){let n=e.valueMinor??Number(t.default_value_minor??0),i=e.valueMinor!==void 0?"dynamic":(t.default_value_minor!==null,"fixed"),r=e.valueMinor!==void 0?"source_event":null,s={mode:e.valueMinor!==void 0?"dynamic":"fixed",source:r,observedAt:e.valueMinor!==void 0?e.occurredAt:null,fallback:!1,ruleVersion:String(t.version),resolution:i,baseValueMinor:n,resolvedValueMinor:n,currency:(e.currency||t.default_currency||"USD").toUpperCase(),linkedEvent:null};return{valueMinor:n,currency:s.currency,provenance:s}}async function as(e,t){if(!t.rule.value_rule)return ss(t.event,t.rule);let n=t.rule.value_rule.conditionalOverride?.linkedEventName,i=n&&t.personId?(await e.query(`SELECT id, event_name, occurred_at FROM analytics_external_events
|
|
727
|
+
WHERE site_id=$1 AND person_id=$2 AND event_name=$3 AND id<>$4
|
|
728
|
+
ORDER BY occurred_at DESC LIMIT 100`,[t.siteId,t.personId,n,t.insertedEventId])).rows.map(s=>({eventId:s.id,eventName:s.event_name,occurredAt:s.occurred_at.toISOString(),identityTier:"confirmed"})):[],r=t.event.valueMinor===void 0?void 0:{source_event:{valueMinor:t.event.valueMinor,currency:(t.event.currency||t.rule.default_currency||"USD").toUpperCase(),observedAt:t.event.occurredAt},crm_field:t.event.source.includes("crm")?{valueMinor:t.event.valueMinor,currency:(t.event.currency||t.rule.default_currency||"USD").toUpperCase(),observedAt:t.event.occurredAt}:void 0,transaction:t.event.eventKind==="transaction"?{valueMinor:t.event.valueMinor,currency:(t.event.currency||t.rule.default_currency||"USD").toUpperCase(),observedAt:t.event.occurredAt}:void 0};return Rn(t.rule.value_rule,{targetEvent:{eventId:t.insertedEventId,eventName:t.event.eventName,occurredAt:t.event.occurredAt,identityTier:t.personId?"confirmed":"candidate"},linkedEvents:i,observations:r,observedAt:t.event.occurredAt,providerOperation:"new_conversion"})}async function os(e){if(!e.namespaceId||!e.personId)return[];let t=await e.client.query(`SELECT id, name, score_kind, rules, minimum_score, maximum_score, version
|
|
729
|
+
FROM analytics_score_definitions WHERE site_id=$1 AND status='active'`,[e.siteId]);if(!t.rowCount)return[];let n=await e.client.query(`SELECT id, event_name, event_kind, occurred_at, source, properties
|
|
730
|
+
FROM analytics_external_events WHERE site_id=$1 AND person_id=$2
|
|
731
|
+
ORDER BY occurred_at DESC LIMIT 5000`,[e.siteId,e.personId]),i={events:n.rows.map(s=>({ref:s.id,name:s.event_name,occurredAt:s.occurred_at.toISOString()})),pages:n.rows.filter(s=>typeof s.properties.path=="string").map(s=>({ref:s.id,name:String(s.properties.path),occurredAt:s.occurred_at.toISOString()})),sources:n.rows.map(s=>({ref:s.id,name:s.source,occurredAt:s.occurred_at.toISOString()})),calls:n.rows.filter(s=>s.event_kind==="call").map(s=>({ref:s.id,name:String(s.properties.outcome??s.properties.status??s.event_name),occurredAt:s.occurred_at.toISOString()})),crmStages:n.rows.filter(s=>s.event_kind==="crm").map(s=>({ref:s.id,name:String(s.properties.stageId??s.properties.stageName??s.event_name),occurredAt:s.occurred_at.toISOString()}))},r=[];for(let s of t.rows){let a=s.rules&&typeof s.rules=="object"&&!Array.isArray(s.rules)?s.rules:{},o=Array.isArray(s.rules)?s.rules:a.groups;if(!Array.isArray(o)||o.length===0)continue;let c=Array.isArray(a.permittedProperties)?a.permittedProperties.filter(d=>typeof d=="string"):[];try{let d=Un();Fn({tables:d,id:s.id,definition:{namespaceId:e.namespaceId,name:s.name,kind:s.score_kind,targetObject:"person",minimum:s.minimum_score,maximum:s.maximum_score,groups:o,permittedProperties:c,enabled:!0},now:e.occurredAt});let _=jn({tables:d,namespaceId:e.namespaceId,scoreId:s.id,subjectRef:e.personId,identityTier:"confirmed",facts:i,now:e.occurredAt});await e.client.query(`INSERT INTO analytics_score_evaluations(
|
|
732
|
+
id, site_id, namespace_id, definition_id, definition_version,
|
|
733
|
+
person_id, score, contributions, evaluated_at
|
|
734
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9)`,[_.id,e.siteId,e.namespaceId,s.id,s.version,e.personId,_.value,JSON.stringify(_.components),_.evaluatedAt]),r.push(_.id)}catch{}}return r}async function cs(e){return e.namespaceId?(await e.client.query(`SELECT t.id, d.event_name, t.minimum_confirmed_people, t.minimum_positive_outcomes,
|
|
735
|
+
(SELECT count(DISTINCT p.id)::text FROM analytics_people p
|
|
736
|
+
WHERE p.site_id=t.site_id AND p.namespace_id=t.namespace_id) AS confirmed_people,
|
|
737
|
+
(SELECT count(DISTINCT e.person_id)::text FROM analytics_external_events e
|
|
738
|
+
JOIN analytics_people p ON p.id=e.person_id AND p.site_id=e.site_id
|
|
739
|
+
WHERE e.site_id=t.site_id AND p.namespace_id=t.namespace_id AND e.event_name=d.event_name) AS positive_outcomes,
|
|
740
|
+
EXISTS(SELECT 1 FROM analytics_prediction_manifests m
|
|
741
|
+
WHERE m.target_id=t.id AND m.status IN ('validated','active')) AS validated_manifest
|
|
742
|
+
FROM analytics_prediction_targets t
|
|
743
|
+
JOIN analytics_event_definitions d ON d.id=t.target_event_definition_id
|
|
744
|
+
WHERE t.site_id=$1 AND t.namespace_id=$2 AND t.status<>'disabled'`,[e.siteId,e.namespaceId])).rows.map(n=>Jr({targetId:n.id,confirmedPeople:Number(n.confirmed_people),positiveOutcomes:Number(n.positive_outcomes),minimumConfirmedPeople:n.minimum_confirmed_people,minimumPositiveOutcomes:n.minimum_positive_outcomes,validatedManifest:n.validated_manifest})):[]}async function ti(e){let t=Qe.parse(e.event),n=e.db??v();e.userId!==void 0&&await J(n,e.siteId,e.userId);let i=!e.transactionClient,r=e.transactionClient??await n.connect();try{i&&await r.query("BEGIN");let s=e.trustedCrmContext?await is(r,{siteId:e.siteId,context:e.trustedCrmContext}):null,a=e.trustedCrmContext?Qe.parse({...t,personRef:void 0,consent:s?.consent,properties:{...t.properties,_xrayCrmLinkReason:s?.reason??"mapping_unavailable"}}):t,o=Qn(a),c=s?.personId&&s.namespaceId?{personId:s.personId,namespaceId:s.namespaceId,identityTier:"confirmed"}:e.trustedCrmContext?{personId:null,namespaceId:null,identityTier:"unlinked"}:await ns(r,e.siteId,a),d=c.personId,_=await r.query(`INSERT INTO analytics_external_events(
|
|
745
|
+
id, site_id, receipt_id, person_id, source, source_account_ref,
|
|
746
|
+
source_event_id, event_kind, event_name, occurred_at, visitor_hmac,
|
|
747
|
+
session_hmac, call_id, deal_id, order_id, value_minor, currency,
|
|
748
|
+
click_ids, properties
|
|
749
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb)
|
|
750
|
+
ON CONFLICT(site_id, source, source_account_ref, source_event_id) DO NOTHING
|
|
751
|
+
RETURNING id`,[se(),e.siteId,e.receiptId??null,d,a.source,a.sourceAccountRef,a.sourceEventId,a.eventKind,a.eventName,a.occurredAt,a.visitorId?lt("visitor_id",a.visitorId):null,a.sessionId?lt("session_id",a.sessionId):null,a.callId??null,a.dealId??null,a.orderId??null,a.valueMinor??null,a.currency?.toUpperCase()??null,JSON.stringify(G(a.clickIds)),JSON.stringify({...a.properties,_xrayConsentEligibility:o,_xrayIdentityTier:c.identityTier,...c.namespaceId?{_xrayNamespaceId:c.namespaceId}:{}})]),u=_.rows[0]?.id??(await r.query(`SELECT id FROM analytics_external_events
|
|
752
|
+
WHERE site_id=$1 AND source=$2 AND source_account_ref=$3 AND source_event_id=$4`,[e.siteId,a.source,a.sourceAccountRef,a.sourceEventId])).rows[0]?.id;if(!u)throw new f("analytics_external_event_conflict","The canonical event could not be resolved.",409);let E=(await r.query(`SELECT id, name, conversion_kind, condition, default_value_minor,
|
|
753
|
+
default_currency, version
|
|
754
|
+
FROM analytics_conversion_rules WHERE site_id = $1 AND status = 'active'`,[e.siteId])).rows.filter(A=>ts({name:A.name,conversionKind:A.conversion_kind,condition:(({valueRule:k,...Le})=>Le)(A.condition),defaultValueMinor:A.default_value_minor?Number(A.default_value_minor):void 0,defaultCurrency:A.default_currency,valueRule:A.condition.valueRule,enabled:!0},a).matches),b=[],V={};for(let A of E){let k=await as(r,{siteId:e.siteId,personId:d,insertedEventId:u,event:a,rule:{value_rule:A.condition.valueRule,default_value_minor:A.default_value_minor,default_currency:A.default_currency,version:A.version}}),Le=k.valueMinor??0,Pt=(k.currency||A.default_currency||"USD").toUpperCase(),ci=k.provenance.resolution==="absent"?"absent":k.provenance.resolution==="fallback"?"dynamic":k.provenance.resolution,di=se(),Se=await r.query(`INSERT INTO analytics_conversions(
|
|
755
|
+
id, source_event_id, site_id, session_id, conversion_kind,
|
|
756
|
+
value_minor, currency, occurred_at, person_id, rule_id, rule_version, order_id,
|
|
757
|
+
value_source, value_provenance, session_hmac
|
|
758
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb,$15)
|
|
759
|
+
ON CONFLICT(site_id, source_event_id) DO NOTHING RETURNING id`,[di,`rule:${A.id}:${a.source}:${a.sourceEventId}`,e.siteId,a.sessionId??null,A.conversion_kind,Le,Pt,a.occurredAt,d,A.id,A.version,a.orderId??null,ci,JSON.stringify(k.provenance),a.sessionId?Ne("session_id",a.sessionId):null]);if(!Se.rowCount||(b.push(Se.rows[0].id),V[Se.rows[0].id]=k.provenance,!d||!o.advertisingMeasurement))continue;let li=await r.query(`SELECT id FROM analytics_activation_destinations
|
|
760
|
+
WHERE site_id = $1 AND status = 'active' AND readiness IN ('verified','live')`,[e.siteId]),ui=await rs(r,{siteId:e.siteId,personId:d,occurredAt:a.occurredAt,eventClickIds:a.clickIds}),_i=de(JSON.stringify({eventId:Se.rows[0].id,eventName:A.conversion_kind,eventTime:a.occurredAt,valueMinor:Le,currency:Pt,valueProvenance:k.provenance,attributionModelProvenance:{model:typeof a.properties.attributionModel=="string"?a.properties.attributionModel:"unassigned",evidenceTier:"confirmed"},orderId:a.orderId,clickIds:ui,source:a.source,personId:d,identityTier:"confirmed",namespaceId:c.namespaceId,consent:{receiptId:o.receiptId,advertisingMeasurement:!0}}),"activation-payload");for(let mi of li.rows)await r.query(`INSERT INTO analytics_activation_jobs(
|
|
761
|
+
id, destination_id, conversion_id, person_id, payload_ciphertext
|
|
762
|
+
) VALUES ($1,$2,$3,$4,$5)
|
|
763
|
+
ON CONFLICT(destination_id, conversion_id) DO NOTHING`,[se(),mi.id,Se.rows[0].id,d,_i])}let P=await os({client:r,siteId:e.siteId,namespaceId:c.namespaceId,personId:d,occurredAt:a.occurredAt}),h=await cs({client:r,siteId:e.siteId,namespaceId:c.namespaceId});d&&await Sn({siteId:e.siteId,personId:d,canonicalEventId:u,event:a,consent:{...o.receiptId?{receiptId:o.receiptId}:{},siteAnalytics:o.siteAnalytics,advertisingMeasurement:o.advertisingMeasurement},db:r}),i&&await r.query("COMMIT");let T=null,m=typeof a.properties.candidateAssociationId=="string"?a.properties.candidateAssociationId:null,K=typeof a.properties.deterministicEvidenceRef=="string"?a.properties.deterministicEvidenceRef:null,w=a.properties.deterministicEvidenceKind;return d&&c.namespaceId&&m&&K&&["verified_email","verified_phone","crm_contact","signed_linker","authenticated_user"].includes(String(w))&&(T=await Pn({siteId:e.siteId,namespaceId:c.namespaceId,associationId:m,personId:d,evidenceKind:w,evidenceRef:K,occurredAt:a.occurredAt}).catch(()=>null)),{id:_.rows[0]?.id??null,created:!!_.rowCount,personId:d,identityTier:c.identityTier,namespaceId:c.namespaceId,conversionIds:b,consentEligibility:o,valueProvenance:V,scoreEvaluationIds:P,predictionEvidence:h,candidatePromotion:T}}catch(s){throw i&&await r.query("ROLLBACK"),s}finally{i&&r.release()}}async function sc(e,t,n){let i=v();if(await U(i,e,t),!(await i.query("SELECT 1 FROM analytics_people WHERE id = $1 AND site_id = $2",[n,e])).rowCount)throw new f("analytics_person_not_found","Attributed person not found.",404);let[s,a,o,c,d]=await Promise.all([i.query(`SELECT id, started_at, last_active_at, ended_at, duration_ms, landing_path,
|
|
764
|
+
exit_path, pageview_count, event_count
|
|
765
|
+
FROM analytics_sessions WHERE site_id = $1 AND person_id = $2 ORDER BY started_at`,[e,n]),i.query(`SELECT occurred_at, source, medium, campaign, referrer, landing_path
|
|
766
|
+
FROM analytics_attribution_touches WHERE site_id = $1 AND person_id = $2 ORDER BY occurred_at`,[e,n]),i.query(`SELECT source, event_kind, event_name, occurred_at, value_minor, currency
|
|
767
|
+
FROM analytics_external_events WHERE site_id = $1 AND person_id = $2 ORDER BY occurred_at`,[e,n]),i.query(`SELECT conversion_kind, value_minor, currency, occurred_at, rule_version
|
|
768
|
+
FROM analytics_conversions WHERE site_id = $1 AND person_id = $2 ORDER BY occurred_at`,[e,n]),i.query(`SELECT j.status, j.attempts, j.response_category, j.created_at, j.delivered_at,
|
|
769
|
+
d.platform, d.name AS destination_name
|
|
770
|
+
FROM analytics_activation_jobs j
|
|
771
|
+
JOIN analytics_activation_destinations d ON d.id = j.destination_id
|
|
772
|
+
WHERE d.site_id = $1 AND j.person_id = $2 ORDER BY j.created_at`,[e,n])]),_=u=>{if(typeof u!="string"||!u)return null;try{let y=new URL(u);return`${y.origin}${y.pathname}`.slice(0,2e3)}catch{return null}};return{personRef:O("person",e,n),sessions:s.rows.map(u=>({sessionRef:O("session",e,String(u.id)),started_at:u.started_at,last_active_at:u.last_active_at,ended_at:u.ended_at,duration_ms:u.duration_ms,landing_path:u.landing_path,exit_path:u.exit_path,pageview_count:u.pageview_count,event_count:u.event_count})),touches:a.rows.map(u=>({occurred_at:u.occurred_at,source:u.source,medium:u.medium,campaign:u.campaign,referrer:_(u.referrer),landing_path:u.landing_path})),externalEvents:o.rows.map(u=>({source:u.source,event_kind:u.event_kind,event_name:u.event_name,occurred_at:u.occurred_at,value_minor:u.value_minor,currency:u.currency})),conversions:c.rows.map(u=>({conversion_kind:u.conversion_kind,value_minor:u.value_minor,currency:u.currency,occurred_at:u.occurred_at,rule_version:u.rule_version})),deliveries:d.rows.map(u=>({status:u.status,attempts:u.attempts,response_category:u.response_category,created_at:u.created_at,delivered_at:u.delivered_at,platform:u.platform,destination_name:u.destination_name}))}}async function ac(e,t){let n=v();await U(n,e,t);let r=(await n.query(`SELECT
|
|
773
|
+
count(*)::int AS total_events,
|
|
774
|
+
count(*) FILTER (WHERE person_id IS NOT NULL)::int AS identified_events,
|
|
775
|
+
count(*) FILTER (WHERE click_ids <> '{}'::jsonb)::int AS events_with_click_id,
|
|
776
|
+
count(*) FILTER (WHERE event_kind = 'call')::int AS call_events,
|
|
777
|
+
count(*) FILTER (WHERE event_kind = 'call' AND person_id IS NOT NULL)::int AS joined_call_events,
|
|
778
|
+
count(*) FILTER (WHERE event_kind = 'crm')::int AS crm_events,
|
|
779
|
+
count(*) FILTER (WHERE event_kind = 'crm' AND person_id IS NOT NULL)::int AS joined_crm_events,
|
|
780
|
+
count(*) FILTER (WHERE properties->>'status' = 'spam' OR properties->'tags' ? 'spam')::int AS spam_events
|
|
781
|
+
FROM analytics_external_events WHERE site_id = $1`,[e])).rows[0],s=(a,o)=>o?a/o:null;return{...r,identifiedRate:s(r.identified_events,r.total_events),clickIdCaptureRate:s(r.events_with_click_id,r.total_events),phoneJoinRate:s(r.joined_call_events,r.call_events),crmStageJoinRate:s(r.joined_crm_events,r.crm_events)}}var xe="mcp_scraper_subscription_paid";function ds(e){return/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e)}async function ls(e,t){return(await e.query(`SELECT p.id::text AS person_id, p.namespace_id::text AS namespace_id,
|
|
782
|
+
cs.latest_receipt_id::text AS receipt_id, cs.policy_version,
|
|
783
|
+
cs.site_analytics, cs.enhanced_matching, cs.advertising_measurement,
|
|
784
|
+
cs.effective_at
|
|
785
|
+
FROM analytics_identity_nodes node
|
|
786
|
+
JOIN analytics_identity_edges edge
|
|
787
|
+
ON edge.identity_node_id=node.id AND edge.site_id=node.site_id
|
|
788
|
+
JOIN analytics_people p
|
|
789
|
+
ON p.id=edge.person_id AND p.site_id=node.site_id
|
|
790
|
+
JOIN analytics_identity_namespaces ns
|
|
791
|
+
ON ns.id=p.namespace_id AND ns.site_id=p.site_id AND ns.status='approved'
|
|
792
|
+
JOIN analytics_sites site
|
|
793
|
+
ON site.id=p.site_id AND site.status='active'
|
|
794
|
+
JOIN analytics_restricted_data profile
|
|
795
|
+
ON profile.site_id=p.site_id AND profile.person_id=p.id
|
|
796
|
+
AND profile.purpose='enhanced_matching' AND profile.expires_at>now()
|
|
797
|
+
JOIN analytics_consent_receipts provenance
|
|
798
|
+
ON provenance.id=profile.consent_receipt_id AND provenance.site_id=profile.site_id
|
|
799
|
+
JOIN analytics_consent_current_state cs
|
|
800
|
+
ON cs.site_id=p.site_id AND cs.subject_ref=provenance.subject_ref
|
|
801
|
+
JOIN analytics_consent_receipts receipt
|
|
802
|
+
ON receipt.id=cs.latest_receipt_id AND receipt.site_id=cs.site_id
|
|
803
|
+
WHERE node.site_id=$1 AND node.kind='customer_id' AND node.value_hmac=$2
|
|
804
|
+
AND p.public_ref IS NOT NULL
|
|
805
|
+
AND cs.site_analytics=true AND cs.enhanced_matching=true
|
|
806
|
+
AND cs.gpc=false AND receipt.action NOT IN ('withdraw','gpc')
|
|
807
|
+
AND cs.effective_at <= now()
|
|
808
|
+
AND (cs.expires_at IS NULL OR cs.expires_at > now())
|
|
809
|
+
AND NOT EXISTS (
|
|
810
|
+
SELECT 1 FROM analytics_identity_tombstones tombstone
|
|
811
|
+
WHERE tombstone.site_id=p.site_id AND tombstone.person_id=p.id
|
|
812
|
+
)
|
|
813
|
+
AND NOT EXISTS (
|
|
814
|
+
SELECT 1 FROM analytics_privacy_audits audit
|
|
815
|
+
WHERE audit.site_id=p.site_id AND audit.subject_ref=p.public_ref
|
|
816
|
+
AND audit.action='deletion_requested' AND audit.result_code='running'
|
|
817
|
+
)
|
|
818
|
+
ORDER BY edge.confidence DESC, edge.last_seen_at DESC
|
|
819
|
+
LIMIT 1
|
|
820
|
+
FOR SHARE OF p, ns, cs, receipt`,[t.siteId,Ne("customer_id",t.stableUserId)])).rows[0]??null}async function Lt(e,t){let n=process.env.MCP_SCRAPER_XRAY_SITE_ID?.trim();if(!n)return{status:"skipped",eventName:xe,reason:"site_not_configured"};if(!ds(n))return{status:"skipped",eventName:xe,reason:"site_id_invalid"};let i=String(e.userId).trim();if(!i||!e.invoiceId.trim()||!Number.isSafeInteger(e.amountPaidMinor)||e.amountPaidMinor<0||!/^[a-z]{3}$/i.test(e.currency)||!Number.isFinite(new Date(e.occurredAt).getTime()))throw new Error("analytics_first_party_outcome_invalid");let r=t??{db:v(),record:(a,o)=>ti({...a,transactionClient:o})},s=await r.db.connect();try{await s.query("BEGIN");let a=await ls(s,{siteId:n,stableUserId:i});if(!a)return await s.query("ROLLBACK"),{status:"skipped",eventName:xe,reason:"identity_or_consent_unavailable"};let o={schemaVersion:1,eventId:`mcp_invoice_${e.invoiceId}`.slice(0,200),sourceEventId:`stripe:invoice:${e.invoiceId}`.slice(0,300),source:"mcp_scraper",sourceAccountRef:"first_party_billing",eventKind:"transaction",eventName:xe,occurredAt:e.occurredAt,personRef:i,orderId:e.invoiceId,valueMinor:e.amountPaidMinor,currency:e.currency.toUpperCase(),consent:{analytics:"granted",adUserData:"granted",adPersonalization:a.advertising_measurement?"granted":"denied",globalPrivacyControl:!1,occurredAt:new Date(a.effective_at).toISOString(),receiptId:a.receipt_id,policyVersion:a.policy_version,purposes:{site_analytics:"granted",enhanced_matching:"granted",advertising_measurement:a.advertising_measurement?"granted":"denied"}},properties:{product:"mcp_scraper",billingKind:"base_plan_paid_invoice",revenueProvenance:"stripe_paid_invoice"}},c=await r.record({siteId:n,event:o},s);return await s.query("COMMIT"),{status:"recorded",eventName:xe,reason:"recorded",created:c.created,conversionIds:c.conversionIds}}catch(a){throw await s.query("ROLLBACK").catch(()=>{}),a}finally{s.release()}}function ni(e){let t=e;return t?.price?.id??t?.pricing?.price_details?.price??t?.plan?.id}function Ie(e){if(typeof e=="string")return e;if(e&&typeof e=="object"&&"id"in e)return String(e.id)}function us(e){let t=e.subscription;if(t)return Ie(t);let n=e.parent;return Ie(n?.subscription_details?.subscription)}async function _s(e,t){let n=await M(e);return n||!t||(n=await $e(t),n&&await Pe(n.id,e)),n}async function ms(e,t,n){let i=t.memory_subscription_id;if(!(!i||i===n))try{let r=await e.subscriptions.retrieve(i);r.status!=="canceled"&&!r.cancel_at_period_end&&await e.subscriptions.update(i,{cancel_at_period_end:!0})}catch(r){console.error("[stripe] could not wind down standalone memory subscription",i,r instanceof Error?r.message:r)}}async function $t(e,t,n){if(!t.id||t.status!=="paid")return{status:"not_applicable",invoiceId:t.id??null};let i=t.lines.data.some(y=>{let E=ni(y);return!!E&&E in D});if(!hn(i,t.total))return{status:"not_applicable",invoiceId:t.id};let r=us(t),a=(r?le(await e.subscriptions.retrieve(r))?.price?.id:void 0)??t.lines.data.map(ni).find(y=>!!y&&y in D),o=a?D[a]:void 0;if(!o)return{status:"not_applicable",invoiceId:t.id};let c=Ie(t.customer);if(!c)throw new Error("Paid subscription invoice has no Stripe customer");let d=await _s(c,t.customer_email);if(!d)throw new Error("Paid subscription invoice does not map to an MCP Scraper account");let _=await Ut({invoiceId:t.id,userId:d.id,subscriptionId:r??d.subscription_id,tier:o.tier,concurrency:o.concurrency,creditsMc:o.credits_mc,description:`${o.label} subscription credits`,neverExpires:!!o.credits_never_expire,includedMemoryPlan:o.includes_memory?"pro":null});o.includes_memory&&(await ms(e,d,r??d.subscription_id),await z(d,F({...d,memory_plan:"pro",subscription_tier:o.tier})));let u=await qe(d);u.ok||console.warn("[stripe] scheduled-action credential sync failed:",u.error??"unknown error");try{await Lt({userId:d.id,invoiceId:t.id,amountPaidMinor:t.amount_paid,currency:t.currency,occurredAt:new Date(((t.status_transitions?.paid_at??n)||Math.floor(Date.now()/1e3))*1e3).toISOString()})}catch(y){console.error("[stripe] X-Ray paid outcome failed",{invoiceId:t.id,error:y instanceof Error?y.message:String(y)})}return{status:_.granted?"fulfilled":"already_fulfilled",invoiceId:t.id,userId:d.id,tier:o.tier}}async function ii(e,t,n){let i=await e.checkout.sessions.retrieve(t,{expand:["invoice"]});if(i.mode!=="subscription")throw new Error("Checkout session is not a subscription purchase");if(i.payment_status!=="paid"&&i.payment_status!=="no_payment_required")return{status:"not_applicable",invoiceId:Ie(i.invoice)??null};let r=Ie(i.customer);if(n&&r!==n)throw new Error("Checkout session does not belong to this billing account");let s=typeof i.invoice=="object"&&i.invoice&&!("deleted"in i.invoice)?i.invoice:await e.invoices.retrieve(Ie(i.invoice)??"");if(!s?.id)throw new Error("Paid subscription checkout has no invoice");return $t(e,s,i.created)}import ps from"stripe";import{Hono as ys}from"hono";var $=new ps(process.env.STRIPE_SECRET_KEY,{apiVersion:"2026-02-25.clover"});async function gs(e,t){if(!(!e||e===t))try{let n=await $.subscriptions.retrieve(e),i=n.items.data[0]?.price?.id,r=n.status!=="canceled"&&!n.cancel_at_period_end;i&&i in ce&&r&&await $.subscriptions.update(e,{cancel_at_period_end:!0})}catch(n){console.error("[stripe] could not wind down standalone memory subscription",e,n instanceof Error?n.message:n)}}var fs=new ys;fs.post("/webhooks",async e=>{let t=e.req.header("stripe-signature"),n=await e.req.text(),i;try{i=$.webhooks.constructEvent(n,t,process.env.STRIPE_WEBHOOK_SECRET)}catch{return e.json({error:"Invalid signature"},400)}try{let r=await kt(i.id,i.type,i.created);return e.json({received:!0,disposition:r})}catch(r){let s=r instanceof Error?r.message:String(r);return console.error("[stripe] webhook enqueue failed",i.id,s),e.json({error:"Webhook enqueue failed"},500)}});async function Dc(e){if(e.type==="checkout.session.completed"||e.type==="checkout.session.async_payment_succeeded"){let t=e.data.object;await ii($,t.id);return}if(e.type==="invoice.created"){let t=e.data.object;ri(t)&&t.status==="draft"&&await $.invoices.update(t.id,{description:Bt,footer:Vt,custom_fields:[{name:"Pack benefit",value:"2 browsers per $5 monthly pack"},{name:"Billing cadence",value:"Monthly"}]});return}if(e.type==="invoice.paid"||e.type==="invoice.payment_succeeded"){let t=e.data.object;if(ri(t)){let i=t.customer_email?void 0:await ai(t.customer,void 0),r=t.customer_email??i?.email;if(!r)throw new Error("Paid concurrency invoice has no customer receipt email");await vs(t.id,r);return}if(t.lines.data.map(oi).find(i=>i&&i in ce))return;await $t($,t,e.created)}if(e.type==="customer.subscription.created"||e.type==="customer.subscription.updated"){let t=e.data.object,n=si(t);if(n){let s=await M(t.customer);if(!s)return;let a=Ce(t.status);await nt(s.id,a?Es(n):0),await it(s.id,a?t.id:null);return}let i=le(t)?.price?.id??t.items.data[0]?.price?.id;if(i&&i in ce){let s=ce[i],a=await ai(t.customer,t.customer_email);if(a){let o=Ce(t.status),c=o?s.plan:"free";await oe(a.id,c,o?t.id:null),await z(a,F({...a,memory_plan:c}))}return}let r=await M(t.customer);if(!r)return;if(i&&i in D){let s=D[i],a=Ce(t.status),o=a?s.tier:null;if(await rt(r.id,o,a?s.concurrency:0,a?t.id:null),s.includes_memory){let c=a?"pro":"free";a&&await gs(r.memory_subscription_id,t.id),await oe(r.id,c,null),await z(r,F({...r,memory_plan:c}))}else r.memory_subscription_id===t.id&&(await oe(r.id,"free",null),await z(r,F({...r,memory_plan:"free"})));if(a){let c=await qe(r);c.ok||console.warn("[stripe] scheduled-action credential sync failed:",c.error??"unknown error")}}}if(e.type==="customer.subscription.deleted"){let t=e.data.object;if(si(t)){let i=await M(t.customer);i&&(await nt(i.id,0),await it(i.id,null));return}let n=le(t)?.price?.id??t.items.data[0]?.price?.id;if(n&&n in D){let i=D[n],r=await M(t.customer);r&&(await rt(r.id,null,0,null),(i.includes_memory||r.memory_subscription_id===t.id)&&(await oe(r.id,"free",null),await z(r,F({...r,memory_plan:"free"}))));return}if(n&&n in ce||await st(t.id)){let i=await st(t.id)??await M(t.customer);i&&(await oe(i.id,"free",null),await z(i,F({...i,memory_plan:"free"})));return}}}function ri(e){return e.lines.data.some(t=>oi(t)===at)}async function vs(e,t){let i=(await $.invoicePayments.list({invoice:e,status:"paid",limit:10})).data.find(a=>a.status==="paid");if(!i)throw new Error("Paid concurrency invoice has no paid Stripe payment");let r;if(i.payment.type==="charge")r=i.payment.charge;else if(i.payment.type==="payment_intent"&&i.payment.payment_intent){let a=typeof i.payment.payment_intent=="string"?i.payment.payment_intent:i.payment.payment_intent.id;r=(await $.paymentIntents.retrieve(a,{expand:["latest_charge"]})).latest_charge}if(!r)throw new Error("Paid concurrency invoice has no receipt-capable charge");let s=typeof r=="string"?await $.charges.retrieve(r):r;s.receipt_email!==t&&await $.charges.update(s.id,{receipt_email:t})}function si(e){return e.items.data.find(t=>t.price?.id===at)}function Es(e){let t=e.quantity??1;if(!Number.isSafeInteger(t)||t<1)throw new Error("Stripe concurrency pack quantity must be a positive safe integer");return t}function oi(e){let t=e;return t?.price?.id??t?.pricing?.price_details?.price??t?.plan?.id}async function ai(e,t){let n=await M(e);if(n)return n;if(t)return n=await $e(t),n&&await Pe(n.id,e),n}export{bi as a,Is as b,rn as c,ut as d,Ds as e,Ts as f,an as g,Ti as h,xs as i,Ls as j,xi as k,cn as l,qe as m,pt as n,j as o,le as p,Hs as q,qs as r,Ws as s,Ze as t,Qe as u,Dn as v,gr as w,vr as x,At as y,Ua as z,Fa as A,ja as B,Ha as C,za as D,Pn as E,zs as F,Ys as G,Xs as H,Gs as I,Zs as J,Qs as K,ea as L,ta as M,na as N,ia as O,ra as P,sa as Q,aa as R,_n as S,oa as T,qi as U,ca as V,ee as W,Ke as X,x as Y,da as Z,Vi as _,ma as $,pa as aa,ya as ba,bn as ca,An as da,ga as ea,Ra as fa,Un as ga,Fn as ha,Za as ia,Qa as ja,jn as ka,no as la,Hn as ma,qn as na,Wn as oa,Bn as pa,Vn as qa,Kn as ra,tt as sa,yo as ta,go as ua,fo as va,vo as wa,Eo as xa,bo as ya,zn as za,Yn as Aa,Ot as Ba,Xn as Ca,Gn as Da,Xr as Ea,Ho as Fa,qo as Ga,Wo as Ha,Bo as Ia,Vo as Ja,Ko as Ka,Jo as La,zo as Ma,Yo as Na,Xo as Oa,Go as Pa,Zo as Qa,Qo as Ra,ec as Sa,tc as Ta,nc as Ua,Zn as Va,ic as Wa,rc as Xa,ti as Ya,sc as Za,ac as _a,ii as $a,$ as ab,fs as bb,Dc as cb};
|