diff --git a/.env.example b/.env.example index 6d0104e..4ab3dbc 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,14 @@ PUBLIC_SUPABASE_ANON_KEY="" SUPABASE_SERVICE_ROLE_KEY="" STRIPE_SECRET_KEY="" STRIPE_ENDPOINT_SECRET="" -VITE_PRODUCT_ID_EXAMPLEPRODUCT="" \ No newline at end of file +VITE_PRODUCT_ID_EXAMPLEPRODUCT="" +VITE_AXIOM_DATASET="" +VITE_AXIOM_TOKEN="" +# Brevo API configuration for transactional emails (account closure notifications) +VITE_BREVO_API_KEY="" +# Default sender email for system notifications +VITE_BREVO_SENDER_EMAIL="noreply@example.com" +# Display name for the sender email +VITE_BREVO_SENDER_NAME="No Reply" +# Contact email for the contact page form to send to +VITE_CONTACT_EMAIL="" \ No newline at end of file diff --git a/docs/database-setup.md b/docs/database-setup.md index 00f6553..5ee424d 100644 --- a/docs/database-setup.md +++ b/docs/database-setup.md @@ -79,7 +79,15 @@ There are two options to create a migration file: ## Applying migrations -Reset database in cloud +To do so without resetting the database, you can use the following command: + +`npx supabase migration up` + +> Use this command when you want to preserve existing data whilst applying new schema changes. For a fresh start with test data, use `npx supabase db reset` instead. + +And for the database in the cloud: + +`npx supabase db push` > Prerequisite login using `npx supabase login` diff --git a/package.json b/package.json index 3893bae..f151ba8 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,13 @@ }, "type": "module", "dependencies": { + "@axiomhq/winston": "^1.2.0", + "@getbrevo/brevo": "^2.2.0", "@supabase/ssr": "^0.4.0", "@supabase/supabase-js": "^2.44.2", "stripe": "^15.4.0", - "ts-debounce": "^4.0.0" + "ts-debounce": "^4.0.0", + "winston": "^3.15.0" }, "engines": { "node": ">=18.13.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 437f249..fa1b771 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@axiomhq/winston': + specifier: ^1.2.0 + version: 1.2.0 + '@getbrevo/brevo': + specifier: ^2.2.0 + version: 2.2.0 '@supabase/ssr': specifier: ^0.4.0 version: 0.4.1(@supabase/supabase-js@2.45.4) @@ -20,6 +26,9 @@ importers: ts-debounce: specifier: ^4.0.0 version: 4.0.0 + winston: + specifier: ^3.15.0 + version: 3.15.0 devDependencies: '@sveltejs/adapter-auto': specifier: ^3.0.0 @@ -134,6 +143,14 @@ packages: peerDependencies: ajv: '>=8' + '@axiomhq/js@1.2.0': + resolution: {integrity: sha512-ALvNo8YeDwE7EHWr9TGijdfo/gbmgTB5XHHBcIcu6Ra+A9+2ltBO5UvTzvSSGPaxn4neEyh+8ESzEBJVEjNAnA==} + engines: {node: '>=16'} + + '@axiomhq/winston@1.2.0': + resolution: {integrity: sha512-PE3dy49MZj9lrgTLcADu9iw8xeLxIA6Vcdc35JwgDDKDpvxy68+rz7LKWvhtnNi5T5Xpc5QMVjO3OGGDD35HrQ==} + engines: {node: '>=16'} + '@babel/code-frame@7.24.7': resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -709,6 +726,13 @@ packages: '@canvas/image-data@1.0.0': resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -865,6 +889,9 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@getbrevo/brevo@2.2.0': + resolution: {integrity: sha512-mNCkXtgqn6jqLglAx1JzZcTj53kBZ5dK9Yd6zVuEyXAvhz68f5Ps6dSJar9HvkHH0Lfr3NrqW76xurjcoxqhIg==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1187,6 +1214,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1270,6 +1300,10 @@ packages: '@vite-pwa/assets-generator': optional: true + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1339,9 +1373,19 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -1357,6 +1401,12 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1400,6 +1450,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bin-links@4.0.4: resolution: {integrity: sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -1411,6 +1464,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1436,6 +1492,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -1459,6 +1518,9 @@ packages: caniuse-lite@1.0.30001660: resolution: {integrity: sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==} + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1505,6 +1567,9 @@ packages: color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} @@ -1512,6 +1577,13 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -1547,6 +1619,9 @@ packages: core-js-compat@3.38.1: resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1575,6 +1650,10 @@ packages: resolution: {integrity: sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==} engines: {node: '>=16.9.0'} + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -1634,6 +1713,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1669,6 +1752,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -1683,6 +1769,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -1801,10 +1890,25 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1838,10 +1942,16 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fetch-retry@6.0.0: + resolution: {integrity: sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1864,6 +1974,9 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -1871,6 +1984,13 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1918,6 +2038,9 @@ packages: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -1973,6 +2096,15 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2003,6 +2135,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + https-proxy-agent@7.0.5: resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} @@ -2152,6 +2288,9 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} @@ -2161,6 +2300,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -2180,6 +2322,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -2204,6 +2349,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2216,6 +2364,10 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2229,6 +2381,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -2273,6 +2428,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + logform@2.6.1: + resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} + engines: {node: '>= 12.0.0'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2296,6 +2455,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + 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'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -2397,6 +2564,9 @@ packages: resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2420,6 +2590,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2468,6 +2641,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -2606,6 +2782,13 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -2624,6 +2807,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2648,6 +2835,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2677,6 +2868,11 @@ packages: resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2693,6 +2889,9 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rewire@7.0.0: + resolution: {integrity: sha512-DyyNyzwMtGYgu0Zl/ya0PR/oaunM+VuCuBxCuhYJHHaV0V+YvYa3bBGxb5OZ71vndgmp1pYY8F4YOwQo1siRGw==} + rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -2735,6 +2934,13 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sander@0.5.1: resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} @@ -2827,6 +3033,14 @@ packages: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + streamx@2.20.1: resolution: {integrity: sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==} @@ -3010,6 +3224,9 @@ packages: text-decoder@1.2.0: resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -3042,12 +3259,20 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -3066,6 +3291,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3148,6 +3376,19 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + vite-plugin-pwa@0.20.5: resolution: {integrity: sha512-aweuI/6G6n4C5Inn0vwHumElU/UEpNuO+9iZzwPZGTCH87TeZ6YFMrEY6ZUBQdIHHlhTsbMDryFARcSuOdsz9Q==} engines: {node: '>=16.0.0'} @@ -3233,6 +3474,14 @@ packages: engines: {node: '>= 8'} hasBin: true + winston-transport@4.8.0: + resolution: {integrity: sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==} + engines: {node: '>= 12.0.0'} + + winston@3.15.0: + resolution: {integrity: sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==} + engines: {node: '>= 12.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -3351,6 +3600,16 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 + '@axiomhq/js@1.2.0': + dependencies: + fetch-retry: 6.0.0 + uuid: 8.3.2 + + '@axiomhq/winston@1.2.0': + dependencies: + '@axiomhq/js': 1.2.0 + winston-transport: 4.8.0 + '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 @@ -4114,6 +4373,14 @@ snapshots: '@canvas/image-data@1.0.0': {} + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -4206,6 +4473,14 @@ snapshots: '@eslint/js@8.57.1': {} + '@getbrevo/brevo@2.2.0': + dependencies: + bluebird: 3.7.2 + request: 2.88.2 + rewire: 7.0.0 + transitivePeerDependencies: + - supports-color + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -4562,6 +4837,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': {} '@types/ws@8.5.12': @@ -4668,6 +4945,10 @@ snapshots: optionalDependencies: '@vite-pwa/assets-generator': 0.2.6 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 @@ -4739,8 +5020,16 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + async@3.2.6: {} + asynckit@0.4.0: {} + at-least-node@1.0.0: {} autoprefixer@10.4.20(postcss@8.4.47): @@ -4757,6 +5046,10 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + axobject-query@4.1.0: {} b4a@1.6.6: {} @@ -4813,6 +5106,10 @@ snapshots: base64-js@1.5.1: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + bin-links@4.0.4: dependencies: cmd-shim: 6.0.3 @@ -4828,6 +5125,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.7.2: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -4857,6 +5156,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + builtin-modules@3.3.0: {} cac@6.7.14: {} @@ -4875,6 +5179,8 @@ snapshots: caniuse-lite@1.0.30001660: {} + caseless@0.12.0: {} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -4931,6 +5237,11 @@ snapshots: color-name: 1.1.4 simple-swizzle: 0.2.2 + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + color@4.2.3: dependencies: color-convert: 2.0.1 @@ -4938,6 +5249,15 @@ snapshots: colorette@2.0.20: {} + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.1.0: {} commander@2.20.3: {} @@ -4960,6 +5280,8 @@ snapshots: dependencies: browserslist: 4.23.3 + core-util-is@1.0.2: {} + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -4991,6 +5313,10 @@ snapshots: transitivePeerDependencies: - postcss + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + data-uri-to-buffer@4.0.1: {} data-view-buffer@1.0.1: @@ -5050,6 +5376,8 @@ snapshots: defu@6.1.4: {} + delayed-stream@1.0.0: {} + detect-indent@6.1.0: {} detect-libc@2.0.3: {} @@ -5076,6 +5404,11 @@ snapshots: eastasianwidth@0.2.0: {} + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + ejs@3.1.10: dependencies: jake: 10.9.2 @@ -5086,6 +5419,8 @@ snapshots: emoji-regex@9.2.2: {} + enabled@2.0.0: {} + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -5303,8 +5638,16 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + + events@3.3.0: {} + expand-template@2.0.3: {} + extend@3.0.2: {} + + extsprintf@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -5333,11 +5676,15 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fecha@4.2.3: {} + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fetch-retry@6.0.0: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -5363,6 +5710,8 @@ snapshots: flatted@3.3.1: {} + fn.name@1.1.0: {} + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -5372,6 +5721,14 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + forever-agent@0.6.1: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -5421,6 +5778,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.4 + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + github-from-package@0.0.0: {} glob-parent@5.1.2: @@ -5489,6 +5850,13 @@ snapshots: graphemer@1.4.0: {} + har-schema@2.0.0: {} + + har-validator@5.1.5: + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -5511,6 +5879,12 @@ snapshots: dependencies: function-bind: 1.1.2 + http-signature@1.2.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 @@ -5643,6 +6017,8 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-typedarray@1.0.0: {} + is-weakref@1.0.2: dependencies: call-bind: 1.0.7 @@ -5651,6 +6027,8 @@ snapshots: isexe@2.0.0: {} + isstream@0.1.2: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -5672,6 +6050,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsbn@0.1.1: {} + jsesc@0.5.0: {} jsesc@2.5.2: {} @@ -5686,6 +6066,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} jsonfile@6.1.0: @@ -5696,6 +6078,13 @@ snapshots: jsonpointer@5.0.1: {} + jsprim@1.4.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5706,6 +6095,8 @@ snapshots: kolorist@1.8.0: {} + kuler@2.0.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -5737,6 +6128,15 @@ snapshots: lodash@4.17.21: {} + logform@2.6.1: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -5760,6 +6160,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-response@3.1.0: {} min-indent@1.0.1: {} @@ -5835,6 +6241,8 @@ snapshots: npm-normalize-package-bin@3.0.1: {} + oauth-sign@0.9.0: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -5854,6 +6262,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -5901,6 +6313,8 @@ snapshots: path-type@4.0.0: {} + performance-now@2.1.0: {} + periscopic@3.1.0: dependencies: '@types/estree': 1.0.5 @@ -6012,6 +6426,10 @@ snapshots: pretty-bytes@6.1.1: {} + process@0.11.10: {} + + psl@1.9.0: {} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -6035,6 +6453,8 @@ snapshots: dependencies: side-channel: 1.0.6 + qs@6.5.3: {} + queue-microtask@1.2.3: {} queue-tick@1.0.1: {} @@ -6062,6 +6482,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -6098,6 +6526,29 @@ snapshots: dependencies: jsesc: 0.5.0 + request@2.88.2: + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -6110,6 +6561,12 @@ snapshots: reusify@1.0.4: {} + rewire@7.0.0: + dependencies: + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + rimraf@2.7.1: dependencies: glob: 7.2.3 @@ -6171,6 +6628,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + sander@0.5.1: dependencies: es6-promise: 3.3.1 @@ -6280,6 +6741,20 @@ snapshots: sourcemap-codec@1.4.8: {} + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + stack-trace@0.0.10: {} + streamx@2.20.1: dependencies: fast-fifo: 1.3.2 @@ -6546,6 +7021,8 @@ snapshots: dependencies: b4a: 1.6.6 + text-hex@1.0.0: {} + text-table@0.2.0: {} thenify-all@1.6.0: @@ -6576,12 +7053,19 @@ snapshots: totalist@3.0.1: {} + tough-cookie@2.5.0: + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + tr46@0.0.3: {} tr46@1.0.1: dependencies: punycode: 2.3.1 + triple-beam@1.4.1: {} + ts-api-utils@1.3.0(typescript@5.6.2): dependencies: typescript: 5.6.2 @@ -6596,6 +7080,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tweetnacl@0.14.5: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6684,6 +7170,16 @@ snapshots: util-deprecate@1.0.2: {} + uuid@3.4.0: {} + + uuid@8.3.2: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + vite-plugin-pwa@0.20.5(@vite-pwa/assets-generator@0.2.6)(vite@5.4.6(@types/node@22.5.5)(terser@5.33.0))(workbox-build@7.1.1)(workbox-window@7.1.0): dependencies: debug: 4.3.7 @@ -6758,6 +7254,26 @@ snapshots: dependencies: isexe: 2.0.0 + winston-transport@4.8.0: + dependencies: + logform: 2.6.1 + readable-stream: 4.5.2 + triple-beam: 1.4.1 + + winston@3.15.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.6.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.8.0 + word-wrap@1.2.5: {} workbox-background-sync@7.1.0: diff --git a/src/lib/components/CookieConsent.svelte b/src/lib/components/CookieConsent.svelte index 8872559..f850191 100644 --- a/src/lib/components/CookieConsent.svelte +++ b/src/lib/components/CookieConsent.svelte @@ -26,7 +26,9 @@ >

