mcp-scraper 0.84.0 → 0.86.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.
Files changed (26) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +2 -2
  3. package/dist/{analytics-repository-4VZQ4TKW.js → analytics-repository-GGJJCVVP.js} +9 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.js +3 -3
  6. package/dist/bin/mcp-scraper-core.js +6 -6
  7. package/dist/bin/mcp-scraper-install.js +2 -2
  8. package/dist/bin/mcp-stdio-server.js +6 -6
  9. package/dist/{chunk-OUTE3755.js → chunk-36EAKRR6.js} +1 -1
  10. package/dist/{chunk-LHIWX7RM.js → chunk-6IOGTFNS.js} +1 -1
  11. package/dist/{chunk-NLWBM55O.js → chunk-7523VOQT.js} +2 -2
  12. package/dist/{chunk-U77X4NIR.js → chunk-FBB6DEUV.js} +5735 -292
  13. package/dist/{chunk-ZB55UMMF.js → chunk-KQWVJOVR.js} +1 -1
  14. package/dist/{chunk-UM7DCY6W.js → chunk-NKRO2SUO.js} +3 -3
  15. package/dist/{chunk-4S6HPHPB.js → chunk-P7FWOMU7.js} +6 -9
  16. package/dist/{chunk-WDWWCNMW.js → chunk-QKWNEMGH.js} +3 -3
  17. package/dist/{chunk-4KFD2WXG.js → chunk-WFGAR4BZ.js} +33 -7
  18. package/dist/{chunk-TLRJV4SL.js → chunk-X623GTBV.js} +427 -155
  19. package/dist/{extract-bundle-SI35H4HR.js → extract-bundle-FALNLKYY.js} +2 -2
  20. package/dist/{gmail-service-MOZ6S56I.js → gmail-service-BUSU4BVX.js} +2 -2
  21. package/dist/{server-3F3JMHBF.js → server-OZBTZIDZ.js} +1518 -1249
  22. package/dist/{site-extract-repository-3WC3VB5Y.js → site-extract-repository-S5FYNFDY.js} +2 -2
  23. package/dist/{worker-VTFGHWZL.js → worker-OQSFSLNG.js} +1 -1
  24. package/package.json +15 -4
  25. package/public/.well-known/xray-install.json +5 -8
  26. package/public/agent-install.md +4 -8
