@twin3-ai/agent-id 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/b1-sync.js +85 -0
- package/bin/agent-id-b1-sync.js +44 -0
- package/bin/agent-id-cloudflare-a1.js +53 -0
- package/bin/agent-id.js +328 -75
- package/cloudflare-a1-sync.js +105 -0
- package/domain-proof.js +193 -0
- package/edge-html-injection.js +258 -0
- package/enterprise-identity.js +21 -0
- package/installer.js +565 -43
- package/package.json +12 -3
- package/production-preflight.js +35 -4
- package/release-verifier.js +46 -1
- package/repository-connector.js +123 -5
- package/site-agent.js +55 -3
- package/sync-service.js +1 -1
package/b1-sync.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
|
|
7
|
+
const GA4_ENDPOINT = "https://analyticsdata.googleapis.com/v1beta/properties";
|
|
8
|
+
const AI_HOSTS = Object.freeze(["chat.openai.com", "chatgpt.com", "claude.ai", "copilot.microsoft.com", "gemini.google.com", "perplexity.ai"]);
|
|
9
|
+
|
|
10
|
+
function hash(value) { return crypto.createHash("sha256").update(String(value)).digest("hex"); }
|
|
11
|
+
function emptyState() { return { schema: "agentx-b1-sync-state-v1", cursor: "", pending: null, seen: [], retry_at: 0, attempts: 0, next_run_at: 0, last_sync_at: 0, last_error: null }; }
|
|
12
|
+
function loadState(statePath) {
|
|
13
|
+
try { return { ...emptyState(), ...JSON.parse(fs.readFileSync(statePath, "utf8")) }; }
|
|
14
|
+
catch (error) { if (error.code === "ENOENT") return emptyState(); throw new Error("b1_sync_state_invalid"); }
|
|
15
|
+
}
|
|
16
|
+
function saveState(statePath, state) {
|
|
17
|
+
const target = path.resolve(statePath); fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
18
|
+
const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
19
|
+
try { fs.writeFileSync(temporary, JSON.stringify(state, null, 2) + "\n", { mode:0o600, flag:"wx" }); fs.renameSync(temporary, target); fs.chmodSync(target, 0o600); }
|
|
20
|
+
finally { try { fs.rmSync(temporary, { force:true }); } catch (_error) {} }
|
|
21
|
+
}
|
|
22
|
+
function aiHost(value) {
|
|
23
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
24
|
+
if (!raw || ["(direct)", "(none)", "(not set)"].includes(raw)) return "";
|
|
25
|
+
try {
|
|
26
|
+
const host = new URL(raw.includes("://") ? raw : `https://${raw}`).hostname.replace(/\.$/, "");
|
|
27
|
+
return AI_HOSTS.find((known) => host === known || host.endsWith(`.${known}`)) ? host : "";
|
|
28
|
+
} catch (_error) { return ""; }
|
|
29
|
+
}
|
|
30
|
+
function safeLabel(value) { return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(String(value || "")) ? String(value) : ""; }
|
|
31
|
+
function reportMap(row, dimensionHeaders, metricHeaders) {
|
|
32
|
+
const dimensions = Object.fromEntries(dimensionHeaders.map((name, index) => [name, String(row.dimensionValues && row.dimensionValues[index] && row.dimensionValues[index].value || "")]));
|
|
33
|
+
const metrics = Object.fromEntries(metricHeaders.map((name, index) => [name, String(row.metricValues && row.metricValues[index] && row.metricValues[index].value || "0")]));
|
|
34
|
+
return { dimensions, metrics };
|
|
35
|
+
}
|
|
36
|
+
function ga4Conversions(response, siteUrl, windowKey) {
|
|
37
|
+
const dimensions = (response.dimensionHeaders || []).map((item) => item && item.name).filter(Boolean);
|
|
38
|
+
const metrics = (response.metricHeaders || []).map((item) => item && item.name).filter(Boolean);
|
|
39
|
+
return (response.rows || []).flatMap((row) => {
|
|
40
|
+
const mapped = reportMap(row || {}, dimensions, metrics);
|
|
41
|
+
const source = aiHost(mapped.dimensions.sessionSource);
|
|
42
|
+
const count = Math.max(0, Math.floor(Number(mapped.metrics.keyEvents || 0)));
|
|
43
|
+
const revenue = Math.max(0, Number(mapped.metrics.totalRevenue || 0));
|
|
44
|
+
if (!source || count < 1 || !Number.isFinite(revenue)) return [];
|
|
45
|
+
const campaign = safeLabel(mapped.dimensions.sessionCampaignName);
|
|
46
|
+
const currency = String(response.currencyCode || response.metadata && response.metadata.currencyCode || "USD").toUpperCase();
|
|
47
|
+
return [{ conversion_id:`ga4_${hash(JSON.stringify([windowKey, source, campaign])).slice(0,32)}`, event:"ga4_key_event", url:siteUrl, referrer:`https://${source}/`, utm_source:source, utm_campaign:campaign, conversion_count:count, value:Number(revenue.toFixed(2)), currency:/^[A-Z]{3}$/.test(currency)?currency:"USD", timestamp:Date.parse(`${windowKey}T23:59:59Z`) }];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function defaultFetchGa4({ token, propertyId, startDate, endDate, fetchImpl = globalThis.fetch }) {
|
|
51
|
+
if (typeof fetchImpl !== "function") throw new Error("ga4_fetch_unavailable");
|
|
52
|
+
const response = await fetchImpl(`${GA4_ENDPOINT}/${encodeURIComponent(propertyId)}:runReport`, { method:"POST", headers:{ authorization:`Bearer ${token}`, "content-type":"application/json" }, body:JSON.stringify({ dateRanges:[{startDate,endDate}], dimensions:[{name:"sessionSource"},{name:"sessionMedium"},{name:"sessionCampaignName"}], metrics:[{name:"sessions"},{name:"keyEvents"},{name:"totalRevenue"}] }), redirect:"error" });
|
|
53
|
+
if (!response.ok) throw new Error(`ga4_http_${response.status}`);
|
|
54
|
+
const body = await response.json(); if (body.error) throw new Error("ga4_provider_error"); return body;
|
|
55
|
+
}
|
|
56
|
+
function createBaseSync({ client, statePath, siteUrl, intervalMs, collect, now, random }) {
|
|
57
|
+
if (!client || typeof client.conversions !== "function" || !statePath || !siteUrl || typeof collect !== "function") throw new Error("b1_sync_configuration_invalid");
|
|
58
|
+
const url = new URL(siteUrl).origin; const state = loadState(statePath); const persist = () => saveState(statePath, state);
|
|
59
|
+
async function upload(batch, cursor) {
|
|
60
|
+
try {
|
|
61
|
+
const result = batch.length ? await client.conversions({ url, conversions:batch, source:"b1_automatic_sync" }) : { stored:0 };
|
|
62
|
+
state.pending=null; state.cursor=cursor; state.seen=[...new Set([...state.seen,...batch.map(item=>item.conversion_id)])].slice(-10000); state.retry_at=0; state.attempts=0; state.last_sync_at=now(); state.next_run_at=state.last_sync_at+Math.max(60000,intervalMs); state.last_error=null; persist();
|
|
63
|
+
return {ok:true,status:"synced",rows:batch.length,conversions:batch.reduce((sum,item)=>sum+Math.max(1,Number(item.conversion_count||1)),0),stored:Number(result.stored||0),cursor};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
state.pending={batch:batch.slice(0,5000),cursor}; state.attempts+=1; const backoff=Math.min(300000,1000*(2**Math.min(8,state.attempts-1))); state.retry_at=now()+Math.round(backoff*(0.5+random())); state.last_error={code:String(error.code||"b1_delivery_failed").slice(0,80)}; persist();
|
|
66
|
+
return {ok:false,status:"queued_for_retry",pending:state.pending.batch.length,retry_at:state.retry_at};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function runOnce({force=false}={}) {
|
|
70
|
+
const timestamp=now(); if(!force&&state.retry_at>timestamp)return {ok:false,status:"retry_wait",retry_at:state.retry_at}; if(state.pending)return upload(state.pending.batch,state.pending.cursor); if(!force&&state.next_run_at>timestamp)return {ok:true,status:"not_due",next_run_at:state.next_run_at};
|
|
71
|
+
const collected=await collect({timestamp,cursor:state.cursor,seen:new Set(state.seen)}); return upload(collected.batch||[],collected.cursor||state.cursor);
|
|
72
|
+
}
|
|
73
|
+
async function status(){return {schema:state.schema,host:new URL(url).hostname,cursor:state.cursor,pending:state.pending?state.pending.batch.length:0,retry_at:state.retry_at,next_run_at:state.next_run_at,last_sync_at:state.last_sync_at,last_error:state.last_error};}
|
|
74
|
+
return Object.freeze({runOnce,status});
|
|
75
|
+
}
|
|
76
|
+
function createGa4B1Sync({client,statePath,siteUrl,propertyId,accessToken,fetchGa4=defaultFetchGa4,intervalMs=86400000,now=Date.now,random=Math.random}={}) {
|
|
77
|
+
if(!propertyId||!accessToken)throw new Error("ga4_b1_configuration_invalid");
|
|
78
|
+
return createBaseSync({client,statePath,siteUrl,intervalMs,now,random,collect:async({timestamp,cursor})=>{const date=new Date(timestamp-86400000).toISOString().slice(0,10);if(cursor===date)return {batch:[],cursor};const response=await fetchGa4({token:accessToken,propertyId,startDate:date,endDate:date});return {batch:ga4Conversions(response,new URL(siteUrl).origin,date),cursor:date};}});
|
|
79
|
+
}
|
|
80
|
+
function createOrderB1Sync({client,statePath,siteUrl,sourceId,loadOrders,intervalMs=3600000,now=Date.now,random=Math.random}={}) {
|
|
81
|
+
if(!safeLabel(sourceId)||typeof loadOrders!=="function")throw new Error("order_b1_configuration_invalid");
|
|
82
|
+
return createBaseSync({client,statePath,siteUrl,intervalMs,now,random,collect:async({timestamp,seen})=>{const rows=await loadOrders();const batch=(Array.isArray(rows)?rows:[]).flatMap((row)=>{const source=aiHost(row&&row.source);const rawId=String(row&&row.order_id||"");const receipt=rawId?`order_${hash(`${sourceId}:${rawId}`).slice(0,32)}`:"";const amount=Number(row&&row.amount||0);if(!source||!receipt||seen.has(receipt)||!Number.isFinite(amount)||amount<0)return [];return [{conversion_id:receipt,event:safeLabel(row.event)||"order",url:new URL(siteUrl).origin,referrer:`https://${source}/`,value:Number(amount.toFixed(2)),currency:/^[A-Z]{3}$/.test(String(row.currency||"").toUpperCase())?String(row.currency).toUpperCase():"USD",timestamp:Number(row.timestamp||timestamp)}];});return {batch,cursor:String(timestamp)};}});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports={createGa4B1Sync,createOrderB1Sync,ga4Conversions,defaultFetchGa4};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { createSiteAgentClient } = require("../site-agent.js");
|
|
7
|
+
const { createGa4B1Sync, createOrderB1Sync } = require("../b1-sync.js");
|
|
8
|
+
|
|
9
|
+
function valueAfter(args, name, fallback = "") { const index=args.indexOf(name); return index>=0&&args[index+1]?args[index+1]:fallback; }
|
|
10
|
+
function help() {
|
|
11
|
+
console.log(`agent-id-b1-sync
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
agent-id-b1-sync --ga4 --url https://example.com
|
|
15
|
+
agent-id-b1-sync --orders ./crm-orders.json --source-id crm-main --url https://example.com
|
|
16
|
+
|
|
17
|
+
Shared environment:
|
|
18
|
+
SITE_AGENT_KEY Agent ID Site Agent Key
|
|
19
|
+
AGENT_ID_ENDPOINT Parent Agent ID origin
|
|
20
|
+
AGENT_ID_B1_STATE Local cursor and retry state path
|
|
21
|
+
|
|
22
|
+
GA4 environment:
|
|
23
|
+
GA4_PROPERTY_ID GA4 numeric property ID
|
|
24
|
+
GA4_ACCESS_TOKEN Read-only Analytics Data API OAuth token
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
async function main() {
|
|
28
|
+
const args=process.argv.slice(2); if(args.includes("--help")||args.includes("-h"))return help();
|
|
29
|
+
const siteUrl=valueAfter(args,"--url",process.env.SITE_URL||""); if(!siteUrl)throw new Error("--url or SITE_URL is required");
|
|
30
|
+
const endpoint=valueAfter(args,"--endpoint",process.env.AGENT_ID_ENDPOINT||"");
|
|
31
|
+
const statePath=valueAfter(args,"--state",process.env.AGENT_ID_B1_STATE||path.join(process.cwd(),".agent-id","b1-sync-state.json"));
|
|
32
|
+
const client=createSiteAgentClient({endpoint,agentKey:process.env.SITE_AGENT_KEY});
|
|
33
|
+
let sync;
|
|
34
|
+
if(args.includes("--ga4")) {
|
|
35
|
+
sync=createGa4B1Sync({client,statePath,siteUrl,propertyId:process.env.GA4_PROPERTY_ID,accessToken:process.env.GA4_ACCESS_TOKEN});
|
|
36
|
+
} else {
|
|
37
|
+
const orderPath=valueAfter(args,"--orders",""); const sourceId=valueAfter(args,"--source-id","");
|
|
38
|
+
if(!orderPath||!sourceId)throw new Error("--ga4 or both --orders and --source-id are required");
|
|
39
|
+
sync=createOrderB1Sync({client,statePath,siteUrl,sourceId,loadOrders:async()=>JSON.parse(fs.readFileSync(path.resolve(orderPath),"utf8"))});
|
|
40
|
+
}
|
|
41
|
+
const result=args.includes("--status")?await sync.status():await sync.runOnce({force:args.includes("--force")});
|
|
42
|
+
console.log(JSON.stringify(result,null,2));
|
|
43
|
+
}
|
|
44
|
+
main().catch((error)=>{console.error(error&&error.message?error.message:"b1_sync_failed");process.exit(1);});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { createSiteAgentClient } = require("../site-agent.js");
|
|
6
|
+
const { createCloudflareA1Sync } = require("../cloudflare-a1-sync.js");
|
|
7
|
+
|
|
8
|
+
function valueAfter(args, name, fallback = "") {
|
|
9
|
+
const index = args.indexOf(name);
|
|
10
|
+
return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
|
|
11
|
+
}
|
|
12
|
+
function help() {
|
|
13
|
+
console.log(`agent-id-cloudflare-a1
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
agent-id-cloudflare-a1 --url https://example.com --once
|
|
17
|
+
agent-id-cloudflare-a1 --url https://example.com --status
|
|
18
|
+
|
|
19
|
+
Required environment:
|
|
20
|
+
SITE_AGENT_KEY Agent ID Site Agent Key
|
|
21
|
+
CLOUDFLARE_API_TOKEN Read-only Cloudflare Analytics token
|
|
22
|
+
CLOUDFLARE_ZONE_ID Website zone ID
|
|
23
|
+
|
|
24
|
+
Optional environment:
|
|
25
|
+
AGENT_ID_ENDPOINT Parent Agent ID origin
|
|
26
|
+
AGENT_ID_A1_STATE Local cursor and retry state path
|
|
27
|
+
AGENT_ID_A1_INTERVAL_MS Minimum interval between successful syncs
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
async function main() {
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
if (args.includes("--help") || args.includes("-h")) return help();
|
|
33
|
+
const siteUrl = valueAfter(args, "--url", process.env.SITE_URL || "");
|
|
34
|
+
const endpoint = valueAfter(args, "--endpoint", process.env.AGENT_ID_ENDPOINT || "");
|
|
35
|
+
const statePath = valueAfter(args, "--state", process.env.AGENT_ID_A1_STATE || path.join(process.cwd(), ".agent-id", "cloudflare-a1-state.json"));
|
|
36
|
+
if (!siteUrl) throw new Error("--url or SITE_URL is required");
|
|
37
|
+
const client = createSiteAgentClient({ endpoint, agentKey: process.env.SITE_AGENT_KEY });
|
|
38
|
+
const sync = createCloudflareA1Sync({
|
|
39
|
+
client,
|
|
40
|
+
statePath,
|
|
41
|
+
siteUrl,
|
|
42
|
+
zoneId: process.env.CLOUDFLARE_ZONE_ID,
|
|
43
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
|
44
|
+
intervalMs: Number(process.env.AGENT_ID_A1_INTERVAL_MS || 900000)
|
|
45
|
+
});
|
|
46
|
+
const result = args.includes("--status") ? await sync.status() : await sync.runOnce({ force: args.includes("--force") });
|
|
47
|
+
console.log(JSON.stringify(result, null, 2));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch((error) => {
|
|
51
|
+
console.error(error && error.message ? error.message : "cloudflare_a1_sync_failed");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|