We use cookies to ensure you get the best experience on our website. By continuing, you agree - to our use of cookies. + to our use of cookies. For details, please see our Privacy Policy.

- import { createEventDispatcher } from 'svelte'; - import { signOut } from '$lib/utils/supabase/auth'; + import { createEventDispatcher, onDestroy } from 'svelte'; import type { SupabaseClient } from '@supabase/supabase-js'; + import { basket, type Basket } from '$lib/stores/basket.js'; + const dispatch = createEventDispatcher(); function emitSignedOutEvent() { @@ -9,13 +10,39 @@ } // Access the supabase client from the layout data - export let supabase: SupabaseClient | null; + export let supabase: SupabaseClient; - async function signOutClick() { - if (!supabase) return; - await signOut(supabase, emitSignedOutEvent); + let localBasket: Basket; + const unsubscribe = basket.subscribe((value) => { + localBasket = value; + }); + + onDestroy(unsubscribe); + + async function signOut() { + const { error } = await supabase.auth.signOut(); + + if (error) { + console.error('Error signing out:', error.message); + alert('Error signing out'); + return; + } + + try { + emitSignedOutEvent(); + + // Clear the basket + localBasket = { items: [], subtotal: 0 }; + basket.set(localBasket); + unsubscribe(); + if (typeof localStorage !== 'undefined') { + localStorage.removeItem('basket'); + } + } catch (err) { + console.error('Error clearing basket:', err); + alert('Error clearing basket'); + } } - + diff --git a/src/lib/components/SignUp.svelte b/src/lib/components/SignUp.svelte deleted file mode 100644 index dbb92f1..0000000 --- a/src/lib/components/SignUp.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - -
-
- -
-
- -
-
- -
-
diff --git a/src/lib/components/checkout/BasketItem.svelte b/src/lib/components/checkout/BasketItem.svelte index 3924f37..1a50ef5 100644 --- a/src/lib/components/checkout/BasketItem.svelte +++ b/src/lib/components/checkout/BasketItem.svelte @@ -11,10 +11,11 @@ export let price = 0; export let quantity = 0; + $: dispatch('changeQuantity', { itemId, quantity }); + function reduceQuantityByOne() { if (quantity > 1) { quantity--; - dispatch('changeQuantity', { itemId, quantity }); } else { dispatch('removeItem', { itemId }); } @@ -22,7 +23,6 @@ function increaseQuantityByOne() { quantity++; - dispatch('changeQuantity', { itemId, quantity }); } diff --git a/src/lib/types/supabase.d.ts b/src/lib/types/supabase.d.ts index d774c18..199b7cd 100644 --- a/src/lib/types/supabase.d.ts +++ b/src/lib/types/supabase.d.ts @@ -34,6 +34,32 @@ export type Database = { } public: { Tables: { + account_deletion_requests: { + Row: { + requested_at: string + token: string + user_id: string + } + Insert: { + requested_at?: string + token: string + user_id: string + } + Update: { + requested_at?: string + token?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "account_deletion_requests_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } credit_transactions: { Row: { created_at: string diff --git a/src/lib/utils/brevo/email.ts b/src/lib/utils/brevo/email.ts new file mode 100644 index 0000000..d0f4cc6 --- /dev/null +++ b/src/lib/utils/brevo/email.ts @@ -0,0 +1,34 @@ +import brevo from '@getbrevo/brevo'; +import { + VITE_BREVO_API_KEY, + VITE_BREVO_SENDER_EMAIL, + VITE_BREVO_SENDER_NAME +} from '$env/static/private'; + +const apiInstance = new brevo.TransactionalEmailsApi(); +const apiKey = apiInstance.authentications['apiKey']; +if (!VITE_BREVO_API_KEY) { + throw new Error('Brevo API key is not configured'); +} +apiKey.apiKey = VITE_BREVO_API_KEY; + +const sendEmail = async (subject: string, htmlContentString: string, toAddress: string) => { + const sendSmtpEmail = new brevo.SendSmtpEmail(); + + sendSmtpEmail.subject = subject; + sendSmtpEmail.htmlContent = htmlContentString; + sendSmtpEmail.sender = { name: VITE_BREVO_SENDER_NAME, email: VITE_BREVO_SENDER_EMAIL }; + sendSmtpEmail.to = [{ email: toAddress, name: toAddress }]; + sendSmtpEmail.replyTo = { email: VITE_BREVO_SENDER_EMAIL, name: VITE_BREVO_SENDER_NAME }; + + apiInstance.sendTransacEmail(sendSmtpEmail).then( + function (data) { + console.log('API called successfully. Returned data: ' + JSON.stringify(data)); + }, + function (error) { + console.error(error); + } + ); +}; + +export { sendEmail }; diff --git a/src/lib/utils/logger/logger.ts b/src/lib/utils/logger/logger.ts new file mode 100644 index 0000000..b56c756 --- /dev/null +++ b/src/lib/utils/logger/logger.ts @@ -0,0 +1,40 @@ +import { createLogger, transports, format } from 'winston'; +import { WinstonTransport as AxiomTransport } from '@axiomhq/winston'; +import { VITE_AXIOM_DATASET, VITE_AXIOM_TOKEN } from '$env/static/private'; + +const axiomTransport = new AxiomTransport({ + dataset: VITE_AXIOM_DATASET, + token: VITE_AXIOM_TOKEN +}); + +const logger = createLogger({ + level: 'info', + defaultMeta: { service: 'example-site' }, // Change this so you can identify your website if you have multiple logging into Axiom + format: format.combine( + format.errors({ stack: true }), // Captures error stack traces + format.timestamp(), + format.json() // Formats logs as JSON for structured logging + ), + transports: [ + new transports.Console({ + format: format.combine( + format.colorize(), + format.printf(({ timestamp, level, message, stack }) => { + // Stack traces will be included in error messages automatically + return `${timestamp} [${level.toUpperCase()}]: ${message}${stack ? `\n${stack}` : ''}`; + }) + ) + }), + axiomTransport + ], + exceptionHandlers: [axiomTransport], // Logs uncaught exceptions + rejectionHandlers: [axiomTransport] // Logs unhandled promise rejections +}); + +// Uncomment this for testing your initial logging setup +// logger.log({ +// level: 'info', +// message: 'Logger successfully setup' +// }); + +export default logger; diff --git a/src/lib/utils/rateLimiter.ts b/src/lib/utils/rateLimiter.ts new file mode 100644 index 0000000..1328b24 --- /dev/null +++ b/src/lib/utils/rateLimiter.ts @@ -0,0 +1,30 @@ +type RateLimitStore = { + [key: string]: { count: number; lastRequest: number }; +}; + +const rateLimitStore: RateLimitStore = {}; +const RATE_LIMIT_WINDOW = 60 * 5000; // 5 minutes +const MAX_REQUESTS = 5; + +export function isRateLimited(ip: string): boolean { + const currentTime = Date.now(); + const requestInfo = rateLimitStore[ip]; + + if (!requestInfo) { + rateLimitStore[ip] = { count: 1, lastRequest: currentTime }; + return false; + } + + if (currentTime - requestInfo.lastRequest > RATE_LIMIT_WINDOW) { + rateLimitStore[ip] = { count: 1, lastRequest: currentTime }; + return false; + } + + if (requestInfo.count >= MAX_REQUESTS) { + return true; + } + + rateLimitStore[ip].count += 1; + rateLimitStore[ip].lastRequest = currentTime; + return false; +} diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index 4052695..659f3ed 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -4,6 +4,8 @@ import type { Database, Tables, TablesInsert } from '$lib/types/supabase'; import { PUBLIC_SUPABASE_URL } from '$env/static/public'; import { SUPABASE_SERVICE_ROLE_KEY } from '$env/static/private'; import { stripe as stripeClient } from '$lib/utils/stripe'; +import logger from '$lib/utils/logger/logger'; +import { sendEmail } from '../brevo/email'; const toDateTime = (secs: number) => { const t = new Date(+0); // Unix epoch start. @@ -39,7 +41,7 @@ const upsertProductRecord = async (product: stripe.Product) => { const { error: upsertError } = await supabaseAdmin.from('products').upsert([productData]); if (upsertError) throw new Error(`Product insert/update failed: ${upsertError.message}`); - console.log(`Product inserted/updated: ${product.id}`); + logger.info(`Product inserted/updated: ${product.id}`); }; const upsertPriceRecord = async (price: stripe.Price, retryCount = 0, maxRetries = 3) => { @@ -61,7 +63,7 @@ const upsertPriceRecord = async (price: stripe.Price, retryCount = 0, maxRetries if (upsertError?.message.includes('foreign key constraint')) { if (retryCount < maxRetries) { - console.log(`Retry attempt ${retryCount + 1} for price ID: ${price.id}`); + logger.info(`Retry attempt ${retryCount + 1} for price ID: ${price.id}`); await new Promise((resolve) => setTimeout(resolve, 2000)); await upsertPriceRecord(price, retryCount + 1, maxRetries); } else { @@ -72,7 +74,7 @@ const upsertPriceRecord = async (price: stripe.Price, retryCount = 0, maxRetries } else if (upsertError) { throw new Error(`Price insert/update failed: ${upsertError.message}`); } else { - console.log(`Price inserted/updated: ${price.id}`); + logger.info(`Price inserted/updated: ${price.id}`); } }; @@ -82,13 +84,13 @@ const deleteProductRecord = async (product: stripe.Product) => { .delete() .eq('id', product.id); if (deletionError) throw new Error(`Product deletion failed: ${deletionError.message}`); - console.log(`Product deleted: ${product.id}`); + logger.info(`Product deleted: ${product.id}`); }; const deletePriceRecord = async (price: stripe.Price) => { const { error: deletionError } = await supabaseAdmin.from('prices').delete().eq('id', price.id); if (deletionError) throw new Error(`Price deletion failed: ${deletionError.message}`); - console.log(`Price deleted: ${price.id}`); + logger.info(`Price deleted: ${price.id}`); }; const upsertCustomerToSupabase = async (uuid: string, customerId: string) => { @@ -254,11 +256,18 @@ const manageSubscriptionStatusChange = async ( trial_end: subscription.trial_end ? toDateTime(subscription.trial_end).toISOString() : null }; - const { error: upsertError } = await supabaseAdmin - .from('subscriptions') - .upsert([subscriptionData]); - if (upsertError) throw new Error(`Subscription insert/update failed: ${upsertError.message}`); - console.log(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`); + try { + const { error: upsertError } = await supabaseAdmin + .from('subscriptions') + .upsert([subscriptionData]); + if (upsertError) { + throw new Error(`Subscription insert/update failed: ${upsertError.message}`); + } + } catch (error) { + logger.error(error); + } + + logger.info(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`); // // For a new subscription copy the billing details to the customer object. // // NOTE: This is a costly operation and should happen at the very end. @@ -271,7 +280,7 @@ const manageSubscriptionStatusChange = async ( }; const recordProductPurchase = async (userId: string, productId: string, priceId: string) => { - console.log( + logger.info( `Recording product purchase for user [${userId}] and product [${productId}] with price [${priceId}]` ); @@ -284,40 +293,48 @@ const recordProductPurchase = async (userId: string, productId: string, priceId: throw new Error(`Failed to record product purchase: ${error.message}`); } } catch (error) { - console.error(error); + logger.error(error); } - console.log(`Product purchased recorded for user [${userId}] and product [${productId}]`); + logger.info(`Product purchased recorded for user [${userId}] and product [${productId}]`); }; const hasProductAccess = async (userId: string, productId: string) => { // Check if user has purchased the product - const { data: purchases, error: purchaseError } = await supabaseAdmin - .from('purchases') - .select('*') - .eq('user_id', userId) - .eq('product_id', productId); + try { + const { data: purchases, error: purchaseError } = await supabaseAdmin + .from('purchases') + .select('*') + .eq('user_id', userId) + .eq('product_id', productId); - if (purchaseError) throw new Error(purchaseError.message); + if (purchaseError) throw new Error(purchaseError.message); - if (purchases && purchases.length > 0) { - // User has purchased the product - return true; + if (purchases && purchases.length > 0) { + // User has purchased the product + return true; + } + } catch (error) { + logger.error(error); } // Check if user has an active subscription for the product - const { data: subscriptions, error: subError } = await supabaseAdmin - .from('subscriptions') - .select('*') - .eq('user_id', userId) - .eq('product_id', productId) - .in('status', ['active', 'trialing']); + try { + const { data: subscriptions, error: subError } = await supabaseAdmin + .from('subscriptions') + .select('*') + .eq('user_id', userId) + .eq('product_id', productId) + .in('status', ['active', 'trialing']); - if (subError) throw new Error(subError.message); + if (subError) throw new Error(subError.message); - if (subscriptions && subscriptions.length > 0) { - // User has an active subscription - return true; + if (subscriptions && subscriptions.length > 0) { + // User has an active subscription + return true; + } + } catch (error) { + logger.error(error); } return false; @@ -381,7 +398,7 @@ const updateUserCredits = async (userId: string, creditsChange: number, descript await logCreditTransaction(userId, creditsChange, description); - console.log( + logger.info( `${creditsChange > 0 ? 'Added' : 'Deducted'} ${Math.abs(creditsChange)} credits to user [${userId}]` ); }; @@ -390,7 +407,7 @@ const addCredits = async (userId: string, creditsToAdd: number, description: str try { await updateUserCredits(userId, creditsToAdd, description); } catch (error) { - console.error(error); + logger.error(error); } }; @@ -398,7 +415,7 @@ const deductCredits = async (userId: string, creditsToDeduct: number, descriptio try { await updateUserCredits(userId, -creditsToDeduct, description); } catch (error) { - console.error(error); + logger.error(error); } }; @@ -480,6 +497,142 @@ const getProductById = async (productId: string) => { return product as Product; }; +const requestAccountDeletion = async (userId: string, userEmail: string, baseUrl: string) => { + const token = crypto.randomUUID(); + + const { error: insertError } = await supabaseAdmin.from('account_deletion_requests').upsert({ + user_id: userId, + token, + requested_at: new Date().toISOString() + }); + + if (insertError) throw new Error(`Account deletion request failed: ${insertError.message}`); + + const deletionLink = `${baseUrl}/api/delete-account?token=${token}`; + + // Send deletion email using Brevo + try { + await sendEmail( + 'Confirm Account Deletion', + `

Click here to confirm your account deletion.

`, + userEmail + ); + console.log('Deletion email sent successfully.'); + } catch (emailError) { + console.error('Failed to send deletion email:', emailError); + throw new Error('Failed to send confirmation email.'); + } +}; + +const deleteAccount = async (token: string) => { + // Verify the token + const { data: deletionRequest, error } = await supabaseAdmin + .from('account_deletion_requests') + .select('user_id') + .eq('token', token) + .single(); + + if (error || !deletionRequest) { + return new Response('Invalid or expired token', { status: 400 }); + } + + // N.B. The SQL here will either cascade delete or nullify the foreign key constraints depending on the table, to enable anonymisation + const { error: deletionError } = await supabaseAdmin.auth.admin.deleteUser( + deletionRequest.user_id + ); + if (deletionError) { + throw new Error(`Failed to delete user: ${deletionError.message}`); + } +}; + +const getUserSubscriptions = async (userId: string) => { + try { + // Step 1: Retrieve subscription data + const { data: subscriptions, error } = await supabaseAdmin + .from('subscriptions') + .select('*') + .eq('user_id', userId); + + if (error) { + logger.error('Error retrieving subscriptions:', error.message); + throw new Error('Failed to fetch subscriptions'); + } + + // Step 2: Map through each subscription and fetch product and price details + const subscriptionDetails = await Promise.all( + subscriptions.map(async (sub) => { + // Fetch product and price details from Stripe + const product = await stripeClient.products.retrieve(sub.product_id); + const price = await stripeClient.prices.retrieve(sub.price_id); + + const expiryDate = new Date(sub.current_period_end); + + // Format output data + return { + productName: product.name, + amount: (price.unit_amount / 100).toFixed(2), + currency: price.currency.toUpperCase(), + interval: price.recurring?.interval || 'one-time', + expiryDate: expiryDate.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }), + status: sub.status + }; + }) + ); + + return subscriptionDetails; + } catch (error) { + logger.error('An error occurred while retrieving subscription details:', error); + throw new Error('Could not retrieve subscription details'); + } +}; + +const getUserTransactions = async (userId: string) => { + try { + // Step 1: Retrieve the customer ID associated with the user + const { data, error } = await supabaseAdmin + .from('customers') + .select('stripe_customer_id') + .eq('id', userId) + .single(); + + if (error) { + console.error('Error retrieving user data:', error.message); + throw new Error('Failed to fetch user data'); + } + + const customerId = data?.stripe_customer_id; + if (!customerId) { + throw new Error('No Stripe customer ID found for this user'); + } + + // Step 2: Fetch charge transactions from Stripe + const charges = await stripeClient.charges.list({ customer: customerId }); + + // Step 3: Format transaction data + const transactions = charges.data.map((charge) => ({ + amount: (charge.amount / 100).toFixed(2), + currency: charge.currency.toUpperCase(), + description: charge.description ?? 'No description provided', + status: charge.status, + created: new Date(charge.created * 1000).toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }), + receipt_url: charge.receipt_url + })); + + return transactions; + } catch (error) { + console.error('An error occurred while retrieving transactions:', error); + throw new Error('Could not retrieve transactions'); + } +}; + export { upsertProductRecord, upsertPriceRecord, @@ -495,5 +648,9 @@ export { getActiveProductsWithPrices, getProductById, upsertCustomerToSupabase, - getStripeCustomerId + getStripeCustomerId, + getUserSubscriptions, + getUserTransactions, + requestAccountDeletion, + deleteAccount }; diff --git a/src/routes/(policies)/cookies/+page.svelte b/src/routes/(policies)/cookies/+page.svelte new file mode 100644 index 0000000..511b4e0 --- /dev/null +++ b/src/routes/(policies)/cookies/+page.svelte @@ -0,0 +1,85 @@ +
+

Cookie Policy

+

Effective Date: [Insert Date]

+ +
+

1. Introduction

+

+ This Cookie Policy explains how [Your Company/Website Name] ("we", "us", or "our") uses + cookies and similar technologies to collect and store information when you visit our website. +

+
+ +
+

2. What Are Cookies?

+

+ Cookies are small text files stored on your device (computer, tablet, smartphone) by your web + browser when you visit a website. They help us remember your preferences and improve your + experience on our website. +

+
+ +
+

3. Types of Cookies We Use

+ +
+ +
+

4. How We Use Cookies

+

+ We use cookies to improve the functionality and performance of our website, analyse visitor + usage, and provide personalised content and advertisements. This helps us improve our website + and better understand our audience. +

+
+ +
+

5. Managing Your Cookie Preferences

+

+ You have the right to accept or reject cookies. You can manage your cookie preferences by + adjusting your browser settings. However, disabling certain cookies may affect your experience + on our website. For specific instructions on managing cookies, please consult your browser's + help documentation. +

+
+ +
+

6. Changes to Our Cookie Policy

+

+ We may update this Cookie Policy from time to time. Significant changes will be posted on this + page. We encourage you to review this policy periodically to stay informed about our use of + cookies. +

+
+ +
+

Contact Us

+

+ For questions about our Cookie Policy, please contact us at [Your Contact Email] + or visit our Contact Page. +

+
+
diff --git a/src/routes/(policies)/privacy/+page.svelte b/src/routes/(policies)/privacy/+page.svelte new file mode 100644 index 0000000..6901837 --- /dev/null +++ b/src/routes/(policies)/privacy/+page.svelte @@ -0,0 +1,125 @@ +
+

Privacy Policy

+

Effective Date: [Insert Date]

+ +
+

1. Introduction

+

+ This Privacy Policy explains how we at [Your Company/Website Name] collect, use, and protect + your personal information when you visit our website. We are committed to safeguarding your + data and complying with relevant data protection laws, including the General Data Protection + Regulation (GDPR). +

+
+ +
+

2. Information We Collect

+

+ We collect personal and non-personal information to provide our services and enhance your + experience. +

+ +
+ +
+

3. How We Use Your Information

+ +
+ +
+

4. Sharing Information with Third Parties

+

+ We work with trusted third-party providers to offer secure services, such as payment + processing and authentication. +

+
+

Stripe

+

+ For payment processing, we use Stripe. Please refer to their + Privacy Policy for details. +

+
+
+

Supabase

+

+ For database and authentication, we use Supabase. See their + Privacy Policy for more information. +

+
+
+ +
+

5. Cookies and Tracking

+

+ We use cookies to provide essential site functionality, analytics, and improved user + experience. By continuing to use our site, you consent to our use of cookies. You can manage + your cookie preferences in your browser settings. For more details, please refer to our Cookie Policy. +

+
+ +
+

6. Your Data Rights

+

+ Under GDPR, you have the right to access, correct, delete, and restrict the processing of your + personal data. To make a request, please contact us at [Your Contact Email]. +

+
+ +
+

7. Data Security

+

+ We take reasonable steps to protect your information through encryption and secure access + protocols. However, no online service can be 100% secure. +

+
+ +
+

8. Policy Updates

+

+ We may update this policy periodically. Significant changes will be posted on this page, so + please review it regularly. +

+
+ +
+

Contact Us

+

+ If you have questions about this Privacy Policy, please contact us at [Your Contact Email] + or visit our Contact Page. +

+
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 378abc4..90aa24c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -69,8 +69,12 @@ d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z" > -

Copyright © 2024 - All right reserved

+

Copyright © 2024 - All rights reserved

+
+ Privacy Policy + Cookie Policy +