From 2e12d281ef6f2e5c7672ce6c9f24bdac6dc4216d Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Thu, 18 Dec 2025 12:56:49 -0800 Subject: [PATCH] conversion api wrapper --- package.json | 3 + pnpm-lock.yaml | 259 ++++++++++++++++++++++++++++++++++ src/lib/conversion/client.ts | 91 ++++++++++++ src/lib/conversion/control.ts | 117 +++++++++++++++ src/lib/conversion/index.ts | 3 + src/lib/conversion/server.ts | 67 +++++++++ src/lib/types/conversion.d.ts | 53 +++++++ 7 files changed, 593 insertions(+) create mode 100644 src/lib/conversion/client.ts create mode 100644 src/lib/conversion/control.ts create mode 100644 src/lib/conversion/index.ts create mode 100644 src/lib/conversion/server.ts create mode 100644 src/lib/types/conversion.d.ts diff --git a/package.json b/package.json index f7aecd5..0a729b9 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,10 @@ "svelte" ], "dependencies": { + "@types/facebook-nodejs-business-sdk": "^23.0.0", "@types/umami": "^2.10.1", + "facebook-nodejs-business-sdk": "^24.0.1", + "http-status-codes": "^2.3.0", "loglevel": "^1.9.2" }, "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 203d369..a509694 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,18 @@ importers: .: dependencies: + '@types/facebook-nodejs-business-sdk': + specifier: ^23.0.0 + version: 23.0.0 '@types/umami': specifier: ^2.10.1 version: 2.10.1 + facebook-nodejs-business-sdk: + specifier: ^24.0.1 + version: 24.0.1 + http-status-codes: + specifier: ^2.3.0 + version: 2.3.0 loglevel: specifier: ^1.9.2 version: 1.9.2 @@ -473,6 +482,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/facebook-nodejs-business-sdk@23.0.0': + resolution: {integrity: sha512-k9saLiTd4kvBCdBHEpTyDMYlqSsiDw0yHs2ePe5M1PTIldtdtNOGx73Gtu8dQqbTAlwBWbiG3VfI+9s45Cf88w==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -565,6 +577,12 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -578,6 +596,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -605,6 +627,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -621,6 +647,9 @@ packages: engines: {node: '>=4'} hasBin: true + currency-codes@1.5.1: + resolution: {integrity: sha512-hqy8vtlIYKzO6pe2TE0V4/riZALIc7nhtE9cvxk5FDRCvfGplgzUvpTmZlMsyO+NeK5U41j+sQXJOo8l8v9kdg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -640,9 +669,37 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + devalue@5.6.1: resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + email-validator@2.0.4: + resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==} + engines: {node: '>4.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.27.1: resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} engines: {node: '>=18'} @@ -716,6 +773,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + facebook-nodejs-business-sdk@24.0.1: + resolution: {integrity: sha512-31uR42FO+V3ikQfVZyrnoc34pVYzxo4PE+BnKt1ziX9nGhSwsS7ogGDsMJQk7A3SdoWPIGgR2G4UbAY2s9zNVQ==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -742,6 +802,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + first-match@0.0.1: + resolution: {integrity: sha512-VvKbnaxrC0polTFDC+teKPTdl2mn6B/KUW+WB3C9RzKDeNwbzfLdnUz3FxC+tnjvus6bI0jWrWicQyVIPdS37A==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -749,11 +812,35 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -766,10 +853,29 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -800,6 +906,12 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + iso-3166-1@2.1.1: + resolution: {integrity: sha512-RZxXf8cw5Y8LyHZIwIRvKw8sWTIHh2/txBT+ehO0QroesVfnz3JNFFX4i/OC/Yuv2bDIVYrHna5PMvjtpefq5w==} + + js-sha256@0.9.0: + resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -848,6 +960,18 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -855,6 +979,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + mixwith@0.1.1: + resolution: {integrity: sha512-DQsf/liljH/9e+94jR+xfK8vlKceeKdOM9H9UEXLwGuvEEpO6debNtJ9yt1ZKzPKPrwqGxzMdu0BR1fnQb6i4A==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -874,6 +1001,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nub@0.0.0: + resolution: {integrity: sha512-dK0Ss9C34R/vV0FfYJXuqDAqHlaW9fvWVufq9MmGF2umCuDbd5GRfRD9fpi/LiM0l4ZXf8IBB+RYmZExqCrf0w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -955,6 +1085,9 @@ packages: engines: {node: '>=14'} hasBin: true + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + publint@0.3.16: resolution: {integrity: sha512-MFqyfRLAExPVZdTQFwkAQELzA8idyXzROVOytg6nEJ/GEypXBUmMGrVaID8cTuzRS1U5L8yTOdOJtMXgFUJAeA==} engines: {node: '>=18'} @@ -1447,6 +1580,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/facebook-nodejs-business-sdk@23.0.0': {} + '@types/json-schema@7.0.15': {} '@types/node@24.10.4': @@ -1567,6 +1702,16 @@ snapshots: aria-query@5.3.2: {} + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -1580,6 +1725,11 @@ snapshots: dependencies: balanced-match: 1.0.2 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} chalk@4.1.2: @@ -1603,6 +1753,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} cookie@0.6.0: {} @@ -1615,6 +1769,11 @@ snapshots: cssesc@3.0.0: {} + currency-codes@1.5.1: + dependencies: + first-match: 0.0.1 + nub: 0.0.0 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -1625,8 +1784,33 @@ snapshots: deepmerge@4.3.1: {} + delayed-stream@1.0.0: {} + devalue@5.6.1: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + email-validator@2.0.4: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.27.1: optionalDependencies: '@esbuild/aix-ppc64': 0.27.1 @@ -1752,6 +1936,17 @@ snapshots: esutils@2.0.3: {} + facebook-nodejs-business-sdk@24.0.1: + dependencies: + axios: 1.13.2 + currency-codes: 1.5.1 + email-validator: 2.0.4 + iso-3166-1: 2.1.1 + js-sha256: 0.9.0 + mixwith: 0.1.1 + transitivePeerDependencies: + - debug + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -1771,6 +1966,8 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + first-match@0.0.1: {} + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -1778,9 +1975,39 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -1789,8 +2016,22 @@ snapshots: globals@16.5.0: {} + gopd@1.2.0: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-status-codes@2.3.0: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -1814,6 +2055,10 @@ snapshots: isexe@2.0.0: {} + iso-3166-1@2.1.1: {} + + js-sha256@0.9.0: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -1853,6 +2098,14 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -1861,6 +2114,8 @@ snapshots: dependencies: brace-expansion: 2.0.2 + mixwith@0.1.1: {} + mri@1.2.0: {} mrmime@2.0.1: {} @@ -1871,6 +2126,8 @@ snapshots: natural-compare@1.4.0: {} + nub@0.0.0: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -1937,6 +2194,8 @@ snapshots: prettier@3.7.4: {} + proxy-from-env@1.1.0: {} + publint@0.3.16: dependencies: '@publint/pack': 0.1.2 diff --git a/src/lib/conversion/client.ts b/src/lib/conversion/client.ts new file mode 100644 index 0000000..b583c0a --- /dev/null +++ b/src/lib/conversion/client.ts @@ -0,0 +1,91 @@ +import type { TrackingManager } from '$lib/tracking.svelte'; +import type { + ConversionErrorResponseBody, + ConversionRequestBody, + ConversionResponseBody, + ConversionUserData +} from '$lib/types/conversion.js'; +import type { StandardEventName } from '$lib/types/fbq.js'; + +/** + * Client for sending conversion events to a server endpoint. + */ +export class ConversionClient { + private _href: string; + private _trackingManager: TrackingManager; + + /** + * Creates a new ConversionClient. + * + * @param serverHref - The server endpoint URL. + * @param trackingManager - The tracking manager instance. + */ + constructor(serverHref: string, trackingManager: TrackingManager) { + this._href = serverHref; + this._trackingManager = trackingManager; + } + + /** + * Sends a conversion event to the server. + * + * @param eventName - The name of the standard event to send. + * @param options - Additional options for the event. + * @returns A promise that resolves to the event response or error. + */ + async trackEvent( + eventName: StandardEventName, + options: { + eventID: string; + user?: Omit; + customData?: Record; + } + ): Promise { + // Extract user data + const fbp = await cookieStore.get('_fbp'); + const fbc = await cookieStore.get('_fbc'); + + const user: ConversionUserData = { + ...options.user, + fbp: fbp?.value, + fbc: fbc?.value + }; + + // Get event source URL & extract UTM params if present + const eventSourceURL = window.location.href; + const url = new URL(eventSourceURL); + const utms: Record = {}; + url.searchParams.forEach((value, key) => { + if (key.startsWith('utm_')) { + utms[key] = value; + } + }); + + // Build request body + const requestBody: ConversionRequestBody = { + consent: this._trackingManager.consent === true, + eventName, + eventID: options.eventID, + user, + eventSourceURL, + utms, + customData: options.customData + }; + + // Send request to server + const response = await fetch(this._href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + if (response.ok) { + const data = (await response.json()) as ConversionResponseBody; + return data; + } else { + const errorData = (await response.json()) as ConversionErrorResponseBody; + return errorData; + } + } +} diff --git a/src/lib/conversion/control.ts b/src/lib/conversion/control.ts new file mode 100644 index 0000000..eff7f1d --- /dev/null +++ b/src/lib/conversion/control.ts @@ -0,0 +1,117 @@ +import { + CustomData, + EventRequest, + EventResponse, + ServerEvent, + UserData +} from 'facebook-nodejs-business-sdk'; +import type { StandardEventName } from '../types/fbq.js'; +import type { ConversionUserData } from '$lib/types/conversion.js'; + +/** + * Builds UserData for conversion events. + * + * @param data - The user data to include. + * @returns The constructed UserData object. + */ +export const buildConversionUserData = (data: ConversionUserData): UserData => { + const userData = new UserData(); + if (data.email) userData.setEmail(data.email); + if (data.phone) userData.setPhone(data.phone); + if (data.firstName) userData.setFirstName(data.firstName); + if (data.lastName) userData.setLastName(data.lastName); + if (data.ip) userData.setClientIpAddress(data.ip); + if (data.fbp) userData.setFbp(data.fbp); + if (data.fbc) userData.setFbc(data.fbc); + if (data.ua) userData.setClientUserAgent(data.ua); + if (data.externalId) userData.setExternalId(data.externalId); + return userData; +}; + +/** + * Builds CustomData for conversion events. + * + * @param params - The custom data parameters. + * @returns The constructed CustomData object. + */ +export const buildCustomData = (params: Record): CustomData => { + const c = new CustomData(); + // Map known fields safely + if (params.currency) c.setCurrency(params.currency); + if (typeof params.value === 'number') c.setValue(params.value); + if (Array.isArray(params.contents)) c.setContents(params.contents); + if (params.content_type) c.setContentType(params.content_type); + if (Array.isArray(params.content_ids)) c.setContentIds(params.content_ids); + if (typeof params.num_items === 'number') c.setNumItems(params.num_items); + if (params.search_string) c.setSearchString(params.search_string); + // Attach anything else as custom_properties + const extras = { ...params }; + delete extras.currency; + delete extras.value; + delete extras.contents; + delete extras.content_type; + delete extras.content_ids; + delete extras.num_items; + delete extras.search_string; + if (Object.keys(extras).length) c.setCustomProperties(extras); + return c; +}; + +/** + * Parameters for sending a conversion event to Meta Pixel. + */ +export type ConversionEventOptions = { + eventID: string; + eventSourceURL?: string; + actionSource: 'website' | 'app' | 'offline' | 'other'; + userData: UserData; + customData?: CustomData; +}; + +/** + * Control class for sending Conversion API events to Meta Pixel. + */ +export class ConversionControl { + private _accessToken: string; + private _pixelID: string; + private _testEventCode?: string; + + /** + * Creates a new ConversionControl instance. + * + * @param accessToken - Your Meta Pixel Conversion API access token. + * @param pixelID - Your Meta Pixel ID. + * @param testEventCode - Optional test event code for testing events. + */ + constructor(accessToken: string, pixelID: string, testEventCode?: string) { + this._accessToken = accessToken; + this._pixelID = pixelID; + this._testEventCode = testEventCode; + } + + /** + * Sends a conversion event to Meta Pixel. + * + * @param eventName - The name of the standard event to send. + * @param options - Additional options for the event. + * @returns A promise that resolves to the event response. + */ + async trackEvent( + eventName: StandardEventName, + options: ConversionEventOptions + ): Promise { + const event = new ServerEvent() + .setEventName(eventName) + .setEventTime(Math.floor(Date.now() / 1000)) + .setEventId(options.eventID) + .setUserData(options.userData) + .setActionSource(options.actionSource); + + if (options?.eventSourceURL) event.setEventSourceUrl(options.eventSourceURL); + if (options?.customData) event.setCustomData(options.customData); + + const req = new EventRequest(this._accessToken, this._pixelID).setEvents([event]); + if (this._testEventCode) req.setTestEventCode(this._testEventCode); + return req.execute(); + } +} diff --git a/src/lib/conversion/index.ts b/src/lib/conversion/index.ts new file mode 100644 index 0000000..252dc22 --- /dev/null +++ b/src/lib/conversion/index.ts @@ -0,0 +1,3 @@ +export * from './client.ts'; +export * from './server.ts'; +export * from './control.ts'; diff --git a/src/lib/conversion/server.ts b/src/lib/conversion/server.ts new file mode 100644 index 0000000..f7e5f0f --- /dev/null +++ b/src/lib/conversion/server.ts @@ -0,0 +1,67 @@ +import { json, type RequestHandler } from '@sveltejs/kit'; +import { buildConversionUserData, buildCustomData, ConversionControl } from './control.ts'; +import type { + ConversionErrorResponseBody, + ConversionRequestBody, + ConversionResponseBody +} from '$lib/types/conversion.js'; + +import { StatusCodes } from 'http-status-codes'; + +export const createConversionRequestHandler: (control: ConversionControl) => RequestHandler = ( + control +) => { + const handle: RequestHandler = async ({ request, getClientAddress }) => { + try { + const body = (await request.json()) as ConversionRequestBody; + + // Build user data with IP and user agent + const ip = getClientAddress(); + const ua = request.headers.get('user-agent'); + + const userData = buildConversionUserData({ + ...body.user, + ip, + ua: ua ?? body.user?.ua ?? undefined + }); + + // Build custom data with UTM params if applicable + let rawCustomData = body.customData ?? {}; + if (body.eventName === 'PageView' && body.utms) { + // For PageView events, automatically include UTM params if provided + rawCustomData = { + ...rawCustomData, + ...body.utms + }; + } + + const customData = buildCustomData(rawCustomData); + + // Send the event via the control + const response = await control.trackEvent(body.eventName, { + eventID: body.eventID, + eventSourceURL: body.eventSourceURL, + actionSource: 'website', + userData, + customData + }); + + // Structure the response + const structuredResponse: ConversionResponseBody = { + pixelID: response.id, + fbtrace_id: response.fbtrace_id, + receivedEvents: response.events_received, + processedEvents: response.num_processed_entries, + messages: response.messages + }; + + return json(structuredResponse, { status: StatusCodes.OK }); + } catch (e) { + return json( + { error: e instanceof Error ? e.message : String(e) } as ConversionErrorResponseBody, + { status: StatusCodes.INTERNAL_SERVER_ERROR } + ); + } + }; + return handle; +}; diff --git a/src/lib/types/conversion.d.ts b/src/lib/types/conversion.d.ts new file mode 100644 index 0000000..2a0590d --- /dev/null +++ b/src/lib/types/conversion.d.ts @@ -0,0 +1,53 @@ +import type { StandardEventName } from '../types/fbq.js'; + +/** + * Supported user data fields for conversion events. + */ +export type ConversionUserData = { + email?: string; + phone?: string; + firstName?: string; + lastName?: string; + ip?: string; + fbp?: string; + fbc?: string; + /** user agent */ + ua?: string; + externalId?: string; +}; + +/** + * Request body for conversion event tracking. + */ +export type ConversionRequestBody = { + consent: boolean; + eventName: StandardEventName; + eventID: string; + eventSourceURL?: string; + utms?: Partial< + Record<'utm_source' | 'utm_medium' | 'utm_campaign' | 'utm_content' | 'utm_term', string> + >; + user?: Omit; + customData?: Record; +}; + +/** + * Response body for conversion event tracking. + */ +export type ConversionResponseBody = { + /** Dataset or Pixel ID to which the event successfully posted. */ + pixelID: string; + /** fbtrace_id for debugging purposes. */ + fbtrace_id: string; + /** Number of events received that were sent by the request. */ + receivedEvents: number; + /** Number of events successfully posted by the request. */ + processedEvents: number; + /** Messages returned by the server. */ + messages: string[]; +}; + +/** Error response body for conversion event tracking. */ +export type ConversionErrorResponseBody = { + error: string; +};