@@ -1688,6 +1688,20 @@ async function migrateAnalytics() {
1688
1688
  await client.query(
1689
1689
  `ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS crm_delivery_status text NOT NULL DEFAULT 'pending'`
1690
1690
  );
1691
+ await client.query(
1692
+ `ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS crm_entity_id uuid`
1693
+ );
1694
+ await client.query(
1695
+ `ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS bridge_run_id uuid`
1696
+ );
1697
+ await client.query(
1698
+ `ALTER TABLE analytics_form_submissions ADD COLUMN IF NOT EXISTS idempotency_key text`
1699
+ );
1700
+ await client.query(
1701
+ `CREATE UNIQUE INDEX IF NOT EXISTS analytics_form_submissions_idempotency
1702
+ ON analytics_form_submissions(form_id, idempotency_key)
1703
+ WHERE idempotency_key IS NOT NULL`
1704
+ );
1691
1705
  await client.query(`
1692
1706
  CREATE TABLE IF NOT EXISTS analytics_form_delivery_jobs (
1693
1707
  id uuid PRIMARY KEY,
@@ -1703,6 +1717,29 @@ async function migrateAnalytics() {
1703
1717
  delivered_at timestamptz
1704
1718
  )
1705
1719
  `);
1720
+ await client.query(
1721
+ `ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS bridge_run_id uuid`
1722
+ );
1723
+ await client.query(
1724
+ `ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS crm_entity_id uuid`
1725
+ );
1726
+ await client.query(
1727
+ `ALTER TABLE analytics_form_delivery_jobs ADD COLUMN IF NOT EXISTS idempotency_key text`
1728
+ );
1729
+ await client.query(`
1730
+ CREATE TABLE IF NOT EXISTS analytics_form_bridge_receipts (
1731
+ id uuid PRIMARY KEY,
1732
+ bridge_run_id uuid NOT NULL,
1733
+ submission_id uuid NOT NULL REFERENCES analytics_form_submissions(id) ON DELETE CASCADE,
1734
+ system text NOT NULL CHECK (system IN ('analytics','crm')),
1735
+ status text NOT NULL CHECK (status IN ('pending','delivered','failed','expired')),
1736
+ opaque_entity_id uuid,
1737
+ safe_error_code text,
1738
+ occurred_at timestamptz NOT NULL DEFAULT now(),
1739
+ updated_at timestamptz NOT NULL DEFAULT now(),
1740
+ UNIQUE(bridge_run_id, system)
1741
+ )
1742
+ `);
1706
1743
  await client.query(`
1707
1744
  CREATE TABLE IF NOT EXISTS analytics_people (
1708
1745
  id uuid PRIMARY KEY,
@@ -1713,6 +1750,14 @@ async function migrateAnalytics() {
1713
1750
  UNIQUE(site_id, crm_person_ref)
1714
1751
  )
1715
1752
  `);
1753
+ await client.query(
1754
+ `ALTER TABLE analytics_people ADD COLUMN IF NOT EXISTS crm_entity_id uuid`
1755
+ );
1756
+ await client.query(
1757
+ `CREATE UNIQUE INDEX IF NOT EXISTS analytics_people_crm_entity
1758
+ ON analytics_people(site_id, crm_entity_id)
1759
+ WHERE crm_entity_id IS NOT NULL`
1760
+ );
1716
1761
  await client.query(`
1717
1762
  CREATE TABLE IF NOT EXISTS analytics_identity_nodes (
1718
1763
  id uuid PRIMARY KEY,
@@ -1738,6 +1783,72 @@ async function migrateAnalytics() {
1738
1783
  UNIQUE(person_id, identity_node_id, evidence_kind)
1739
1784
  )
1740
1785
  `);
1786
+ await client.query(`
1787
+ CREATE TABLE IF NOT EXISTS analytics_identity_assignments (
1788
+ identity_node_id uuid PRIMARY KEY REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
1789
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1790
+ person_id uuid NOT NULL REFERENCES analytics_people(id) ON DELETE CASCADE,
1791
+ crm_entity_id uuid,
1792
+ evidence_kind text NOT NULL,
1793
+ assigned_at timestamptz NOT NULL DEFAULT now(),
1794
+ updated_at timestamptz NOT NULL DEFAULT now(),
1795
+ CHECK (crm_entity_id IS NULL OR crm_entity_id::text <> '')
1796
+ )
1797
+ `);
1798
+ await client.query(`
1799
+ CREATE TABLE IF NOT EXISTS analytics_identity_assignment_conflicts (
1800
+ id uuid PRIMARY KEY,
1801
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
1802
+ identity_node_id uuid REFERENCES analytics_identity_nodes(id) ON DELETE CASCADE,
1803
+ active_person_id uuid REFERENCES analytics_people(id) ON DELETE CASCADE,
1804
+ attempted_person_id uuid REFERENCES analytics_people(id) ON DELETE CASCADE,
1805
+ active_crm_entity_id uuid,
1806
+ attempted_crm_entity_id uuid,
1807
+ evidence_kind text NOT NULL,
1808
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','resolved','dismissed')),
1809
+ observed_at timestamptz NOT NULL DEFAULT now(),
1810
+ resolved_at timestamptz,
1811
+ CONSTRAINT analytics_identity_assignment_conflicts_pending_identity
1812
+ CHECK (status <> 'pending' OR (
1813
+ identity_node_id IS NOT NULL AND active_person_id IS NOT NULL
1814
+ AND attempted_person_id IS NOT NULL AND active_person_id <> attempted_person_id
1815
+ ))
1816
+ )
1817
+ `);
1818
+ await client.query(
1819
+ `ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN identity_node_id DROP NOT NULL`
1820
+ );
1821
+ await client.query(
1822
+ `ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN active_person_id DROP NOT NULL`
1823
+ );
1824
+ await client.query(
1825
+ `ALTER TABLE analytics_identity_assignment_conflicts ALTER COLUMN attempted_person_id DROP NOT NULL`
1826
+ );
1827
+ await client.query(
1828
+ `ALTER TABLE analytics_identity_assignment_conflicts
1829
+ DROP CONSTRAINT IF EXISTS analytics_identity_assignment_conflicts_check`
1830
+ );
1831
+ await client.query(`
1832
+ DO $$ BEGIN
1833
+ IF NOT EXISTS (
1834
+ SELECT 1 FROM pg_constraint
1835
+ WHERE conrelid='analytics_identity_assignment_conflicts'::regclass
1836
+ AND conname='analytics_identity_assignment_conflicts_pending_identity'
1837
+ ) THEN
1838
+ ALTER TABLE analytics_identity_assignment_conflicts
1839
+ ADD CONSTRAINT analytics_identity_assignment_conflicts_pending_identity
1840
+ CHECK (status <> 'pending' OR (
1841
+ identity_node_id IS NOT NULL AND active_person_id IS NOT NULL
1842
+ AND attempted_person_id IS NOT NULL AND active_person_id <> attempted_person_id
1843
+ ));
1844
+ END IF;
1845
+ END $$
1846
+ `);
1847
+ await client.query(
1848
+ `CREATE UNIQUE INDEX IF NOT EXISTS analytics_identity_assignment_conflicts_pending
1849
+ ON analytics_identity_assignment_conflicts(identity_node_id, active_person_id, attempted_person_id)
1850
+ WHERE status='pending'`
1851
+ );
1741
1852
  await client.query(`
1742
1853
  CREATE TABLE IF NOT EXISTS analytics_linker_tokens (
1743
1854
  id uuid PRIMARY KEY,
@@ -4131,9 +4242,8 @@ async function prepareAnalyticsLinkerIssue(input) {
4131
4242
  [randomUUID2(), siteId, identityHmac("visitor_id", input.visitorId)]
4132
4243
  );
4133
4244
  const existingPerson = await client.query(
4134
- `SELECT person_id FROM analytics_identity_edges
4135
- WHERE site_id=$1 AND identity_node_id=$2
4136
- ORDER BY confidence DESC, first_seen_at ASC LIMIT 1`,
4245
+ `SELECT person_id FROM analytics_identity_assignments
4246
+ WHERE site_id=$1 AND identity_node_id=$2`,
4137
4247
  [siteId, node.rows[0].id]
4138
4248
  );
4139
4249
  let personId = existingPerson.rows[0]?.person_id;
@@ -4151,6 +4261,12 @@ async function prepareAnalyticsLinkerIssue(input) {
4151
4261
  ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,
4152
4262
  [randomUUID2(), siteId, personId, node.rows[0].id]
4153
4263
  );
4264
+ await assignAnalyticsIdentityNode(client, {
4265
+ siteId,
4266
+ personId,
4267
+ identityNodeId: node.rows[0].id,
4268
+ evidenceKind: "first_party_linker"
4269
+ });
4154
4270
  }
4155
4271
  const tokenId = randomUUID2();
4156
4272
  await client.query(
@@ -4195,9 +4311,8 @@ async function redeemAnalyticsLinkerRecord(input) {
4195
4311
  409
4196
4312
  );
4197
4313
  const sourceEdge = await client.query(
4198
- `SELECT person_id FROM analytics_identity_edges
4199
- WHERE site_id=$1 AND identity_node_id=$2
4200
- ORDER BY confidence DESC, first_seen_at ASC LIMIT 1`,
4314
+ `SELECT person_id FROM analytics_identity_assignments
4315
+ WHERE site_id=$1 AND identity_node_id=$2`,
4201
4316
  [input.siteId, input.identityNodeId]
4202
4317
  );
4203
4318
  const personId = sourceEdge.rows[0]?.person_id;
@@ -4216,6 +4331,18 @@ async function redeemAnalyticsLinkerRecord(input) {
4216
4331
  ON CONFLICT(person_id, identity_node_id, evidence_kind) DO UPDATE SET last_seen_at=now()`,
4217
4332
  [randomUUID2(), input.siteId, personId, targetNode.rows[0].id]
4218
4333
  );
4334
+ const targetAssignment = await assignAnalyticsIdentityNode(client, {
4335
+ siteId: input.siteId,
4336
+ personId,
4337
+ identityNodeId: targetNode.rows[0].id,
4338
+ evidenceKind: "cross_domain_linker"
4339
+ });
4340
+ if (targetAssignment === "conflict")
4341
+ throw new AnalyticsRepositoryError(
4342
+ "identity_assignment_conflict",
4343
+ "The destination identity is already assigned and requires review.",
4344
+ 409
4345
+ );
4219
4346
  await client.query(
4220
4347
  "UPDATE analytics_events SET person_id=$1 WHERE site_id=$2 AND visitor_id=$3 AND person_id IS NULL",
4221
4348
  [personId, input.siteId, input.visitorId]
@@ -6256,6 +6383,12 @@ async function backfillAnalyticsJourneyLinkageHmacs(client) {
6256
6383
  function newAnalyticsPersonPublicRef() {
6257
6384
  return `xrp_${randomBytes2(18).toString("base64url")}`;
6258
6385
  }
6386
+ function deterministicAnalyticsCrmEntityId(siteId, stableIdentity) {
6387
+ const hex = identityHmac("crm_entity_id", `${siteId}:${stableIdentity}`).slice(0, 32).split("");
6388
+ hex[12] = "5";
6389
+ hex[16] = ["8", "9", "a", "b"][Number.parseInt(hex[16], 16) % 4];
6390
+ return `${hex.slice(0, 8).join("")}-${hex.slice(8, 12).join("")}-${hex.slice(12, 16).join("")}-${hex.slice(16, 20).join("")}-${hex.slice(20).join("")}`;
6391
+ }
6259
6392
  async function ensurePrimaryAnalyticsIdentityNamespace(client, siteId, source) {
6260
6393
  let namespace = await client.query(
6261
6394
  "SELECT id FROM analytics_identity_namespaces WHERE site_id=$1 AND status='approved' ORDER BY version DESC LIMIT 1",
@@ -6347,142 +6480,107 @@ async function backfillAnalyticsConfirmedHistory(client, input) {
6347
6480
  };
6348
6481
  }
6349
6482
  async function resolveAndMergeConfirmedAnalyticsPerson(client, input) {
6350
- const matches = await client.query(
6351
- `SELECT p.id
6352
- FROM analytics_people p
6353
- LEFT JOIN (
6354
- SELECT DISTINCT e.person_id
6355
- FROM analytics_identity_nodes n
6356
- JOIN analytics_identity_edges e ON e.identity_node_id=n.id AND e.site_id=n.site_id
6357
- WHERE n.site_id=$1 AND EXISTS (
6358
- SELECT 1 FROM jsonb_to_recordset($2::jsonb) signal(kind text,value_hmac text)
6359
- WHERE signal.kind=n.kind AND signal.value_hmac=n.value_hmac
6360
- )
6361
- ) matched ON matched.person_id=p.id
6362
- WHERE p.site_id=$1 AND (p.namespace_id=$3 OR p.namespace_id IS NULL)
6363
- AND (matched.person_id IS NOT NULL OR p.crm_person_ref=$4)
6364
- ORDER BY CASE WHEN p.crm_person_ref LIKE 'XRay::anonymous::%' THEN 1 ELSE 0 END,
6365
- p.first_seen_at ASC,p.id ASC
6366
- FOR UPDATE OF p`,
6367
- [
6368
- input.siteId,
6369
- JSON.stringify(input.signals.map((signal) => ({ kind: signal.kind, value_hmac: signal.valueHmac }))),
6370
- input.namespaceId,
6371
- input.crmPersonRef
6372
- ]
6373
- );
6374
- const canonicalId = matches.rows[0]?.id;
6375
- if (!canonicalId) {
6483
+ const explicit = await client.query(
6484
+ `SELECT id FROM analytics_people
6485
+ WHERE site_id=$1 AND (namespace_id=$2 OR namespace_id IS NULL)
6486
+ AND (($3::uuid IS NOT NULL AND crm_entity_id=$3::uuid)
6487
+ OR ($3::uuid IS NULL AND $4::text IS NOT NULL AND crm_person_ref=$4))
6488
+ ORDER BY first_seen_at,id LIMIT 1 FOR UPDATE`,
6489
+ [input.siteId, input.namespaceId, input.crmEntityId ?? null, input.crmPersonRef ?? null]
6490
+ );
6491
+ let personId = explicit.rows[0]?.id;
6492
+ if (!personId && !input.crmEntityId && !input.crmPersonRef && input.signals.length) {
6493
+ const assigned = await client.query(
6494
+ `SELECT assignment.person_id
6495
+ FROM analytics_identity_nodes node
6496
+ JOIN analytics_identity_assignments assignment
6497
+ ON assignment.identity_node_id=node.id AND assignment.site_id=node.site_id
6498
+ WHERE node.site_id=$1 AND EXISTS (
6499
+ SELECT 1 FROM jsonb_to_recordset($2::jsonb) signal(kind text,value_hmac text)
6500
+ WHERE signal.kind=node.kind AND signal.value_hmac=node.value_hmac
6501
+ )
6502
+ ORDER BY assignment.assigned_at,assignment.person_id LIMIT 1`,
6503
+ [input.siteId, JSON.stringify(input.signals.map((signal) => ({ kind: signal.kind, value_hmac: signal.valueHmac })))]
6504
+ );
6505
+ personId = assigned.rows[0]?.person_id;
6506
+ }
6507
+ if (!personId) {
6508
+ const entityId = input.crmEntityId ?? null;
6509
+ const compatibilityRef = input.crmPersonRef ?? (entityId ? `XRay::entity::${entityId}` : `XRay::anonymous::${randomUUID2()}`);
6376
6510
  const inserted = await client.query(
6377
- `INSERT INTO analytics_people(id, site_id, namespace_id, crm_person_ref, public_ref) VALUES ($1,$2,$3,$4,$5)
6378
- ON CONFLICT(site_id, crm_person_ref) DO UPDATE SET last_seen_at = now(),
6511
+ `INSERT INTO analytics_people(id,site_id,namespace_id,crm_person_ref,crm_entity_id,public_ref)
6512
+ VALUES($1,$2,$3,$4,$5::uuid,$6)
6513
+ ON CONFLICT(site_id,crm_person_ref) DO UPDATE SET last_seen_at=now(),
6379
6514
  namespace_id=COALESCE(analytics_people.namespace_id,EXCLUDED.namespace_id),
6380
- public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref) RETURNING id`,
6381
- [randomUUID2(), input.siteId, input.namespaceId, input.crmPersonRef, newAnalyticsPersonPublicRef()]
6515
+ crm_entity_id=COALESCE(analytics_people.crm_entity_id,EXCLUDED.crm_entity_id),
6516
+ public_ref=COALESCE(analytics_people.public_ref,EXCLUDED.public_ref)
6517
+ RETURNING id`,
6518
+ [randomUUID2(), input.siteId, input.namespaceId, compatibilityRef, entityId, newAnalyticsPersonPublicRef()]
6382
6519
  );
6383
- return { personId: inserted.rows[0].id, mergedPersonIds: [] };
6384
- }
6385
- await client.query(
6386
- `UPDATE analytics_people SET last_seen_at=now(),
6387
- namespace_id=COALESCE(namespace_id,$2),public_ref=COALESCE(public_ref,$3),
6388
- crm_person_ref=CASE WHEN crm_person_ref LIKE 'XRay::anonymous::%' THEN $5 ELSE crm_person_ref END
6389
- WHERE id=$1 AND site_id=$4`,
6390
- [canonicalId, input.namespaceId, newAnalyticsPersonPublicRef(), input.siteId, input.crmPersonRef]
6391
- );
6392
- const duplicateIds = matches.rows.slice(1).map((row) => row.id);
6393
- if (duplicateIds.length === 0) return { personId: canonicalId, mergedPersonIds: [] };
6394
- await client.query(
6395
- `DELETE FROM analytics_identity_edges duplicate
6396
- USING analytics_identity_edges canonical
6397
- WHERE duplicate.site_id=$1 AND duplicate.person_id=ANY($2::uuid[])
6398
- AND canonical.site_id=duplicate.site_id AND canonical.person_id=$3
6399
- AND canonical.identity_node_id=duplicate.identity_node_id
6400
- AND canonical.evidence_kind=duplicate.evidence_kind`,
6401
- [input.siteId, duplicateIds, canonicalId]
6402
- );
6403
- await client.query(
6404
- `UPDATE analytics_identity_edges SET person_id=$3,namespace_id=$4
6405
- WHERE site_id=$1 AND person_id=ANY($2::uuid[])`,
6406
- [input.siteId, duplicateIds, canonicalId, input.namespaceId]
6407
- );
6408
- for (const table of [
6409
- "analytics_form_submissions",
6410
- "analytics_events",
6411
- "analytics_conversions",
6412
- "analytics_sessions",
6413
- "analytics_attribution_touches",
6414
- "analytics_external_events",
6415
- "analytics_restricted_data",
6416
- "analytics_crm_outbound_jobs"
6417
- ]) {
6520
+ personId = inserted.rows[0].id;
6521
+ } else {
6418
6522
  await client.query(
6419
- `UPDATE ${table} SET person_id=$3 WHERE site_id=$1 AND person_id=ANY($2::uuid[])`,
6420
- [input.siteId, duplicateIds, canonicalId]
6523
+ `UPDATE analytics_people SET last_seen_at=now(),
6524
+ namespace_id=COALESCE(namespace_id,$2),
6525
+ crm_entity_id=COALESCE(crm_entity_id,$3::uuid),
6526
+ public_ref=COALESCE(public_ref,$4)
6527
+ WHERE id=$1 AND site_id=$5`,
6528
+ [personId, input.namespaceId, input.crmEntityId ?? null, newAnalyticsPersonPublicRef(), input.siteId]
6421
6529
  );
6422
6530
  }
6423
- await client.query(
6424
- "UPDATE analytics_activation_jobs SET person_id=$2 WHERE person_id=ANY($1::uuid[])",
6425
- [duplicateIds, canonicalId]
6426
- );
6427
- await client.query(
6428
- `UPDATE analytics_candidate_associations SET promoted_person_id=$3,namespace_id=$4
6429
- WHERE site_id=$1 AND promoted_person_id=ANY($2::uuid[])`,
6430
- [input.siteId, duplicateIds, canonicalId, input.namespaceId]
6431
- );
6432
- await client.query(
6433
- `DELETE FROM analytics_candidate_promotions duplicate
6434
- USING analytics_candidate_promotions canonical
6435
- WHERE duplicate.site_id=$1 AND duplicate.person_id=ANY($2::uuid[])
6436
- AND canonical.site_id=duplicate.site_id AND canonical.person_id=$3
6437
- AND canonical.association_id=duplicate.association_id`,
6438
- [input.siteId, duplicateIds, canonicalId]
6439
- );
6440
- await client.query(
6441
- `UPDATE analytics_candidate_promotions SET person_id=$3,namespace_id=$4
6442
- WHERE site_id=$1 AND person_id=ANY($2::uuid[])`,
6443
- [input.siteId, duplicateIds, canonicalId, input.namespaceId]
6444
- );
6445
- await client.query(
6446
- `DELETE FROM analytics_crm_person_mappings duplicate
6447
- USING analytics_crm_person_mappings canonical
6448
- WHERE duplicate.site_id=$1 AND duplicate.person_id=ANY($2::uuid[])
6449
- AND canonical.site_id=duplicate.site_id AND canonical.person_id=$3
6450
- AND canonical.connection_id=duplicate.connection_id`,
6451
- [input.siteId, duplicateIds, canonicalId]
6452
- );
6453
- await client.query(
6454
- `UPDATE analytics_crm_person_mappings SET person_id=$3,namespace_id=$4
6455
- WHERE site_id=$1 AND person_id=ANY($2::uuid[])`,
6456
- [input.siteId, duplicateIds, canonicalId, input.namespaceId]
6457
- );
6458
- await client.query(
6459
- `DELETE FROM analytics_prediction_results duplicate
6460
- USING analytics_prediction_results canonical
6461
- WHERE duplicate.site_id=$1 AND duplicate.person_id=ANY($2::uuid[])
6462
- AND canonical.site_id=duplicate.site_id AND canonical.person_id=$3
6463
- AND canonical.manifest_id=duplicate.manifest_id`,
6464
- [input.siteId, duplicateIds, canonicalId]
6465
- );
6466
- for (const table of ["analytics_score_evaluations", "analytics_prediction_results"]) {
6531
+ return { personId, mergedPersonIds: [] };
6532
+ }
6533
+ async function assignAnalyticsIdentityNode(client, input) {
6534
+ const active = await client.query(
6535
+ `SELECT person_id,crm_entity_id::text FROM analytics_identity_assignments
6536
+ WHERE identity_node_id=$1 AND site_id=$2 FOR UPDATE`,
6537
+ [input.identityNodeId, input.siteId]
6538
+ );
6539
+ let assignment = active.rows[0];
6540
+ if (!assignment) {
6541
+ await client.query(
6542
+ `INSERT INTO analytics_identity_assignments(
6543
+ identity_node_id,site_id,person_id,crm_entity_id,evidence_kind
6544
+ ) VALUES($1,$2,$3,$4::uuid,$5)
6545
+ ON CONFLICT(identity_node_id) DO NOTHING`,
6546
+ [input.identityNodeId, input.siteId, input.personId, input.crmEntityId ?? null, input.evidenceKind]
6547
+ );
6548
+ const winner = await client.query(
6549
+ `SELECT person_id FROM analytics_identity_assignments WHERE identity_node_id=$1 AND site_id=$2`,
6550
+ [input.identityNodeId, input.siteId]
6551
+ );
6552
+ if (winner.rows[0]?.person_id === input.personId) return "assigned";
6553
+ if (!winner.rows[0]) throw new AnalyticsRepositoryError("identity_assignment_unavailable", "Identity assignment could not be recorded.", 409);
6554
+ assignment = { person_id: winner.rows[0].person_id, crm_entity_id: null };
6555
+ }
6556
+ if (assignment.person_id === input.personId) {
6467
6557
  await client.query(
6468
- `UPDATE ${table} SET person_id=$3,namespace_id=$4
6469
- WHERE site_id=$1 AND person_id=ANY($2::uuid[])`,
6470
- [input.siteId, duplicateIds, canonicalId, input.namespaceId]
6558
+ `UPDATE analytics_identity_assignments SET updated_at=now(),
6559
+ crm_entity_id=COALESCE(crm_entity_id,$3::uuid),evidence_kind=$4
6560
+ WHERE identity_node_id=$1 AND site_id=$2`,
6561
+ [input.identityNodeId, input.siteId, input.crmEntityId ?? null, input.evidenceKind]
6471
6562
  );
6563
+ return "existing";
6472
6564
  }
6473
6565
  await client.query(
6474
- "UPDATE analytics_identity_profile_access_audits SET person_id=$3 WHERE site_id=$1 AND person_id=ANY($2::uuid[])",
6475
- [input.siteId, duplicateIds, canonicalId]
6476
- );
6477
- await client.query(
6478
- "UPDATE analytics_identity_tombstones SET person_id=$3 WHERE site_id=$1 AND person_id=ANY($2::uuid[])",
6479
- [input.siteId, duplicateIds, canonicalId]
6480
- );
6481
- await client.query(
6482
- "DELETE FROM analytics_people WHERE site_id=$1 AND id=ANY($2::uuid[])",
6483
- [input.siteId, duplicateIds]
6566
+ `INSERT INTO analytics_identity_assignment_conflicts(
6567
+ id,site_id,identity_node_id,active_person_id,attempted_person_id,
6568
+ active_crm_entity_id,attempted_crm_entity_id,evidence_kind
6569
+ ) VALUES($1,$2,$3,$4,$5,$6::uuid,$7::uuid,$8)
6570
+ ON CONFLICT(identity_node_id,active_person_id,attempted_person_id)
6571
+ WHERE status='pending' DO UPDATE SET observed_at=now(),evidence_kind=EXCLUDED.evidence_kind`,
6572
+ [
6573
+ randomUUID2(),
6574
+ input.siteId,
6575
+ input.identityNodeId,
6576
+ assignment.person_id,
6577
+ input.personId,
6578
+ assignment.crm_entity_id,
6579
+ input.crmEntityId ?? null,
6580
+ input.evidenceKind
6581
+ ]
6484
6582
  );
6485
- return { personId: canonicalId, mergedPersonIds: duplicateIds };
6583
+ return "conflict";
6486
6584
  }
6487
6585
  async function promoteAnalyticsCandidatesForConfirmedPerson(client, input) {
6488
6586
  const associations = await client.query(
@@ -6632,6 +6730,7 @@ async function linkAnalyticsIdentityInTransaction(client, input) {
6632
6730
  for (const [platform, clickId] of Object.entries(sanitizeClickIds(input.clickIds)))
6633
6731
  signals.push({ kind: "click_id", value: `${platform}:${clickId}`, evidence: platform, confidence: 0.9 });
6634
6732
  const identityNodeIds = [];
6733
+ let hasAssignmentConflict = false;
6635
6734
  for (const signal of signals) {
6636
6735
  const node = await client.query(
6637
6736
  `INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
@@ -6648,8 +6747,15 @@ async function linkAnalyticsIdentityInTransaction(client, input) {
6648
6747
  namespace_id=COALESCE(analytics_identity_edges.namespace_id,EXCLUDED.namespace_id)`,
6649
6748
  [randomUUID2(), input.siteId, namespaceId, personId, node.rows[0].id, signal.evidence, signal.confidence]
6650
6749
  );
6750
+ const assignment = await assignAnalyticsIdentityNode(client, {
6751
+ siteId: input.siteId,
6752
+ personId,
6753
+ identityNodeId: node.rows[0].id,
6754
+ evidenceKind: signal.evidence
6755
+ });
6756
+ hasAssignmentConflict ||= assignment === "conflict";
6651
6757
  }
6652
- await completeAnalyticsIdentityPromotion(client, {
6758
+ if (!hasAssignmentConflict) await completeAnalyticsIdentityPromotion(client, {
6653
6759
  siteId: input.siteId,
6654
6760
  namespaceId,
6655
6761
  personId,
@@ -6662,7 +6768,7 @@ async function linkAnalyticsIdentityInTransaction(client, input) {
6662
6768
  sessionId: input.sessionId,
6663
6769
  occurredAt: input.occurredAt
6664
6770
  });
6665
- if (input.customerId) {
6771
+ if (input.customerId && !hasAssignmentConflict) {
6666
6772
  await writeAnalyticsConfirmedIdentityProfile(client, {
6667
6773
  siteId: input.siteId,
6668
6774
  personId,
@@ -6685,7 +6791,7 @@ async function linkAnalyticsFormIdentity(input) {
6685
6791
  input.sessionId
6686
6792
  )) {
6687
6793
  await client.query("COMMIT");
6688
- return { personId: null, linkedSignals: 0 };
6794
+ return { personId: null, linkedSignals: 0, conflicts: 0 };
6689
6795
  }
6690
6796
  const namespaceId = await ensurePrimaryAnalyticsIdentityNamespace(client, input.siteId, "form");
6691
6797
  const deterministicSignals = [
@@ -6705,6 +6811,7 @@ async function linkAnalyticsFormIdentity(input) {
6705
6811
  siteId: input.siteId,
6706
6812
  namespaceId,
6707
6813
  crmPersonRef: input.crmPersonRef,
6814
+ crmEntityId: input.crmEntityId,
6708
6815
  signals: matchSignals
6709
6816
  });
6710
6817
  const personId = resolution.personId;
@@ -6748,6 +6855,7 @@ async function linkAnalyticsFormIdentity(input) {
6748
6855
  });
6749
6856
  }
6750
6857
  const identityNodeIds = [];
6858
+ let conflicts = 0;
6751
6859
  for (const signal of signals) {
6752
6860
  const node = await client.query(
6753
6861
  `INSERT INTO analytics_identity_nodes(id, site_id, namespace_id, kind, value_hmac) VALUES ($1,$2,$3,$4,$5)
@@ -6778,25 +6886,33 @@ async function linkAnalyticsFormIdentity(input) {
6778
6886
  signal.confidence
6779
6887
  ]
6780
6888
  );
6889
+ const assignment = await assignAnalyticsIdentityNode(client, {
6890
+ siteId: input.siteId,
6891
+ personId,
6892
+ crmEntityId: input.crmEntityId,
6893
+ identityNodeId: node.rows[0].id,
6894
+ evidenceKind: signal.evidence
6895
+ });
6896
+ if (assignment === "conflict") conflicts += 1;
6781
6897
  }
6782
6898
  if (input.submissionId)
6783
6899
  await client.query(
6784
6900
  "UPDATE analytics_form_submissions SET person_id = $1 WHERE id = $2 AND site_id = $3",
6785
6901
  [personId, input.submissionId, input.siteId]
6786
6902
  );
6787
- await completeAnalyticsIdentityPromotion(client, {
6903
+ if (conflicts === 0) await completeAnalyticsIdentityPromotion(client, {
6788
6904
  siteId: input.siteId,
6789
6905
  namespaceId,
6790
6906
  personId,
6791
6907
  evidenceKind: "form_fill",
6792
6908
  candidateEvidenceKind: input.email ? "verified_email" : input.phone ? "verified_phone" : "authenticated_user",
6793
- evidenceRef: input.submissionId ?? input.crmPersonRef,
6909
+ evidenceRef: input.submissionId ?? input.crmEntityId ?? input.crmPersonRef ?? personId,
6794
6910
  priorPersonIds: resolution.mergedPersonIds,
6795
6911
  priorIdentityNodeIds: identityNodeIds,
6796
6912
  visitorId: input.visitorId,
6797
6913
  sessionId: input.sessionId
6798
6914
  });
6799
- await writeAnalyticsConfirmedIdentityProfile(client, {
6915
+ if (conflicts === 0) await writeAnalyticsConfirmedIdentityProfile(client, {
6800
6916
  siteId: input.siteId,
6801
6917
  personId,
6802
6918
  consentSubjectRefs: [input.visitorId ?? ""],
@@ -6806,7 +6922,7 @@ async function linkAnalyticsFormIdentity(input) {
6806
6922
  phone: input.phone
6807
6923
  });
6808
6924
  await client.query("COMMIT");
6809
- return { personId, linkedSignals: signals.length };
6925
+ return { personId, linkedSignals: signals.length, conflicts };
6810
6926
  } catch (error) {
6811
6927
  await client.query("ROLLBACK");
6812
6928
  throw error;
@@ -6827,7 +6943,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
6827
6943
  const person = existing.rows[0];
6828
6944
  if (!person || !await hasCurrentPersonIdentityConsent(client, input.siteId, person.public_ref)) {
6829
6945
  await client.query("COMMIT");
6830
- return { personId: null, linkedSignals: 0 };
6946
+ return { personId: null, linkedSignals: 0, conflicts: 0 };
6831
6947
  }
6832
6948
  const signals = [
6833
6949
  ...input.email ? [{ kind: "email", value: input.email, confidence: 1 }] : [],
@@ -6841,6 +6957,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
6841
6957
  siteId: input.siteId,
6842
6958
  namespaceId: person.namespace_id,
6843
6959
  crmPersonRef: input.crmPersonRef,
6960
+ crmEntityId: input.crmEntityId,
6844
6961
  signals: signals.map((signal) => ({ kind: signal.kind, valueHmac: identityHmac(signal.kind, signal.value) }))
6845
6962
  });
6846
6963
  const personId = resolution.personId;
@@ -6850,6 +6967,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
6850
6967
  );
6851
6968
  const canonicalPublicRef = canonical.rows[0]?.public_ref ?? person.public_ref;
6852
6969
  const identityNodeIds = [];
6970
+ let conflicts = 0;
6853
6971
  for (const signal of signals) {
6854
6972
  const node = await client.query(
6855
6973
  `INSERT INTO analytics_identity_nodes(id,site_id,namespace_id,kind,value_hmac) VALUES($1,$2,$3,$4,$5)
@@ -6865,8 +6983,16 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
6865
6983
  confidence=greatest(analytics_identity_edges.confidence,EXCLUDED.confidence)`,
6866
6984
  [randomUUID2(), input.siteId, person.namespace_id, personId, node.rows[0].id, signal.confidence]
6867
6985
  );
6986
+ const assignment = await assignAnalyticsIdentityNode(client, {
6987
+ siteId: input.siteId,
6988
+ personId,
6989
+ crmEntityId: input.crmEntityId,
6990
+ identityNodeId: node.rows[0].id,
6991
+ evidenceKind: "crm_contact"
6992
+ });
6993
+ if (assignment === "conflict") conflicts += 1;
6868
6994
  }
6869
- await completeAnalyticsIdentityPromotion(client, {
6995
+ if (conflicts === 0) await completeAnalyticsIdentityPromotion(client, {
6870
6996
  siteId: input.siteId,
6871
6997
  namespaceId: person.namespace_id,
6872
6998
  personId,
@@ -6876,7 +7002,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
6876
7002
  priorPersonIds: resolution.mergedPersonIds,
6877
7003
  priorIdentityNodeIds: identityNodeIds
6878
7004
  });
6879
- await writeAnalyticsConfirmedIdentityProfile(client, {
7005
+ if (conflicts === 0) await writeAnalyticsConfirmedIdentityProfile(client, {
6880
7006
  siteId: input.siteId,
6881
7007
  personId,
6882
7008
  consentSubjectRefs: [canonicalPublicRef],
@@ -6886,7 +7012,7 @@ async function enrichAnalyticsExistingCrmIdentity(input) {
6886
7012
  phone: input.phone
6887
7013
  });
6888
7014
  await client.query("COMMIT");
6889
- return { personId, linkedSignals: signals.length };
7015
+ return { personId, linkedSignals: signals.length, conflicts };
6890
7016
  } catch (error) {
6891
7017
  await client.query("ROLLBACK");
6892
7018
  throw error;
@@ -7533,6 +7659,111 @@ async function queueAnalyticsFormDelivery(input) {
7533
7659
  );
7534
7660
  return id;
7535
7661
  }
7662
+ async function queueAnalyticsFormBridgeTransaction(input) {
7663
+ const client = await getAnalyticsPool().connect();
7664
+ const proposedSubmissionId = randomUUID2();
7665
+ const proposedBridgeRunId = randomUUID2();
7666
+ const proposedCrmEntityId = input.crmEntityId ?? randomUUID2();
7667
+ const occurredAt = input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
7668
+ try {
7669
+ await client.query("BEGIN");
7670
+ const inserted = await client.query(
7671
+ `INSERT INTO analytics_form_submissions(
7672
+ id,form_id,site_id,pixel_id,visitor_id,session_id,crm_person_ref,
7673
+ crm_entity_id,bridge_run_id,idempotency_key,source,medium,campaign,
7674
+ crm_delivery_status,click_ids,occurred_at
7675
+ ) VALUES($1,$2,$3,$4,$5,$6,$7,$8::uuid,$9,$10,$11,$12,$13,'pending',$14::jsonb,$15)
7676
+ ON CONFLICT(form_id,idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
7677
+ RETURNING id,bridge_run_id,crm_entity_id::text`,
7678
+ [
7679
+ proposedSubmissionId,
7680
+ input.form.id,
7681
+ input.form.site_id,
7682
+ input.form.pixel_id,
7683
+ input.visitorId ?? null,
7684
+ input.sessionId ?? null,
7685
+ input.legacyCrmPersonRef ?? null,
7686
+ proposedCrmEntityId,
7687
+ proposedBridgeRunId,
7688
+ input.idempotencyKey,
7689
+ input.source?.slice(0, 180) ?? null,
7690
+ input.medium?.slice(0, 180) ?? null,
7691
+ input.campaign?.slice(0, 240) ?? null,
7692
+ JSON.stringify(sanitizeClickIds(input.clickIds)),
7693
+ occurredAt
7694
+ ]
7695
+ );
7696
+ const created = Boolean(inserted.rows[0]);
7697
+ const existing = inserted.rows[0] ?? (await client.query(
7698
+ `SELECT id,bridge_run_id::text,crm_entity_id::text
7699
+ FROM analytics_form_submissions
7700
+ WHERE form_id=$1 AND idempotency_key=$2 FOR UPDATE`,
7701
+ [input.form.id, input.idempotencyKey]
7702
+ )).rows[0];
7703
+ if (!existing?.bridge_run_id || !existing.crm_entity_id)
7704
+ throw new AnalyticsRepositoryError(
7705
+ "form_bridge_idempotency_conflict",
7706
+ "The form idempotency receipt belongs to a legacy submission.",
7707
+ 409
7708
+ );
7709
+ const conversionId = randomUUID2();
7710
+ const conversion = await client.query(
7711
+ `INSERT INTO analytics_conversions(
7712
+ id,source_event_id,site_id,pixel_id,session_id,session_hmac,
7713
+ conversion_kind,value_minor,currency,occurred_at
7714
+ ) VALUES($1,$2,$3,$4,$5,$6,'form_submitted',0,'USD',$7)
7715
+ ON CONFLICT(site_id,source_event_id) DO UPDATE SET source_event_id=EXCLUDED.source_event_id
7716
+ RETURNING id`,
7717
+ [
7718
+ conversionId,
7719
+ `form:${existing.id}`,
7720
+ input.form.site_id,
7721
+ input.form.pixel_id,
7722
+ input.sessionId ?? null,
7723
+ input.sessionId ? identityHmac("session_id", input.sessionId) : null,
7724
+ occurredAt
7725
+ ]
7726
+ );
7727
+ await client.query(
7728
+ `INSERT INTO analytics_form_delivery_jobs(
7729
+ id,submission_id,owner_user_id,payload_ciphertext,bridge_run_id,
7730
+ crm_entity_id,idempotency_key
7731
+ ) VALUES($1,$2,$3,$4,$5,$6::uuid,$7)
7732
+ ON CONFLICT(submission_id) DO NOTHING`,
7733
+ [
7734
+ randomUUID2(),
7735
+ existing.id,
7736
+ input.ownerUserId,
7737
+ input.payloadCiphertext,
7738
+ existing.bridge_run_id,
7739
+ existing.crm_entity_id,
7740
+ input.idempotencyKey
7741
+ ]
7742
+ );
7743
+ await client.query(
7744
+ `INSERT INTO analytics_form_bridge_receipts(
7745
+ id,bridge_run_id,submission_id,system,status,opaque_entity_id
7746
+ ) VALUES
7747
+ ($1,$2,$3,'analytics','delivered',NULL),
7748
+ ($4,$2,$3,'crm','pending',$5::uuid)
7749
+ ON CONFLICT(bridge_run_id,system) DO NOTHING`,
7750
+ [randomUUID2(), existing.bridge_run_id, existing.id, randomUUID2(), existing.crm_entity_id]
7751
+ );
7752
+ await client.query("COMMIT");
7753
+ return {
7754
+ submissionId: existing.id,
7755
+ bridgeRunId: existing.bridge_run_id,
7756
+ crmEntityId: existing.crm_entity_id,
7757
+ conversionId: conversion.rows[0].id,
7758
+ created
7759
+ };
7760
+ } catch (error) {
7761
+ await client.query("ROLLBACK");
7762
+ throw error;
7763
+ } finally {
7764
+ client.release();
7765
+ }
7766
+ }
7536
7767
  async function sweepAnalyticsRestrictedRetention() {
7537
7768
  const db = getAnalyticsPool();
7538
7769
  const client = await db.connect();
@@ -7575,7 +7806,8 @@ async function claimAnalyticsFormDeliveryJobs(limit = 10) {
7575
7806
  SET attempts = j.attempts + 1,
7576
7807
  next_attempt_at = now() + interval '5 minutes'
7577
7808
  FROM due WHERE j.id = due.id
7578
- RETURNING j.id, j.submission_id, j.owner_user_id, j.payload_ciphertext, j.attempts`,
7809
+ RETURNING j.id, j.submission_id, j.owner_user_id, j.payload_ciphertext, j.attempts,
7810
+ j.bridge_run_id::text,j.crm_entity_id::text,j.idempotency_key`,
7579
7811
  [Math.min(Math.max(limit, 1), 50)]
7580
7812
  );
7581
7813
  return result.rows.map((row) => ({
@@ -7583,21 +7815,37 @@ async function claimAnalyticsFormDeliveryJobs(limit = 10) {
7583
7815
  submissionId: row.submission_id,
7584
7816
  ownerUserId: Number(row.owner_user_id),
7585
7817
  payloadCiphertext: row.payload_ciphertext,
7586
- attempts: row.attempts
7818
+ attempts: row.attempts,
7819
+ bridgeRunId: row.bridge_run_id,
7820
+ crmEntityId: row.crm_entity_id,
7821
+ idempotencyKey: row.idempotency_key
7587
7822
  }));
7588
7823
  }
7589
7824
  async function completeAnalyticsFormDelivery(jobId, submissionId) {
7825
+ await completeAnalyticsFormBridgeDelivery({ jobId, submissionId });
7826
+ }
7827
+ async function completeAnalyticsFormBridgeDelivery(input) {
7590
7828
  const db = getAnalyticsPool();
7591
7829
  const client = await db.connect();
7592
7830
  try {
7593
7831
  await client.query("BEGIN");
7594
7832
  await client.query(
7595
7833
  `UPDATE analytics_form_delivery_jobs SET status='delivered', delivered_at=now(), last_error_code=NULL WHERE id=$1`,
7596
- [jobId]
7834
+ [input.jobId]
7597
7835
  );
7598
7836
  await client.query(
7599
- `UPDATE analytics_form_submissions SET crm_delivery_status='delivered' WHERE id=$1`,
7600
- [submissionId]
7837
+ `UPDATE analytics_form_submissions SET crm_delivery_status='delivered',
7838
+ crm_entity_id=COALESCE(crm_entity_id,$2::uuid) WHERE id=$1`,
7839
+ [input.submissionId, input.crmEntityId ?? null]
7840
+ );
7841
+ await client.query(
7842
+ `UPDATE analytics_form_bridge_receipts receipt
7843
+ SET status='delivered',opaque_entity_id=COALESCE(opaque_entity_id,$3::uuid),
7844
+ safe_error_code=NULL,updated_at=now()
7845
+ FROM analytics_form_delivery_jobs job
7846
+ WHERE job.id=$1 AND receipt.bridge_run_id=job.bridge_run_id
7847
+ AND receipt.submission_id=$2 AND receipt.system='crm'`,
7848
+ [input.jobId, input.submissionId, input.crmEntityId ?? null]
7601
7849
  );
7602
7850
  await client.query("COMMIT");
7603
7851
  } catch (error) {
@@ -7610,14 +7858,34 @@ async function completeAnalyticsFormDelivery(jobId, submissionId) {
7610
7858
  async function deferAnalyticsFormDelivery(jobId, attempts, errorCode) {
7611
7859
  const terminal = attempts >= 8;
7612
7860
  const delayMinutes = Math.min(5 * 2 ** Math.max(0, attempts - 1), 360);
7613
- await getAnalyticsPool().query(
7614
- `UPDATE analytics_form_delivery_jobs
7615
- SET status = CASE WHEN expires_at <= now() THEN 'expired' WHEN $2 THEN 'failed' ELSE 'pending' END,
7616
- last_error_code = $3,
7617
- next_attempt_at = now() + ($4::text || ' minutes')::interval
7618
- WHERE id = $1`,
7619
- [jobId, terminal, errorCode.slice(0, 80), String(delayMinutes)]
7620
- );
7861
+ const client = await getAnalyticsPool().connect();
7862
+ try {
7863
+ await client.query("BEGIN");
7864
+ const updated = await client.query(
7865
+ `UPDATE analytics_form_delivery_jobs
7866
+ SET status = CASE WHEN expires_at <= now() THEN 'expired' WHEN $2 THEN 'failed' ELSE 'pending' END,
7867
+ last_error_code = $3,
7868
+ next_attempt_at = now() + ($4::text || ' minutes')::interval
7869
+ WHERE id = $1
7870
+ RETURNING bridge_run_id::text,submission_id::text,status`,
7871
+ [jobId, terminal, errorCode.slice(0, 80), String(delayMinutes)]
7872
+ );
7873
+ const job = updated.rows[0];
7874
+ if (job?.bridge_run_id) {
7875
+ await client.query(
7876
+ `UPDATE analytics_form_bridge_receipts SET status=$3,
7877
+ safe_error_code=$4,updated_at=now()
7878
+ WHERE bridge_run_id=$1 AND submission_id=$2 AND system='crm'`,
7879
+ [job.bridge_run_id, job.submission_id, job.status, errorCode.slice(0, 80)]
7880
+ );
7881
+ }
7882
+ await client.query("COMMIT");
7883
+ } catch (error) {
7884
+ await client.query("ROLLBACK");
7885
+ throw error;
7886
+ } finally {
7887
+ client.release();
7888
+ }
7621
7889
  }
7622
7890
  async function isAnalyticsFormPlacementApproved(pixelId, placementUrl) {
7623
7891
  if (!placementUrl) return false;
@@ -7983,7 +8251,9 @@ export {
7983
8251
  recordAnalyticsFormSubmission,
7984
8252
  sanitizeClickIds,
7985
8253
  identityHmac,
8254
+ deterministicAnalyticsCrmEntityId,
7986
8255
  backfillAnalyticsConfirmedHistory,
8256
+ assignAnalyticsIdentityNode,
7987
8257
  linkAnalyticsIdentityInTransaction,
7988
8258
  linkAnalyticsFormIdentity,
7989
8259
  enrichAnalyticsExistingCrmIdentity,
@@ -8005,9 +8275,11 @@ export {
8005
8275
  pollAnalyticsActivationDiagnostics,
8006
8276
  queueAnalyticsActivation,
8007
8277
  queueAnalyticsFormDelivery,
8278
+ queueAnalyticsFormBridgeTransaction,
8008
8279
  sweepAnalyticsRestrictedRetention,
8009
8280
  claimAnalyticsFormDeliveryJobs,
8010
8281
  completeAnalyticsFormDelivery,
8282
+ completeAnalyticsFormBridgeDelivery,
8011
8283
  deferAnalyticsFormDelivery,
8012
8284
  isAnalyticsFormPlacementApproved,
8013
8285
  analyticsHealth,