From a5e50821b7e0dba0400a5f8ada6d3741a92de9d3 Mon Sep 17 00:00:00 2001 From: keyan Date: Thu, 14 Dec 2023 11:30:51 -0600 Subject: [PATCH] gofac yourself --- api/resolvers/item.js | 6 +- api/resolvers/lnurl.js | 5 +- api/resolvers/ofac.js | 24 ++ api/resolvers/wallet.js | 12 +- components/upvote.js | 5 +- package-lock.json | 363 ++++++++++++++++++ package.json | 2 + pages/api/graphql.js | 1 + pages/api/lnurlp/[username]/pay.js | 4 +- pages/wallet.js | 12 +- .../20231212235400_ofac/migration.sql | 31 ++ prisma/schema.prisma | 8 + worker/index.js | 3 + worker/ofac.js | 78 ++++ 14 files changed, 540 insertions(+), 14 deletions(-) create mode 100644 api/resolvers/ofac.js create mode 100644 prisma/migrations/20231212235400_ofac/migration.sql create mode 100644 worker/ofac.js diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 4800c0b0..aeb7c84f 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -20,6 +20,7 @@ import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDel import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications' import { datePivot, whenRange } from '../../lib/time' import { imageFeesInfo, uploadIdsFromText } from './image' +import assertGofacYourself from './ofac' export async function commentFilterClause (me, models) { let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}` @@ -291,7 +292,7 @@ export default { return count }, - items: async (parent, { sub, sort, type, cursor, name, when, from, to, by, limit = LIMIT }, { me, models }) => { + items: async (parent, { sub, sort, type, cursor, name, when, from, to, by, limit = LIMIT }, { me, models, headers }) => { const decodedCursor = decodeCursor(cursor) let items, user, pins, subFull, table @@ -735,8 +736,9 @@ export default { return id }, - act: async (parent, { id, sats, hash, hmac }, { me, models, lnd }) => { + act: async (parent, { id, sats, hash, hmac }, { me, models, lnd, headers }) => { await ssValidate(amountSchema, { amount: sats }) + await assertGofacYourself({ models, headers }) // disallow self tips except anons if (me) { diff --git a/api/resolvers/lnurl.js b/api/resolvers/lnurl.js index 31502b10..0b8c7d25 100644 --- a/api/resolvers/lnurl.js +++ b/api/resolvers/lnurl.js @@ -1,6 +1,7 @@ import { randomBytes } from 'crypto' import { bech32 } from 'bech32' import { GraphQLError } from 'graphql' +import assertGofacYourself from './ofac' function encodedUrl (iurl, tag, k1) { const url = new URL(iurl) @@ -28,7 +29,9 @@ export default { createAuth: async (parent, args, { models }) => { return await models.lnAuth.create({ data: { k1: k1() } }) }, - createWith: async (parent, args, { me, models }) => { + createWith: async (parent, args, { me, models, headers }) => { + await assertGofacYourself({ models, headers }) + if (!me) { throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } }) } diff --git a/api/resolvers/ofac.js b/api/resolvers/ofac.js new file mode 100644 index 00000000..26b9b2a6 --- /dev/null +++ b/api/resolvers/ofac.js @@ -0,0 +1,24 @@ +import { GraphQLError } from 'graphql' + +// this function makes america more secure apparently +export default async function assertGofacYourself ({ models, headers }) { + const country = await gOFACYourself({ models, headers }) + if (!country) return + + throw new GraphQLError( + `Your IP address is in ${country}. We cannot provide financial services to residents of ${country}.`, + { extensions: { code: 'FORBIDDEN' } }) +} + +export async function gOFACYourself ({ models, headers }) { + const { 'x-forwarded-for': xForwardedFor, 'x-real-ip': xRealIp } = headers + const ip = xRealIp || xForwardedFor?.split(',')?.[0] + if (!ip) return false + + const countries = await models.$queryRaw` + SELECT * FROM "OFAC" WHERE iprange("startIP","endIP") >>= ${ip}::ipaddress` + + if (countries.length === 0) return false + + return countries[0].country +} diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 5e0eb142..f7ed7a42 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -10,6 +10,7 @@ import { msatsToSats, msatsToSatsDecimal } from '../../lib/format' import { amountSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate' import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '../../lib/constants' import { datePivot } from '../../lib/time' +import assertGofacYourself from './ofac' export async function getInvoice (parent, { id }, { me, models, lnd }) { const inv = await models.invoice.findUnique({ @@ -266,8 +267,9 @@ export default { }, Mutation: { - createInvoice: async (parent, { amount, hodlInvoice = false, expireSecs = 3600 }, { me, models, lnd }) => { + createInvoice: async (parent, { amount, hodlInvoice = false, expireSecs = 3600 }, { me, models, lnd, headers }) => { await ssValidate(amountSchema, { amount }) + await assertGofacYourself({ models, headers }) let expirePivot = { seconds: expireSecs } let invLimit = INV_PENDING_LIMIT @@ -313,7 +315,9 @@ export default { } }, createWithdrawl: createWithdrawal, - sendToLnAddr: async (parent, { addr, amount, maxFee, comment, ...payer }, { me, models, lnd }) => { + sendToLnAddr: async (parent, { addr, amount, maxFee, comment, ...payer }, { me, models, lnd, headers }) => { + await assertGofacYourself({ models, headers }) + if (!me) { throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } }) } @@ -432,9 +436,11 @@ export default { } } -async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd }) { +async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers }) { await ssValidate(withdrawlSchema, { invoice, maxFee }) + await assertGofacYourself({ models, headers }) + // remove 'lightning:' prefix if present invoice = invoice.replace(/^lightning:/, '') diff --git a/components/upvote.js b/components/upvote.js index 61987ebd..bf452f0a 100644 --- a/components/upvote.js +++ b/components/upvote.js @@ -14,6 +14,7 @@ import { LightningConsumer, useLightning } from './lightning' import { numWithUnits } from '../lib/format' import { payOrLoginError, useInvoiceModal } from './invoice' import useDebounceCallback from './use-debounce-callback' +import { useToast } from './toast' const getColor = (meSats) => { if (!meSats || meSats <= 10) { @@ -72,6 +73,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats } const ref = useRef() const me = useMe() const strike = useLightning() + const toaster = useToast() const [setWalkthrough] = useMutation( gql` mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) { @@ -175,9 +177,10 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats } return } console.error(error) + toaster.danger(error?.message || error?.toString?.()) }) setPendingSats(0) - }, 500, [act, item?.id, showInvoiceModal, setPendingSats]) + }, 500, [act, toaster, item?.id, showInvoiceModal, setPendingSats]) const disabled = useMemo(() => item?.mine || item?.meForward || item?.deletedAt, [item?.mine, item?.meForward, item?.deletedAt]) diff --git a/package-lock.json b/package-lock.json index 4e7d0f1d..d3dd5484 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "canonical-json": "0.0.4", "clipboard-copy": "^4.0.1", "cross-fetch": "^4.0.0", + "csv-parser": "^3.0.0", "domino": "^2.1.6", "formik": "^2.4.5", "github-slugger": "^2.0.0", @@ -79,6 +80,7 @@ "tldts": "^6.0.16", "tsx": "^3.13.0", "unist-util-visit": "^5.0.0", + "unzipper": "^0.10.14", "url-unshort": "^6.1.0", "web-push": "^3.6.6", "webln": "^0.3.2", @@ -4639,11 +4641,31 @@ "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bigi": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/bigi/-/bigi-1.4.2.tgz", "integrity": "sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==" }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5014,6 +5036,14 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", @@ -5022,6 +5052,14 @@ "node": ">=4" } }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -5263,6 +5301,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5854,6 +5903,20 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, + "node_modules/csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -6272,6 +6335,36 @@ "node": ">=4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -7722,6 +7815,31 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -9448,6 +9566,11 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, "node_modules/ln-service": { "version": "57.1.3", "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-57.1.3.tgz", @@ -11170,6 +11293,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -12456,6 +12590,11 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -14012,6 +14151,11 @@ "resolved": "https://registry.npmjs.org/serviceworker-storage/-/serviceworker-storage-0.1.0.tgz", "integrity": "sha512-Vum11Npe8oiFYY05OIhD6obfVP3oCSfBj/NKQGzNLbn6Fr5424j1pv/SvPcbVrDIovdC3EmgGxLgfsLFXgZR1A==" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -14844,6 +14988,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -15263,6 +15415,50 @@ "node": ">=4" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -19657,11 +19853,25 @@ "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, + "big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==" + }, "bigi": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/bigi/-/bigi-1.4.2.tgz", "integrity": "sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==" }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -19953,11 +20163,21 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==" + }, "buffer-writer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==" + }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -20119,6 +20339,14 @@ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -20559,6 +20787,14 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, + "csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "requires": { + "minimist": "^1.2.0" + } + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -20842,6 +21078,38 @@ "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", "integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==" }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -21896,6 +22164,27 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "optional": true }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -23143,6 +23432,11 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, "ln-service": { "version": "57.1.3", "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-57.1.3.tgz", @@ -24214,6 +24508,14 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -25088,6 +25390,11 @@ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==" }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -26245,6 +26552,11 @@ "resolved": "https://registry.npmjs.org/serviceworker-storage/-/serviceworker-storage-0.1.0.tgz", "integrity": "sha512-Vum11Npe8oiFYY05OIhD6obfVP3oCSfBj/NKQGzNLbn6Fr5424j1pv/SvPcbVrDIovdC3EmgGxLgfsLFXgZR1A==" }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -26840,6 +27152,11 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" + }, "trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -27141,6 +27458,52 @@ "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", "integrity": "sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==" }, + "unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + }, + "dependencies": { + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", diff --git a/package.json b/package.json index 959f664a..9f9e2b47 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "canonical-json": "0.0.4", "clipboard-copy": "^4.0.1", "cross-fetch": "^4.0.0", + "csv-parser": "^3.0.0", "domino": "^2.1.6", "formik": "^2.4.5", "github-slugger": "^2.0.0", @@ -82,6 +83,7 @@ "tldts": "^6.0.16", "tsx": "^3.13.0", "unist-util-visit": "^5.0.0", + "unzipper": "^0.10.14", "url-unshort": "^6.1.0", "web-push": "^3.6.6", "webln": "^0.3.2", diff --git a/pages/api/graphql.js b/pages/api/graphql.js index 5532183d..cb3021c1 100644 --- a/pages/api/graphql.js +++ b/pages/api/graphql.js @@ -56,6 +56,7 @@ export default startServerAndCreateNextHandler(apolloServer, { const session = await getServerSession(req, res, getAuthOptions(req)) return { models, + headers: req.headers, lnd, me: session ? session.user diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index ed2513d8..1347ce20 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -8,14 +8,16 @@ import { createHash } from 'crypto' import { datePivot } from '../../../../lib/time' import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants' import { ssValidate, lud18PayerDataSchema } from '../../../../lib/validate' +import assertGofacYourself from '../../../../api/resolvers/ofac' -export default async ({ query: { username, amount, nostr, comment, payerdata: payerData } }, res) => { +export default async ({ query: { username, amount, nostr, comment, payerdata: payerData }, headers }, res) => { const user = await models.user.findUnique({ where: { name: username } }) if (!user) { return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` }) } try { + await assertGofacYourself({ models, headers }) // if nostr, decode, validate sig, check tags, set description hash let description, descriptionHash, noteStr if (nostr) { diff --git a/pages/wallet.js b/pages/wallet.js index fca926fd..cac51fb0 100644 --- a/pages/wallet.js +++ b/pages/wallet.js @@ -129,7 +129,6 @@ export function FundForm () { initial={{ amount: 1000 }} - initialError={error?.toString()} schema={amountSchema} onSubmit={async ({ amount }) => { const { data } = await createInvoice({ variables: { amount: Number(amount) } }) @@ -231,7 +230,6 @@ export function InvWithdrawal () { invoice: '', maxFee: maxFeeDefault }} - initialError={error ? error.toString() : undefined} schema={withdrawlSchema} onSubmit={async ({ invoice, maxFee }) => { const { data } = await createWithdrawl({ variables: { invoice, maxFee: Number(maxFee) } }) @@ -319,12 +317,15 @@ export function LnWithdrawal () { encodedUrl } }`) + const toaster = useToast() useEffect(() => { - createWith() - }, []) + createWith().catch(e => { + toaster.danger(e?.message || e?.toString?.()) + }) + }, [createWith, toaster]) - if (error) return
error
+ if (error) return if (!data) { return @@ -377,7 +378,6 @@ export function LnAddrWithdrawal () { email: '' }} schema={formSchema} - initialError={error ? error.toString() : undefined} onSubmit={async ({ amount, maxFee, ...values }) => { const { data } = await sendToLnAddr({ variables: { diff --git a/prisma/migrations/20231212235400_ofac/migration.sql b/prisma/migrations/20231212235400_ofac/migration.sql new file mode 100644 index 00000000..79462e6d --- /dev/null +++ b/prisma/migrations/20231212235400_ofac/migration.sql @@ -0,0 +1,31 @@ +CREATE EXTENSION IF NOT EXISTS ip4r; + +-- CreateTable +CREATE TABLE "OFAC" ( + "id" SERIAL NOT NULL, + "startIP" ipaddress NOT NULL, + "endIP" ipaddress NOT NULL, + "country" TEXT NOT NULL, + "countryCode" TEXT NOT NULL, + + CONSTRAINT "OFAC_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "OFAC_start_ip_end_ip_idx" ON "OFAC" USING GIST (iprange("startIP", "endIP")); + +CREATE OR REPLACE FUNCTION create_ofac_job() +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE +BEGIN + INSERT INTO pgboss.schedule (name, cron, timezone) VALUES ('ofac', '0 3 * * *', 'America/Chicago') ON CONFLICT DO NOTHING; + return 0; +EXCEPTION WHEN OTHERS THEN + return 0; +END; +$$; + +SELECT create_ofac_job(); +DROP FUNCTION create_ofac_job(); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bb95a363..f6fe52e8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -588,6 +588,14 @@ model Account { @@map("accounts") } +model OFAC { + id Int @id @default(autoincrement()) + startIP Unsupported("ipaddress") + endIP Unsupported("ipaddress") + country String + countryCode String +} + model Session { id Int @id @default(autoincrement()) sessionToken String @unique(map: "sessions.session_token_unique") @map("session_token") diff --git a/worker/index.js b/worker/index.js index a8754f5a..38988d0e 100644 --- a/worker/index.js +++ b/worker/index.js @@ -18,6 +18,7 @@ import { imgproxy } from './imgproxy.js' import { deleteItem } from './ephemeralItems.js' import { deleteUnusedImages } from './deleteUnusedImages.js' import { territoryBilling } from './territory.js' +import { ofac } from './ofac.js' const { loadEnvConfig } = nextEnv const { ApolloClient, HttpLink, InMemoryCache } = apolloClient @@ -27,6 +28,7 @@ loadEnvConfig('..') async function work () { const boss = new PgBoss(process.env.DATABASE_URL) const models = new PrismaClient() + const apollo = new ApolloClient({ link: new HttpLink({ uri: `${process.env.SELF_URL}/api/graphql`, @@ -88,6 +90,7 @@ async function work () { await boss.work('deleteItem', jobWrapper(deleteItem)) await boss.work('deleteUnusedImages', jobWrapper(deleteUnusedImages)) await boss.work('territoryBilling', jobWrapper(territoryBilling)) + await boss.work('ofac', jobWrapper(ofac)) console.log('working jobs') } diff --git a/worker/ofac.js b/worker/ofac.js new file mode 100644 index 00000000..13289396 --- /dev/null +++ b/worker/ofac.js @@ -0,0 +1,78 @@ +import { createReadStream, createWriteStream, unlinkSync } from 'fs' +import unzipper from 'unzipper' +import csvParser from 'csv-parser' +import stream from 'stream' + +const sanctionedCountryCodes = ['IR', 'KP', 'SY', 'RU'] + +const IPV4_URL = 'https://ipapi.is/data/geolocationDatabaseIPv4.csv.zip' +const IPV6_URL = 'https://ipapi.is/data/geolocationDatabaseIPv6.csv.zip' + +export async function ofac ({ models }) { + const ipv4CSVPath = 'ipv4.temp.csv' + const ipv6CSVPath = 'ipv6.temp.csv' + + async function loadCSVIntoDatabase (csvFilePath) { + const results = [] + + return new Promise((resolve, reject) => { + createReadStream(csvFilePath) + .pipe(csvParser()) + .on('data', (data) => { + if (sanctionedCountryCodes.includes(data.country_code)) { + results.push({ + startIP: data.start_ip, + endIP: data.end_ip, + country: data.country, + countryCode: data.country_code + }) + } + }) + .on('end', async () => { + console.log('results', results.length) + await models.$queryRaw` + INSERT INTO "OFAC" ("startIP", "endIP", "country", "countryCode") + SELECT "startIP", "endIP", "country", "countryCode" FROM json_populate_recordset(null::"OFAC", ${JSON.stringify(results)}::JSON)` + console.log('Data imported into the database') + resolve() + }) + .on('error', reject) + }) + } + + try { + await downloadAndUnzipCSV(IPV4_URL, ipv4CSVPath) + await downloadAndUnzipCSV(IPV6_URL, ipv6CSVPath) + await models.$executeRaw`TRUNCATE TABLE "OFAC"` + await loadCSVIntoDatabase(ipv4CSVPath) + await loadCSVIntoDatabase(ipv6CSVPath) + } finally { + unlinkSync(ipv4CSVPath) + unlinkSync(ipv6CSVPath) + } +} + +async function downloadAndUnzipCSV (url, outputFilePath) { + const response = await fetch(url) + if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`) + + return new Promise((resolve, reject) => { + stream.Readable.fromWeb(response.body) + .pipe(unzipper.Parse()) + .on('entry', function (entry) { + console.log('Extracting', entry.path) + if (entry.path.endsWith('.csv')) { + const fileStream = createWriteStream(outputFilePath) + entry.pipe(fileStream) + fileStream.on('finish', () => { + console.log('File write completed:', outputFilePath) + resolve() + }) + fileStream.on('error', reject) + } else { + entry.autodrain() + } + }) + .on('error', reject) + }) +}